This commit is contained in:
Bane Stojkovic
2026-10-06 08:51:32 +02:00
parent 2e2ab93abb
commit dc863e18f4
4 changed files with 347 additions and 100 deletions
@@ -555,7 +555,7 @@
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>TFT Display - 2.8 inch and 2.4 inch ILI9341, 1.8 inch ST7735, 1.54 inch ST7789, 1.28 inch round GC9A01</description>
<description>TFT Display - 2.8 inch and 2.4 inch ILI9341, 1.8 inch and 0.96 inch ST7735, 1.54 inch ST7789, 1.28 inch round GC9A01</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
@@ -85,6 +85,14 @@ public class TFTDisplay extends AbstractMakerBoard {
public static final String[] PIN_NAMES_SD =
new String[] {"SD_CS", "SD_MOSI", "SD_MISO", "SD_SCK"};
/**
* The eight-pin row the 1.8" and the 1.54" share, and the same lines the 0.96" brings out. Two
* different controllers behind one header: what a board of this size exposes is the four-wire SPI
* interface plus reset, data/command and the backlight, and that does not vary with the driver.
*/
public static final String[] PIN_NAMES_SPI_8PIN =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS", "BLK"};
public static Color TFT_RED = Color.decode("#C0392B");
public static Color SCREEN_BG = Color.decode("#111111");
public static Color GLASS_COLOR = Color.decode("#1A1A1A");
@@ -169,11 +177,15 @@ public class TFTDisplay extends AbstractMakerBoard {
}
/**
* Top edge of the board. Every variant carries its main header on the bottom edge, so the board
* always stands above control point zero and the offset is always measured up from the bottom.
* Top edge of the board. Most variants carry their main header on the bottom edge and stand above
* control point zero, so the offset is measured up from the bottom; the 0.96" has its row on the
* top edge and hangs below it instead.
*/
private double getBoardY(double y) {
Controller controller = getController();
if (controller.isHeaderAtTop()) {
return y - px(controller.getHeaderOffsetMm());
}
return y - px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm());
}
@@ -190,15 +202,18 @@ public class TFTDisplay extends AbstractMakerBoard {
relativeOffsets[i][1] = 0;
}
// The second row faces the first across the board -- always above it, since the main header is
// always on the bottom edge -- so its distance from control point zero is what the two offsets
// leave of the length. Both rows are centred on the width, which puts the shorter one in by
// half the difference between the spans; that horizontal placement is the one figure of this
// header that is not measured.
// The second row faces the first across the board, so its distance from control point zero is
// what the two offsets leave of the length -- above the first row on a board headed at the
// bottom and below it on one headed at the top. Both rows are centred on the width, which puts
// the shorter one in by half the difference between the spans; that horizontal placement is the
// one figure of this header that is not measured.
if (secondaryCount > 0) {
double indent = (primaryCount - secondaryCount) * spacing / 2.0;
double across = -px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm()
double across = px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm()
- controller.getSecondaryHeaderOffsetMm());
if (!controller.isHeaderAtTop()) {
across = -across;
}
for (int i = 0; i < secondaryCount; i++) {
relativeOffsets[primaryCount + i][0] = indent + i * spacing;
relativeOffsets[primaryCount + i][1] = across;
@@ -285,15 +300,18 @@ public class TFTDisplay extends AbstractMakerBoard {
double holeInset = px(controller.getHoleInsetMm());
double headerSideInset = px(controller.getHeaderSideHoleInsetMm());
double holeSize = px(controller.getHoleSizeMm());
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
// The deeper inset, where a board has one, belongs to the pair sharing the header's edge.
double topInset = controller.isHeaderAtTop() ? headerSideInset : holeInset;
double bottomInset = controller.isHeaderAtTop() ? holeInset : headerSideInset;
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + topInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + topInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - bottomInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset,
boardY + boardH - headerSideInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - headerSideInset, holeSize);
bandTop = boardY + holeInset;
bandBottom = boardY + boardH - headerSideInset;
boardY + boardH - bottomInset, holeSize);
bandTop = boardY + topInset;
bandBottom = boardY + boardH - bottomInset;
}
double screenW = px(controller.getScreenWidthMm());
@@ -310,23 +328,33 @@ public class TFTDisplay extends AbstractMakerBoard {
screenH);
} else {
double bandCentre = (bandTop + bandBottom) / 2.0;
double screenX;
double screenY;
if (controller.hasGlass()) {
double glassH = px(controller.getGlassLengthMm());
Glass glass = controller.getGlass();
double glassH = px(glass.getLengthMm());
double glassW = glass.spansTheBoard() ? boardW : px(glass.getWidthMm());
double glassX = glass.spansTheBoard() ? boardX
: boardX + boardW - px(glass.getRightInsetMm()) - glassW;
double glassY = bandCentre - glassH / 2.0;
g2d.setColor(GLASS_COLOR);
g2d.fill(new Rectangle2D.Double(boardX, glassY, boardW, glassH));
// The lit area is centred in the glass unless the variant measured it, which the 2.4"
// does: its panel carries the driver's bonding region along the bottom, so the lit area
// sits high in the frame and centring it would drop it 5 mm.
g2d.fill(new Rectangle2D.Double(glassX, glassY, glassW, glassH));
// The lit area belongs to the panel rather than to the board, so it is centred in the
// glass -- which is the same thing as centring it on the board for every variant whose
// glass spans the board, and is not for the 0.96", whose panel is pushed to one side.
// It is centred vertically too unless the variant measured it, which the 2.4" does: its
// panel carries the driver's bonding region along the bottom, so the lit area sits high
// in the frame and centring it would drop it 5 mm.
screenX = glassX + (glassW - screenW) / 2.0;
screenY = controller.hasMeasuredScreenTop() ? boardY + px(controller.getScreenTopMm())
: glassY + (glassH - screenH) / 2.0;
} else {
screenX = boardX + (boardW - screenW) / 2.0;
screenY = controller.hasMeasuredScreenTop() ? boardY + px(controller.getScreenTopMm())
: bandCentre - screenH / 2.0;
}
panel = new Rectangle2D.Double(boardX + (boardW - screenW) / 2.0, screenY, screenW,
screenH);
panel = new Rectangle2D.Double(screenX, screenY, screenW, screenH);
}
g2d.setColor(SCREEN_BG);
@@ -349,13 +377,14 @@ public class TFTDisplay extends AbstractMakerBoard {
// The names go in the strip between the pin row and the panel, lying flat along the row as
// the Nokia's do: the strip is 3.5 mm deep on the two smallest boards and a label stood on
// end needs 4.7 mm. The row is on the bottom edge on every variant, so the strip, and the
// names in it, are always above the pins.
// names in it, are above the pins on a board headed at the bottom and below them on the
// 0.96", which is headed at the top.
//
// Only the primary row is named. The SD row has the glass a couple of millimetres from it and
// nothing but board edge on its other side, so there is no strip to print in; those four pins
// are left to their tooltips, as the matrix's covered rows are.
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, controller.getPinNames().length,
false, SILK_COLOR);
controller.isHeaderAtTop(), SILK_COLOR);
}
g2d.setTransform(oldTx);
@@ -402,8 +431,10 @@ public class TFTDisplay extends AbstractMakerBoard {
// The 2.8"'s micro-SD socket comes out on a four-pin row of its own, the way the 2.4"'s does,
// on the top edge opposite the main header. It keeps the board's own 3 mm offset rather than
// the 2.4"'s 2 mm, each board putting its second row as far in as its first.
ILI9341_2_8("2.8\" ILI9341 240x320 (Touch + SD)", 50.0d, 86.0d, 43.2d, 57.6d, 69.1d, 3.0d,
3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 0d, PIN_NAMES_SD, 3.0d, 3.0d),
ILI9341_2_8("2.8\" ILI9341 240x320 (Touch + SD)", 50.0d, 86.0d, 43.2d, 57.6d, new Glass(69.1d),
3.0d, 3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 0d, PIN_NAMES_SD, 3.0d, 3.0d, false),
// Four of the six are headed at the top; the two ILI9341s and the round board are not. There is
// no default to fall back on, so every constant states its own edge -- see the plan's 12.2.3.
// The one board here measured rather than derived throughout, and the only one with a second
// header. Four of its figures do not behave like the others': the 14-pin row is on the bottom
// edge, the lit area is measured 9.26 mm down from the top rather than centred in the glass,
@@ -412,23 +443,33 @@ public class TFTDisplay extends AbstractMakerBoard {
// row. The glass keeps the family's rule, centred between the hole rows, which the deeper pair
// puts at 6.50 to 66.76 mm: 2.76 mm of frame above the lit area and 8.54 mm below it, the
// asymmetry being the driver's bonding region along the bottom of the panel.
ILI9341_2_4("2.4\" ILI9341 240x320 (Touch + SD)", 42.72d, 77.18d, 36.72d, 48.96d, 60.26d, 2.0d,
3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 9.26d, PIN_NAMES_SD, 2.0d, 6.92d),
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"}),
ST7789_1_54("1.54\" ST7789 240x240", 32.0d, 43.72d, 27.66d, 27.66d, 33.7d, 1.5d, 2.5d, 2.0d,
0d,
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS", "BLK"}),
GC9A01_1_28("1.28\" GC9A01 240x240 Round", 38.0d, 45.5d, 32.5d, 32.5d, 0d, 1.76d, 0d, 0d,
ILI9341_2_4("2.4\" ILI9341 240x320 (Touch + SD)", 42.72d, 77.18d, 36.72d, 48.96d,
new Glass(60.26d), 2.0d, 3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 9.26d, PIN_NAMES_SD, 2.0d,
6.92d, false),
// The one board in the family whose header is on the top edge, and the only one whose panel
// does not span the board: a 23.7 x 12.8 mm bezel pushed into the right-hand side, 2 mm from
// that edge and 4.3 mm from the other, with the driver's circuitry filling the bare L the
// offset leaves. Its lit area is landscape where every other board's is portrait, it is
// measured rather than derived, and it is centred in the bezel with a 1 mm frame all round.
// The bezel is offset only across the board; down it, it keeps the family's rule and sits
// centred, which on a board whose holes share one inset is the same as centred in the PCB.
ST7735_0_96("0.96\" ST7735 80x160", 30.0d, 24.0d, 21.7d, 10.8d,
new Glass(12.8d, 23.7d, 2.0d), 1.44d, 2.0d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 0d, null, 0d,
2.0d, true),
ST7735_1_8("1.8\" ST7735 128x160", 34.0d, 45.8d, 28.56d, 35.7d, null, 1.5d, 3.0d, 3.0d, 0d,
PIN_NAMES_SPI_8PIN, true),
ST7789_1_54("1.54\" ST7789 240x240", 32.0d, 43.72d, 27.66d, 27.66d, new Glass(33.7d), 1.5d,
2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, true),
GC9A01_1_28("1.28\" GC9A01 240x240 Round", 38.0d, 45.5d, 32.5d, 32.5d, null, 1.76d, 0d, 0d,
22.9d,
new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"});
new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"}, false);
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 Glass glass;
private final double headerOffsetMm;
private final double holeInsetMm;
private final double holeSizeMm;
@@ -438,26 +479,27 @@ public class TFTDisplay extends AbstractMakerBoard {
private final String[] secondaryPinNames;
private final double secondaryHeaderOffsetMm;
private final double headerSideHoleInsetMm;
private final boolean headerAtTop;
/** For a board whose lit area is centred and whose pins all leave on one row. */
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, boardWidthMm, boardLengthMm, screenWidthMm, screenLengthMm, glassLengthMm,
headerOffsetMm, holeInsetMm, holeSizeMm, tabWidthMm, pinNames, 0d, null, 0d, holeInsetMm);
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, boolean headerAtTop) {
this(label, boardWidthMm, boardLengthMm, screenWidthMm, screenLengthMm, glass, headerOffsetMm,
holeInsetMm, holeSizeMm, tabWidthMm, pinNames, 0d, null, 0d, holeInsetMm, headerAtTop);
}
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, double glassLengthMm, double headerOffsetMm, double holeInsetMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, double screenTopMm,
String[] secondaryPinNames, double secondaryHeaderOffsetMm,
double headerSideHoleInsetMm) {
String[] secondaryPinNames, double secondaryHeaderOffsetMm, double headerSideHoleInsetMm,
boolean headerAtTop) {
this.label = label;
this.boardWidthMm = boardWidthMm;
this.boardLengthMm = boardLengthMm;
this.screenWidthMm = screenWidthMm;
this.screenLengthMm = screenLengthMm;
this.glassLengthMm = glassLengthMm;
this.glass = glass;
this.headerOffsetMm = headerOffsetMm;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
@@ -467,6 +509,7 @@ public class TFTDisplay extends AbstractMakerBoard {
this.secondaryPinNames = secondaryPinNames;
this.secondaryHeaderOffsetMm = secondaryHeaderOffsetMm;
this.headerSideHoleInsetMm = headerSideHoleInsetMm;
this.headerAtTop = headerAtTop;
}
@Override public String toString() { return label; }
@@ -474,7 +517,8 @@ public class TFTDisplay extends AbstractMakerBoard {
public double getBoardLengthMm() { return boardLengthMm; }
public double getScreenWidthMm() { return screenWidthMm; }
public double getScreenLengthMm() { return screenLengthMm; }
public double getGlassLengthMm() { return glassLengthMm; }
/** The dark panel, or {@code null} on a board whose lit area is all there is to draw. */
public Glass getGlass() { return glass; }
public double getHeaderOffsetMm() { return headerOffsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
public String[] getPinNames() { return pinNames; }
@@ -494,7 +538,14 @@ public class TFTDisplay extends AbstractMakerBoard {
public boolean hasMountingHoles() { return holeSizeMm > 0; }
public boolean hasGlass() { return glassLengthMm > 0; }
public boolean hasGlass() { return glass != null; }
/**
* True where the main row leaves by the top edge, so the board hangs below it. Four of the six
* do; see the plan's section 12.2.3 for why the family is not uniform and why no constant may
* leave this to a default.
*/
public boolean isHeaderAtTop() { return headerAtTop; }
/** Width of the tab the header sits on; meaningful only for a round board. */
public double getTabWidthMm() { return tabWidthMm; }
@@ -518,4 +569,42 @@ public class TFTDisplay extends AbstractMakerBoard {
public double getSecondaryHeaderOffsetMm() { return secondaryHeaderOffsetMm; }
}
/**
* The dark panel bonded to the front of the board, which is not the same thing as the lit area:
* it is the glass the lit area sits in, and the frame it leaves around it is what a reader sees
* as the display's border. Every panel is centred between its board's mounting holes, so what
* varies is how far across the board it reaches. Carried as an object rather than as more figures
* on {@link Controller} because only one board so far needs more than its height, and a
* constructor that already takes fifteen arguments should not take seventeen.
*
* @author Branislav Stojkovic
*/
public static class Glass {
private final double lengthMm;
private final double widthMm;
private final double rightInsetMm;
/** A panel that spans the board. */
public Glass(double lengthMm) {
this(lengthMm, 0d, 0d);
}
public Glass(double lengthMm, double widthMm, double rightInsetMm) {
this.lengthMm = lengthMm;
this.widthMm = widthMm;
this.rightInsetMm = rightInsetMm;
}
public double getLengthMm() { return lengthMm; }
/** Zero where the panel is as wide as the board, which is every variant but the 0.96". */
public double getWidthMm() { return widthMm; }
/** How far the panel stops short of the right edge; meaningful only where it is narrower. */
public double getRightInsetMm() { return rightInsetMm; }
public boolean spansTheBoard() { return widthMm <= 0; }
}
}
@@ -91,6 +91,7 @@ public class TFTDisplayTest {
}
Assert.assertEquals(14, Controller.ILI9341_2_8.getPinNames().length);
Assert.assertEquals(14, Controller.ILI9341_2_4.getPinNames().length);
Assert.assertEquals(8, Controller.ST7735_0_96.getPinNames().length);
Assert.assertEquals(8, Controller.ST7735_1_8.getPinNames().length);
Assert.assertEquals(8, Controller.ST7789_1_54.getPinNames().length);
Assert.assertEquals(7, Controller.GC9A01_1_28.getPinNames().length);
@@ -127,21 +128,26 @@ public class TFTDisplayTest {
}
}
/**
* Three boards, two controllers, one header: what a module of this size brings out is the
* four-wire SPI interface plus reset, data/command and the backlight, and that does not vary with
* the driver behind it. Shared by identity rather than by three copies, so that a correction to
* one reaches all of them.
*/
@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));
}
}
@Test
public void squareBoardPinNamesComeFromTheController() {
TFTDisplay display = of(Controller.ST7789_1_54);
public void theSmallBoardsShareOnePinArray() {
Controller[] small =
{Controller.ST7735_0_96, Controller.ST7735_1_8, Controller.ST7789_1_54};
String[] expected = new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS", "BLK"};
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
for (Controller controller : small) {
Assert.assertSame(controller.toString(), TFTDisplay.PIN_NAMES_SPI_8PIN,
controller.getPinNames());
TFTDisplay display = of(controller);
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals(controller + " pin " + i, expected[i],
display.getControlPointNodeName(i));
}
}
}
@@ -173,8 +179,8 @@ public class TFTDisplayTest {
* The SD row faces the 14-pin row across the board and is centred on the width the way the longer
* row is. Both are worth pinning: the across-the-board figure is what the two offsets leave of
* the length rather than a measurement, and the horizontal placement is the one figure of this
* header that was not supplied at all. Both boards that carry one put it on the top edge, above
* the main row.
* header that was not supplied at all. Both boards that carry one are headed at the bottom, so
* their second row is on the top edge.
*/
@Test
public void theSdRowStandsOffTheOppositeEdge() {
@@ -187,12 +193,18 @@ public class TFTDisplayTest {
int primaryCount = controller.getPinNames().length;
Point2D firstSd = display.getControlPoint(primaryCount);
Assert.assertEquals(controller + " SD row offset from the top edge",
controller.getSecondaryHeaderOffsetMm(), mm(firstSd.getY() - board.getY()), 0.01d);
double fromItsEdge = controller.isHeaderAtTop() ? board.getMaxY() - firstSd.getY()
: firstSd.getY() - board.getY();
Assert.assertEquals(controller + " SD row offset from its own edge",
controller.getSecondaryHeaderOffsetMm(), mm(fromItsEdge), 0.01d);
double between = controller.isHeaderAtTop()
? firstSd.getY() - display.getControlPoint(0).getY()
: display.getControlPoint(0).getY() - firstSd.getY();
Assert.assertEquals(controller + " rows are not on opposite edges",
controller.getBoardLengthMm() - controller.getHeaderOffsetMm()
- controller.getSecondaryHeaderOffsetMm(),
mm(display.getControlPoint(0).getY() - firstSd.getY()), 0.01d);
mm(between), 0.01d);
Point2D lastSd = display.getControlPoint(display.getControlPointCount() - 1);
Assert.assertEquals(controller + " SD row is not centred across the board",
@@ -248,14 +260,12 @@ public class TFTDisplayTest {
}
double holeRadius = px(controller.getHoleSizeMm()) / 2.0;
double boardH = px(controller.getBoardLengthMm());
double bandTop = px(controller.getHoleInsetMm());
double bandBottom = boardH - px(controller.getHeaderSideHoleInsetMm());
double bandTop = px(topInsetOf(controller));
double bandBottom = boardH - px(bottomInsetOf(controller));
double bandCentre = (bandTop + bandBottom) / 2.0;
double panelH = controller.hasGlass() ? px(controller.getGlassLengthMm())
: px(controller.getScreenLengthMm());
double panelTop = bandCentre - panelH / 2.0;
double panelBottom = bandCentre + panelH / 2.0;
double panelH = panelLengthOf(controller);
double panelTop = panelTopOf(controller);
double panelBottom = panelTop + panelH;
Assert.assertTrue(controller + " panel overlaps the holes on its top edge",
panelTop >= bandTop + holeRadius);
@@ -284,12 +294,14 @@ public class TFTDisplayTest {
double inset = px(controller.getHoleInsetMm());
double headerSide = px(controller.getHeaderSideHoleInsetMm());
double size = px(controller.getHoleSizeMm());
double topInset = controller.isHeaderAtTop() ? headerSide : inset;
double bottomInset = controller.isHeaderAtTop() ? inset : headerSide;
Rectangle2D[] holes = new Rectangle2D[] {
hole(board.getX() + inset, board.getY() + inset, size),
hole(board.getMaxX() - inset, board.getY() + inset, size),
hole(board.getX() + inset, board.getMaxY() - headerSide, size),
hole(board.getMaxX() - inset, board.getMaxY() - headerSide, size)};
hole(board.getX() + inset, board.getY() + topInset, size),
hole(board.getMaxX() - inset, board.getY() + topInset, size),
hole(board.getX() + inset, board.getMaxY() - bottomInset, size),
hole(board.getMaxX() - inset, board.getMaxY() - bottomInset, size)};
for (int i = 0; i < display.getControlPointCount(); i++) {
Point2D p = display.getControlPoint(i);
@@ -307,6 +319,32 @@ public class TFTDisplayTest {
return new Rectangle2D.Double(cx - size / 2.0, cy - size / 2.0, size, size);
}
private static double topInsetOf(Controller controller) {
return controller.isHeaderAtTop() ? controller.getHeaderSideHoleInsetMm()
: controller.getHoleInsetMm();
}
private static double bottomInsetOf(Controller controller) {
return controller.isHeaderAtTop() ? controller.getHoleInsetMm()
: controller.getHeaderSideHoleInsetMm();
}
/** The dark panel where there is one, and the lit area alone where there is not. */
private static double panelLengthOf(Controller controller) {
return px(controller.hasGlass() ? controller.getGlass().getLengthMm()
: controller.getScreenLengthMm());
}
/** Mirrors the drawing rule: every panel is centred between the hole rows. */
private static double panelTopOf(Controller controller) {
double bandTop = controller.hasMountingHoles() ? px(topInsetOf(controller))
: px(controller.getHeaderOffsetMm());
double bandBottom = controller.hasMountingHoles()
? px(controller.getBoardLengthMm() - bottomInsetOf(controller))
: px(controller.getBoardLengthMm());
return (bandTop + bandBottom) / 2.0 - panelLengthOf(controller) / 2.0;
}
/**
* The panel has to leave the pin row uncovered. It is placed between the hole rows rather than
* from an edge, so a long panel on a board whose holes sit close to the edges can reach back over
@@ -321,17 +359,16 @@ public class TFTDisplayTest {
continue;
}
double boardH = controller.getBoardLengthMm();
double bandTop = controller.hasMountingHoles() ? controller.getHoleInsetMm()
: controller.getHeaderOffsetMm();
double bandBottom = controller.hasMountingHoles()
? boardH - controller.getHeaderSideHoleInsetMm() : boardH;
double panelH = controller.hasGlass() ? controller.getGlassLengthMm()
: controller.getScreenLengthMm();
double panelTop = (bandTop + bandBottom) / 2.0 - panelH / 2.0;
double panelBottom = panelTop + panelH;
double panelTop = mm(panelTopOf(controller));
double panelBottom = panelTop + mm(panelLengthOf(controller));
Assert.assertTrue(controller + " panel covers the main row",
panelBottom < boardH - controller.getHeaderOffsetMm());
double mainRow = controller.isHeaderAtTop() ? controller.getHeaderOffsetMm()
: boardH - controller.getHeaderOffsetMm();
if (controller.isHeaderAtTop()) {
Assert.assertTrue(controller + " panel covers the main row", panelTop > mainRow);
} else {
Assert.assertTrue(controller + " panel covers the main row", panelBottom < mainRow);
}
if (controller.hasSecondaryHeader()) {
Assert.assertTrue(controller + " panel covers the SD row",
panelTop > controller.getSecondaryHeaderOffsetMm());
@@ -355,11 +392,8 @@ public class TFTDisplayTest {
continue;
}
double boardH = controller.getBoardLengthMm();
double glassH = controller.getGlassLengthMm();
double glassTop =
(controller.getHoleInsetMm() + boardH - controller.getHeaderSideHoleInsetMm()) / 2.0
- glassH / 2.0;
double glassH = controller.getGlass().getLengthMm();
double glassTop = mm(panelTopOf(controller));
double screenTop = controller.getScreenTopMm();
Assert.assertTrue(controller + " lit area starts above its glass", screenTop >= glassTop);
@@ -370,6 +404,39 @@ public class TFTDisplayTest {
}
}
/**
* The 0.96" is the only board whose panel does not span its width: a 23.7 mm bezel on a 30 mm
* board, pushed to the right so that the driver's circuitry has the bare L it leaves. That offset
* is across the board only -- down it the bezel is centred like every other variant's -- and it
* is a measurement rather than a derivation, since centring it across the board too would move it
* 1.15 mm. The lit area is centred inside it with a 1 mm frame on all four sides, which the
* drawing gets for free once the glass is placed, and which would silently break if the lit area
* went back to being centred on the board.
*/
@Test
public void theOffsetPanelIsPlacedFromTheRightEdge() {
for (Controller controller : Controller.values()) {
Assert.assertEquals(controller + " panel spans the board",
controller != Controller.ST7735_0_96,
!controller.hasGlass() || controller.getGlass().spansTheBoard());
}
Controller controller = Controller.ST7735_0_96;
TFTDisplay.Glass glass = controller.getGlass();
double leftMargin = controller.getBoardWidthMm() - glass.getRightInsetMm() - glass.getWidthMm();
Assert.assertEquals("left margin", 4.3d, leftMargin, 0.01d);
Assert.assertTrue("the bezel is where centring would have put it",
Math.abs(leftMargin - (controller.getBoardWidthMm() - glass.getWidthMm()) / 2.0) > 0.1d);
Assert.assertEquals("frame left and right", 1.0d,
(glass.getWidthMm() - controller.getScreenWidthMm()) / 2.0, 0.01d);
Assert.assertEquals("frame top and bottom", 1.0d,
(glass.getLengthMm() - controller.getScreenLengthMm()) / 2.0, 0.01d);
Assert.assertFalse("its lit area is measured, not derived from a centred glass",
controller.hasMeasuredScreenTop());
}
@Test
public void mountingHolesSitInsideTheBoard() {
for (Controller controller : Controller.values()) {
@@ -394,9 +461,10 @@ public class TFTDisplayTest {
*/
@Test
public void mountingHoleFiguresAreEachBoardsOwn() {
double[][] expected = {{3.0d, 3.0d}, {3.0d, 3.0d}, {3.0d, 3.0d}, {2.5d, 2.0d}};
double[][] expected =
{{3.0d, 3.0d}, {3.0d, 3.0d}, {2.0d, 2.0d}, {3.0d, 3.0d}, {2.5d, 2.0d}};
Controller[] withHoles = {Controller.ILI9341_2_8, Controller.ILI9341_2_4,
Controller.ST7735_1_8, Controller.ST7789_1_54};
Controller.ST7735_0_96, Controller.ST7735_1_8, Controller.ST7789_1_54};
for (int i = 0; i < withHoles.length; i++) {
Assert.assertEquals(withHoles[i] + " inset", expected[i][0], withHoles[i].getHoleInsetMm(),
0.01d);
@@ -503,21 +571,33 @@ public class TFTDisplayTest {
}
/**
* Every board in the family carries its main header on the bottom edge and stands above it, so
* switching the variant property never turns a drawing end for end. The boards disagreed on this
* for a while -- the round one and the 2.4" at the bottom, the other three at the top -- and a
* user who changed variants found every wire landing on the wrong end of the part.
* Which edge the main row leaves by, which is a fact about each board rather than a drawing
* convention: with the pin order fixed left to right, a header at the top and a header at the
* bottom are mirror images and not the same part. The two ILI9341s and the round board stand
* above their row; the
* 0.96", the 1.8" and the 1.54" hang below theirs. Asserted for every variant because nothing
* else in the geometry would notice one being flipped.
*/
@Test
public void everyBoardStandsAboveItsHeader() {
public void headerSitsOnItsOwnEdge() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Rectangle2D bounds = display.getBodyShape().getBounds2D();
Point2D firstPin = display.getControlPoint(0);
double fromEdge = controller.isHeaderAtTop() ? firstPin.getY() - bounds.getY()
: bounds.getMaxY() - firstPin.getY();
Assert.assertEquals(controller + " header offset", controller.getHeaderOffsetMm(),
mm(bounds.getMaxY() - firstPin.getY()), 0.01d);
mm(fromEdge), 0.01d);
}
// A roll call rather than a rule: every one of these was sourced from the part, and a future
// variant must be too rather than inheriting whichever way the majority happens to go.
Assert.assertTrue("0.96\"", Controller.ST7735_0_96.isHeaderAtTop());
Assert.assertTrue("1.8\"", Controller.ST7735_1_8.isHeaderAtTop());
Assert.assertTrue("1.54\"", Controller.ST7789_1_54.isHeaderAtTop());
Assert.assertFalse("2.8\"", Controller.ILI9341_2_8.isHeaderAtTop());
Assert.assertFalse("2.4\"", Controller.ILI9341_2_4.isHeaderAtTop());
Assert.assertFalse("1.28\" round", Controller.GC9A01_1_28.isHeaderAtTop());
}
/**
+79 -1
View File
@@ -1752,6 +1752,83 @@ reach the hole columns, while the 2.8"'s same row on a 50 mm board passes inside
SD row does on both. `mountingHolesDoNotOverlapAnyPin` asserts that over every variant and every
pin, and it is what now justifies the 6.92 mm rather than a figure anyone has to take on trust.
### 12.2.3 The 0.96" ST7735, and what it proved about §12.2.2
The first board added after the standardisation broke it. The 0.96" 80x160 ST7735 carries its
eight-pin row on the **top** edge — the maintainer gave the figure and a photograph of the part —
so it hangs below its row where the other five stand above theirs.
**That is not a drawing preference, which is what §12.2.2 assumed it was.** Control point zero is
always the leftmost pin, so with the pin order fixed, a header at the top and a header at the bottom
are mirror images of each other rather than the same board turned round. Which edge the row leaves
by is therefore a fact about the part, in the same class as its outline, and it has to be sourced
per board rather than settled once for the family.
§12.2.2's *reason* still stands — switching variants should not flip a drawing — but it loses to
drawing the part correctly, which is the stronger obligation. `isHeaderAtTop()` is back, carried per
variant; `getBoardY`, the second row's sign and the silk's side all branch on it again. What does
**not** come back is the thing that made the old flag wrong: it no longer stands in for `isRound()`,
and the deeper header-side hole inset now follows the header's edge rather than assuming the bottom.
**Two boards were flipped by rule, and the rule had them backwards.** The 1.8" ST7735 and the
1.54" ST7789 went bottom-headed in §12.2.2 on the premise that the family shared one orientation,
with neither orientation sourced. Checked against the parts, **both are headed at the top**, so
§12.2.2 had moved them away from the truth rather than towards it — the standardisation's one
lasting effect on those two was to make them wrong, and it went unnoticed for exactly as long as no
one looked.
The family divides three and three: the 0.96", the 1.8" and the 1.54" are headed at the top, the
two ILI9341s and the round board at the bottom. That there is no majority to fall back on is useful
rather than awkward, because it removes the temptation to have a default: **the short constructor
now takes the orientation too, so every constant states its own edge and none inherits one.** A
future variant has to answer the question rather than be answered for.
`headerSitsOnItsOwnEdge` ends with a roll call of all six rather than a rule, which is the only
form an assertion about sourced facts can honestly take.
Checking the orientation also corrected the 1.8"'s pin names, which had read `SCK` and `BL` where
the board prints `SCL` and `BLK`. That left its array identical to the 1.54"'s and to the 0.96"'s,
so all three now share `PIN_NAMES_SPI_8PIN`, asserted by identity: two different controllers behind
one header, because what a module of this size brings out is the four-wire SPI interface plus reset,
data/command and the backlight, and that does not vary with the driver. Two near-duplicate pin-name
tests collapsed into one.
### 12.2.4 Two structural additions the 0.96" needed
**A panel that does not span the board.** Every other variant's glass runs the full width, so the
drawing filled it edge to edge and centred the lit area on the board. This one's bezel is 23.7 mm on
a 30 mm board, pushed 2 mm from the right edge and leaving 4.3 mm on the left — the bare L is where
the driver circuitry sits. The offset is **across the board only**; down it the bezel is centred in
the PCB, which on a board whose four holes share one inset is the family's existing rule and needs
no figure at all.
That was not obvious from the figures as first supplied, which placed the bezel 5.15 mm from the top
edge against 5.6 mm for centring. A 0.45 mm discrepancy was carried as a measurement and given a
`topMm` on the new `Glass`, and the maintainer then corrected it: the bezel is centred. The field
came out again, having had exactly one user and never a real one. Worth recording as a near miss of
the kind §12.2.1's bezel paragraph caught and this one did not — a supplied figure that sits within
half a millimetre of what the existing rule already produces is more likely a reading than a
departure, and is worth querying before it earns a mechanism.
Two per-variant figures remain, which is still enough to want an object on a constructor that
already took fifteen arguments: the glass figures live in a nested `Glass` carrying length, width
and right inset — the same treatment `OLEDDisplay.Version` gives its `Layout`. A board with no
separate dark panel passes `null` instead of a zero length, which says the same thing more plainly.
**A lit area that belongs to the panel rather than to the board.** The screen is now centred in the
glass horizontally rather than on the board. For the five variants whose glass spans the board those
are the same point, so nothing moved; for the 0.96" they differ by 1.15 mm. The vertical rule is
unchanged — centred in the glass unless the variant measured it, which only the 2.4" does.
The 0.96"'s own lit area is measured (21.7 x 10.8 mm) rather than derived from the diagonal, and it
is landscape where every other board's is portrait. It sits centred in the bezel with a 1 mm frame on
all four sides, which `theOffsetPanelIsPlacedFromTheRightEdge` asserts along with the horizontal
offset being a figure that centring would have got wrong.
One name was settled by the maintainer rather than copied through: the pin list supplied read `DS`
in seventh place, which is `CS` on the ST7735 and on the 1.8" board already in the enum. Node names
reach the netlist, so it was asked rather than assumed.
**The 2.8" wanted the same row, and it was a missing header rather than an enhancement.** Its
label has always read "(Touch + SD)" while its 14 pins are display and touch only, so the socket's
four lines had nowhere to go. It now carries `PIN_NAMES_SD` on the same terms as the 2.4": same
@@ -1795,7 +1872,8 @@ rediscover that.
Ranked by how often the part appears in builds, not by how cheap it is to draw.
1. ~~**2.4" ILI9341**~~ — **done**, §12.2 and §12.2.1.
2. **0.96" 80x160 ST7735**, and the 1.14" and 2.0" ST7789 that the Pico ecosystem buys.
2. ~~**0.96" 80x160 ST7735**~~ — **done**, §12.2.3 and §12.2.4. Still open in this family: the
1.3" 240x240 ST7789, and the 1.14" and 2.0" ST7789 that the Pico ecosystem buys.
3. **MAX7219 8-digit 7-segment module** — the clock and counter staple, and this package already
models a MAX7219 cascade for the matrix. Needs §12.3's `digitCount` work first.
4. **6-digit TM1637 and 2-digit 0.56"** — the rest of the clock and thermometer range.