This commit is contained in:
Bane Stojkovic
2026-10-03 22:58:38 +02:00
parent d4d6da7bd8
commit 8693215c9c
5 changed files with 120 additions and 84 deletions
+26 -10
View File
@@ -505,25 +505,25 @@
<releaseDate>2026-09-30 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Character LCD - 16x2 and 20x4, with I2C backpack or 16-pin parallel interface</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>0.96" OLED Display (SSD1306) - I2C and SPI</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>7-Segment Display - 1-digit 0.56 inch, 4-digit in 0.36 inch and 0.56 inch, and TM1637 driver module</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>8x8 LED Matrix (MAX7219)</description>
<description>Character LCD - 16x2 and 20x4, with I2C backpack or 16-pin parallel interface</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>TFT Touch Screen (ILI9341) - 2.8 inch 240x320 with touch and SD card slot</description>
<description>LED Bar Graph - 8, 10 and 12 segments</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>LED Matrix (MAX7219) - single 8x8, compact 8x8, 4-in-1 32x8</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Jewel (WS2812B) - 7 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
@@ -533,6 +533,22 @@
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Stick (WS2812B) - 8 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Strip (WS2812B) - 30, 60 and 144 LEDs per meter, up to 144 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Nokia 5110 LCD (PCD8544)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>0.96" OLED Display (SSD1306) - I2C and SPI</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>TFT Display - 2.8 inch ILI9341, 1.8 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>
<description>Bill of Materials now lists the selected variant of microcontroller boards and displays, so different versions of the same board are no longer merged into a single row</description>
@@ -524,43 +524,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
}
/**
* Helper to draw pin names beside a vertical column of pins, reading left to right as they do on
* a row. A column has the whole pin pitch to stack its labels in rather than the width of one,
* so unlike {@link #drawFlatRowPinLabels} it is the board, not the pitch, that limits how long a
* name can be.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated first control point X coordinate (P0.getX())
* @param y Unrotated first control point Y coordinate (P0.getY())
* @param offsets Array of [x, y] relative offsets for all control points
* @param startIndex Index of the first pin of the column
* @param count Number of pins in the column
* @param right True to print the labels to the right of the column, false to print them left
* @param silkColor Silkscreen text color
*/
protected void drawFlatColumnPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
int startIndex, int count, boolean right, Color silkColor) {
if (offsets == null || offsets.length == 0) {
return;
}
double labelOffset = PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels();
g2d.setColor(silkColor);
g2d.setFont(PIN_ROW_FLAT_FONT);
for (int i = startIndex; i < startIndex + count && i < offsets.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double pinX = x + offsets[i][0] + (right ? labelOffset : -labelOffset);
double pinY = y + offsets[i][1];
StringUtils.drawCenteredText(g2d, label, pinX, getLabelBaseline(g2d, pinY),
right ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT, VerticalAlignment.TOP);
}
}
/**
* Helper to draw a complete PCB terminal block (green body + inner compartment lines + screw lugs)
* that matches the visuals of the PCBTerminalBlock component.
@@ -71,8 +71,10 @@ public class TFTDisplay extends AbstractMakerBoard {
// 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.
// disc diameter and its length runs from the tab's outer edge to the far side of the disc, so
// the tab's projection is the difference between them and is not carried separately. It is
// the one variant whose header is at the bottom, so its board grows upwards from the pin row
// rather than down from it.
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,
new String[] {"VCC", "GND", "CS", "RESET", "DC", "MOSI (SDI)", "SCK", "LED", "MISO (SDO)",
@@ -84,7 +86,7 @@ public class TFTDisplay extends AbstractMakerBoard {
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,
22.9d,
new String[] {"VCC", "GND", "SCL", "SDA", "DC", "CS", "RST"});
new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"});
private final String label;
private final double boardWidthMm;
@@ -211,8 +213,17 @@ public class TFTDisplay extends AbstractMakerBoard {
return x - (px(controller.getBoardWidthMm()) - span) / 2.0;
}
/**
* Top edge of the board. The rectangular boards hang below their header, which sits one offset
* down from the top edge; the round one wears its tab at the bottom, so the board stands above
* the row and the offset is measured up from the bottom edge instead.
*/
private double getBoardY(double y) {
return y - px(getController().getHeaderOffsetMm());
Controller controller = getController();
if (controller.isRound()) {
return y - px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm());
}
return y - px(controller.getHeaderOffsetMm());
}
private double[][] getRelativeOffsets() {
@@ -240,16 +251,16 @@ public class TFTDisplay extends AbstractMakerBoard {
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.
// A disc with a rectangular tab hanging below it. The tab has to be carried up to the disc's
// centre line before the two are unioned: taken only as far as the bottom of the disc it
// would meet it at a single tangent point and leave a notch either side of it. Above that
// line the disc is wider than the tab and governs the outline, so only the projection shows.
double discR = boardW / 2.0;
double discCy = boardY + px(controller.getTabProjectionMm()) + discR;
double discCy = boardY + 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)));
Area body = new Area(new Ellipse2D.Double(boardX, boardY, boardW, boardW));
body.add(new Area(new Rectangle2D.Double(boardX + (boardW - tabW) / 2.0, discCy, tabW,
boardY + boardH - discCy)));
return body;
}
@@ -318,11 +329,11 @@ public class TFTDisplay extends AbstractMakerBoard {
Shape screen;
if (controller.isRound()) {
// the lit circle is centred on the disc, which sits below the tab rather than on the
// the lit circle is centred on the disc, which sits above 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;
double discCy = boardY + discR;
screen = new Ellipse2D.Double(discCx - screenW / 2.0, discCy - screenH / 2.0, screenW,
screenH);
} else {
@@ -349,9 +360,10 @@ 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.
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, controlPoints.length, true,
SILK_COLOR);
// end needs 4.7 mm. The round board carries its row at the bottom, so for it alone the
// strip, and the names in it, are above the pins rather than below them.
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, controlPoints.length,
!controller.isRound(), SILK_COLOR);
}
g2d.setTransform(oldTx);
@@ -281,20 +281,26 @@ public class TFTDisplayTest {
Assert.assertFalse(Controller.GC9A01_1_28.hasMountingHoles());
}
/**
* The round board reads RST first, which is to say leftmost. The array had been in the opposite
* order, which looked plausible while the tab was drawn at the top and was wrong against the
* part either way; turning the board over is what made it visible.
*/
@Test
public void roundPinNamesComeFromTheController() {
TFTDisplay display = of(Controller.GC9A01_1_28);
String[] expected = new String[] {"VCC", "GND", "SCL", "SDA", "DC", "CS", "RST"};
String[] expected = new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"};
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.
* The outline is a disc with a narrower tab hanging below it, so the corners of its bounding box
* are off the board while the middles of the top and bottom edges are on it. Taking the tab only
* as far as the bottom of the disc would have left a notch either side of the tangent point,
* which these corners would not catch but the centre-line check would: there the disc is wider
* than the tab.
*/
@Test
public void roundBodyIsADiscWithATab() {
@@ -303,23 +309,45 @@ public class TFTDisplayTest {
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);
Assert.assertEquals("disc top to tab tip", 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",
Assert.assertTrue("the top of the disc is off the board",
body.contains(x + px(19.0d), y + px(1.0d)));
Assert.assertTrue("the middle of the tab is off the board",
body.contains(x + px(19.0d), y + px(44.5d)));
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)));
Assert.assertFalse("the bottom left corner is on the board",
body.contains(x + px(1.0d), y + px(44.5d)));
// 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
// took the disc's outline and not the rectangle's; a third of the way further down only the
// tab is left, and the same point is off the board
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)));
body.contains(x + px(2.0d), y + px(19.0d)));
Assert.assertFalse("the board is as wide as its disc below the centre line",
body.contains(x + px(2.0d), y + px(30.0d)));
}
/**
* Which edge the header sits on. Three of the boards hang below their pin row; the round one
* wears its tab at the bottom, so it stands above its row instead -- the one case where the
* offset is measured up from the bottom edge.
*/
@Test
public void headerSitsOnTheEdgeItsBoardHangsFrom() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Rectangle2D bounds = display.getBodyShape().getBounds2D();
Point2D firstPin = display.getControlPoint(0);
double fromEdge = controller.isRound() ? bounds.getMaxY() - firstPin.getY()
: firstPin.getY() - bounds.getY();
Assert.assertEquals(controller + " header offset", controller.getHeaderOffsetMm(),
mm(fromEdge), 0.01d);
}
}
}
+24 -7
View File
@@ -432,11 +432,24 @@ the netlist still spell them out.
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.
tab has to be carried to the disc's centre line before the union: taken only as far as the rim, 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.
One thing the flip turned up. The enum's pin array ran `VCC GND SCL SDA DC CS RST`, the reverse
of the `RST CS DC SDA SCL GND VCC` recorded two paragraphs above, so the row was mirrored against
the part. With the tab drawn at the top either order looked plausible -- the row has no landmark
to be wrong against -- and turning the board over is what made it visible. The array now reads
RST first, which is to say leftmost, and `roundPinNamesComeFromTheController` pins it there.
**The tab hangs below the disc, not above it.** That makes the round board the second in the
package, after the Nokia, whose header is on the bottom edge and whose outline therefore grows
upwards from control point zero -- so its header offset is measured up from the bottom edge where
every other variant's is measured down from the top, and its pin names go above the row rather
than below it. `headerSitsOnTheEdgeItsBoardHangsFrom` asserts the distinction for all four
variants, because nothing else in the geometry would notice if the round one were flipped back.
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")`,
@@ -790,8 +803,12 @@ part name, then the variant list).
6. **§8** regression samples and the `update.xml` block, last.
Eleven palette entries so far: the seven existing, plus the strip, the bar graph, the Jewel and the
Nokia, plus the panel if appetite holds. The `update.xml` 6.7.0 block still lists only the original
seven -- the strip, the bar graph, the Jewel and the Nokia have no entry yet.
Nokia, plus the panel if appetite holds. The `update.xml` 6.7.0 block now carries all eleven,
alphabetically as the 6.5.0 block lists its boards. Two of the entries it already had were stale
rather than missing, and both were stale because a class had outgrown them: the matrix was still
described as an 8x8 and the TFT as a touch screen with an SD slot, which is one variant of four
and a part that was removed from the drawing. A twelfth entry is needed only if the panel is
built.
## 10. Later slices, and why they are later