diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index 5f8d0c4d..8480434e 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -496,5 +496,49 @@ https://github.com/bancika/diy-layout-creator/releases + + + 6 + 7 + 0 + + 2026-09-30 00:00:00.000 CET + + + + NEW_FEATURE + Character LCD - 16x2 and 20x4, with I2C backpack or 16-pin parallel interface + + + NEW_FEATURE + 0.96" OLED Display (SSD1306) - I2C and SPI + + + NEW_FEATURE + 7-Segment Display - 1-digit 0.56 inch, 4-digit in 0.36 inch and 0.56 inch, and TM1637 driver module + + + NEW_FEATURE + 8x8 LED Matrix (MAX7219) + + + NEW_FEATURE + TFT Touch Screen (ILI9341) - 2.8 inch 240x320 with touch and SD card slot + + + NEW_FEATURE + NeoPixel Ring (WS2812B) - 12, 16 and 24 LEDs + + + NEW_FEATURE + NeoPixel Stick (WS2812B) - 8 LEDs + + + IMPROVEMENT + 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 + + + https://github.com/bancika/diy-layout-creator/releases + diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java index 5adcb953..aa7fd89d 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java @@ -90,6 +90,11 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponentProvenance differs between them and is worth keeping straight. The digit heights are + * definitional, since 0.36 and 0.56 inches are what the parts are named for. The 0.56 inch body, + * digit width and pitch are the figures this class has always carried and have never been checked + * against a datasheet. The 0.36 inch equivalents are **placeholders**, scaled from the digit + * height, and are not measurements at all. See the plan's section 11.10; none of this is settled + * until someone reads a datasheet or measures a part. + */ public enum DisplayType { - SingleDigit_10Pin("1-Digit 0.56\" (10-Pin DIP)"), - FourDigit_12Pin("4-Digit Bare (12-Pin DIP)"), - TM1637_Module_4Pin("4-Digit TM1637 Module (4-Pin)"); + SingleDigit_10Pin("1-Digit 0.56\" (10-Pin DIP)", 12.6d, 19.0d, 8.1d, 14.2d, 0d, 15.24d, true), + FourDigit_0_36_12Pin("4-Digit 0.36\" Bare (12-Pin DIP)", 30.0d, 14.0d, 5.2d, 9.14d, 7.5d, + 10.16d, true), + FourDigit_0_56_12Pin("4-Digit 0.56\" Bare (12-Pin DIP)", 50.3d, 19.0d, 8.1d, 14.2d, 12.7d, + 15.24d, true), + TM1637_Module_4Pin("4-Digit TM1637 Module (4-Pin)", 42.0d, 24.0d, 5.5d, 9.2d, 7.62d, 0d, false); private final String label; - DisplayType(String label) { this.label = label; } + private final double bodyWidthMm; + private final double bodyLengthMm; + private final double digitWidthMm; + private final double digitHeightMm; + private final double digitPitchMm; + private final double rowSpacingMm; + private final boolean dualRow; + + DisplayType(String label, double bodyWidthMm, double bodyLengthMm, double digitWidthMm, + double digitHeightMm, double digitPitchMm, double rowSpacingMm, boolean dualRow) { + this.label = label; + this.bodyWidthMm = bodyWidthMm; + this.bodyLengthMm = bodyLengthMm; + this.digitWidthMm = digitWidthMm; + this.digitHeightMm = digitHeightMm; + this.digitPitchMm = digitPitchMm; + this.rowSpacingMm = rowSpacingMm; + this.dualRow = dualRow; + } + + /** + * Distance between the two pin rows. It cannot be one shared constant: a package only works if + * its rows sit within its body, and the 0.36 inch part is shorter than the 0.6 inch spacing the + * larger packages use. + */ + public double getRowSpacingMm() { return rowSpacingMm; } + @Override public String toString() { return label; } + public double getBodyWidthMm() { return bodyWidthMm; } + public double getBodyLengthMm() { return bodyLengthMm; } + public double getDigitWidthMm() { return digitWidthMm; } + public double getDigitHeightMm() { return digitHeightMm; } + public double getDigitPitchMm() { return digitPitchMm; } + + /** True for the DIP packages, which carry their pins in two rows rather than one. */ + public boolean isDualRow() { return dualRow; } } public static Color BODY_BLACK = Color.decode("#1C1C1C"); @@ -77,13 +124,19 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { public static Color LED_OFF = Color.decode("#2E2E2E"); public static Color LED_OFF_BORDER = Color.decode("#222222"); - // Exact physical proportions from datasheet - public static Size DIGIT_HEIGHT = new Size(14.2d, SizeUnit.mm); // 0.56" - public static Size DIGIT_WIDTH = new Size(8.1d, SizeUnit.mm); - public static Size DIGIT_PITCH = new Size(12.7d, SizeUnit.mm); + // Per-package dimensions live on DisplayType; these apply to every package. public static Size SEGMENT_THICKNESS = new Size(1.4d, SizeUnit.mm); - public static Size SEGMENT_GAP = new Size(0.35d, SizeUnit.mm); - public static double SLANT_DEGREES = 8.0d; + // How far a module's header column sits in from the right edge. The DIP packages straddle their + // pins with a row either side; a module carries its header down one edge instead. + public static Size HEADER_EDGE_OFFSET = new Size(2.5d, SizeUnit.mm); + /** + * The digit is drawn at this fraction of the dimensions above. On the real part the digit window + * runs almost to the pin rows -- at full size the two clear each other by around a hundredth of a + * millimetre -- so a faithful drawing has the pins sitting on the digit. This is a deliberate + * departure from the measurements for legibility, which is why it is a separate factor rather + * than smaller digit sizes on {@link DisplayType}: those stay as the package measures. + */ + public static double DIGIT_DRAW_SCALE = 0.88d; public static final String[] PIN_NAMES_1DIGIT = new String[] { "e", "d", "COM1", "c", "DP", "b", "a", "COM2", "f", "g" @@ -115,6 +168,12 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + DisplayType displayType = getDisplayType(); + return displayType == null ? null : displayType.toString(); + } + @EditableProperty(name = "LED Color") public Color getLedColor() { return ledColor; @@ -125,18 +184,27 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { invalidateCache(); } + /** + * True for the two bare four-digit packages. They differ in size but not in pin function, so they + * share a pin array and differ only in what they measure. + */ + private boolean isFourDigitBare() { + return displayType == DisplayType.FourDigit_0_36_12Pin + || displayType == DisplayType.FourDigit_0_56_12Pin; + } + + private String[] getPinNames() { + if (displayType == DisplayType.SingleDigit_10Pin) { + return PIN_NAMES_1DIGIT; + } + return isFourDigitBare() ? PIN_NAMES_4DIGIT : PIN_NAMES_TM1637; + } + @Override public String getControlPointNodeName(int index) { - switch (displayType) { - case SingleDigit_10Pin: - if (index >= 0 && index < PIN_NAMES_1DIGIT.length) return PIN_NAMES_1DIGIT[index]; - break; - case FourDigit_12Pin: - if (index >= 0 && index < PIN_NAMES_4DIGIT.length) return PIN_NAMES_4DIGIT[index]; - break; - case TM1637_Module_4Pin: - if (index >= 0 && index < PIN_NAMES_TM1637.length) return PIN_NAMES_TM1637[index]; - break; + String[] names = getPinNames(); + if (index >= 0 && index < names.length) { + return names[index]; } return "Pin " + (index + 1); } @@ -146,68 +214,49 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { Point2D firstPoint = controlPoints[0]; double spacing = PIN_SPACING.convertToPixels(); - if (displayType == DisplayType.SingleDigit_10Pin) { - double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); // 120px - double[][] relativeOffsets = new double[10][2]; - for (int i = 0; i < 5; i++) { + int pinCount = getPinNames().length; + double[][] relativeOffsets = new double[pinCount][2]; + + if (displayType.isDualRow()) { + // DIP numbering: pin 1 at the bottom left, along the bottom row, then back along the top + int perRow = pinCount / 2; + double rowSpacing = + new Size(displayType.getRowSpacingMm(), SizeUnit.mm).convertToPixels(); + for (int i = 0; i < perRow; i++) { relativeOffsets[i][0] = i * spacing; relativeOffsets[i][1] = 0; + relativeOffsets[perRow + i][0] = (perRow - 1 - i) * spacing; + relativeOffsets[perRow + i][1] = -rowSpacing; } - for (int i = 0; i < 5; i++) { - relativeOffsets[5 + i][0] = (4 - i) * spacing; - relativeOffsets[5 + i][1] = -rowSpacing; - } - rotatePoints(firstPoint, relativeOffsets); - } else if (displayType == DisplayType.FourDigit_12Pin) { - double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); - double[][] relativeOffsets = new double[12][2]; - for (int i = 0; i < 6; i++) { - relativeOffsets[i][0] = i * spacing; - relativeOffsets[i][1] = 0; - } - for (int i = 0; i < 6; i++) { - relativeOffsets[6 + i][0] = (5 - i) * spacing; - relativeOffsets[6 + i][1] = -rowSpacing; - } - rotatePoints(firstPoint, relativeOffsets); } else { - // TM1637 Module 4-pin - double[][] relativeOffsets = new double[4][2]; - for (int i = 0; i < 4; i++) { - relativeOffsets[i][0] = i * spacing; - relativeOffsets[i][1] = 0; + // a module carries its header as a single column down one edge, not a row across the face + for (int i = 0; i < pinCount; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; } - rotatePoints(firstPoint, relativeOffsets); } + + rotatePoints(firstPoint, relativeOffsets); } @Override public Shape getBodyShape() { Point2D p0 = controlPoints[0]; - double x = p0.getX(); - double y = p0.getY(); - double boardW; - double boardH; + double spacing = PIN_SPACING.convertToPixels(); + double boardW = new Size(displayType.getBodyWidthMm(), SizeUnit.mm).convertToPixels(); + double boardH = new Size(displayType.getBodyLengthMm(), SizeUnit.mm).convertToPixels(); + int pinCount = getPinNames().length; double boardX; double boardY; - if (displayType == DisplayType.SingleDigit_10Pin) { - boardW = new Size(12.6d, SizeUnit.mm).convertToPixels(); - boardH = new Size(19.0d, SizeUnit.mm).convertToPixels(); - double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); - boardX = x - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0; - boardY = y - rowSpacing - (boardH - rowSpacing) / 2.0; - } else if (displayType == DisplayType.FourDigit_12Pin) { - boardW = new Size(50.3d, SizeUnit.mm).convertToPixels(); - boardH = new Size(19.0d, SizeUnit.mm).convertToPixels(); - double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); - boardX = x - (boardW - 5 * PIN_SPACING.convertToPixels()) / 2.0; - boardY = y - rowSpacing - (boardH - rowSpacing) / 2.0; + if (displayType.isDualRow()) { + double rowSpacing = + new Size(displayType.getRowSpacingMm(), SizeUnit.mm).convertToPixels(); + boardX = p0.getX() - (boardW - (pinCount / 2 - 1) * spacing) / 2.0; + boardY = p0.getY() - rowSpacing - (boardH - rowSpacing) / 2.0; } else { - boardW = new Size(42.0d, SizeUnit.mm).convertToPixels(); - boardH = new Size(24.0d, SizeUnit.mm).convertToPixels(); - boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0; - boardY = y - (boardH - PIN_SPACING.convertToPixels()) / 2.0; + boardX = p0.getX() - boardW + HEADER_EDGE_OFFSET.convertToPixels(); + boardY = p0.getY() - (boardH - (pinCount - 1) * spacing) / 2.0; } return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); @@ -256,14 +305,16 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); - // Single 0.56" 7-segment digit with exact datasheet dimensions - double dw = DIGIT_WIDTH.convertToPixels(); - double dh = DIGIT_HEIGHT.convertToPixels(); + // one digit, drawn slightly under size so it keeps clear of the pins + double dw = new Size(displayType.getDigitWidthMm(), SizeUnit.mm).convertToPixels() + * DIGIT_DRAW_SCALE; + double dh = new Size(displayType.getDigitHeightMm(), SizeUnit.mm).convertToPixels() + * DIGIT_DRAW_SCALE; double dx = boardX + (boardW - dw) / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels(); double dy = boardY + (boardH - dh) / 2.0; drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor); - } else if (displayType == DisplayType.FourDigit_12Pin) { + } else if (isFourDigitBare()) { // Dark display face g2d.setColor(FACE_BLACK); g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); @@ -271,11 +322,16 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); - // 4 digits across with 12.70mm center-to-center pitch - double dw = DIGIT_WIDTH.convertToPixels(); - double dh = DIGIT_HEIGHT.convertToPixels(); - double pitch = DIGIT_PITCH.convertToPixels(); - double firstCenterX = boardX + new Size(6.1d, SizeUnit.mm).convertToPixels(); + // Four digits on the package's real centre-to-centre pitch, each drawn slightly under size + // so they keep clear of the pins. The pitch is unscaled, so the digits stay on their true + // centres and only the gaps between them widen, and the row is centred on the body rather + // than measured from its left edge, so it stays put across packages of different widths. + double dw = new Size(displayType.getDigitWidthMm(), SizeUnit.mm).convertToPixels() + * DIGIT_DRAW_SCALE; + double dh = new Size(displayType.getDigitHeightMm(), SizeUnit.mm).convertToPixels() + * DIGIT_DRAW_SCALE; + double pitch = new Size(displayType.getDigitPitchMm(), SizeUnit.mm).convertToPixels(); + double firstCenterX = boardX + boardW / 2.0 - 1.5 * pitch; double dy = boardY + (boardH - dh) / 2.0; for (int d = 0; d < 4; d++) { @@ -284,16 +340,15 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor); } - // Center colon dots between digits 1 & 2 + // Colon between the middle pair, sized off the glyph grid like the decimal point is, so it + // tracks the digit instead of staying a fixed few pixels across. double colonX = boardX + boardW / 2.0; - double colonY1 = dy + dh * 0.35; - double colonY2 = dy + dh * 0.65; + double dotD = dw / GLYPH_WIDTH * 2.0; g2d.setColor(ledColor); - g2d.fill(new Ellipse2D.Double(colonX - 2.0, colonY1 - 2.0, 4.0, 4.0)); - g2d.fill(new Ellipse2D.Double(colonX - 2.0, colonY2 - 2.0, 4.0, 4.0)); + g2d.fill(new Ellipse2D.Double(colonX - dotD / 2.0, dy + dh * 0.35 - dotD / 2.0, dotD, dotD)); + g2d.fill(new Ellipse2D.Double(colonX - dotD / 2.0, dy + dh * 0.65 - dotD / 2.0, dotD, dotD)); - } else { - // TM1637 4-Digit Display Module + } else if (displayType == DisplayType.TM1637_Module_4Pin) { MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + 14, 12); MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + boardH - 14, 12); MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 14, boardY + 14, 12); @@ -302,7 +357,7 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { // Display Bezel (Center) double bezelW = new Size(30.0d, SizeUnit.mm).convertToPixels(); double bezelH = new Size(14.0d, SizeUnit.mm).convertToPixels(); - double bezelX = boardX + (boardW - bezelW) / 2.0 - 6.0; + double bezelX = boardX + (boardW - bezelW) / 2.0; double bezelY = boardY + (boardH - bezelH) / 2.0; g2d.setColor(FACE_BLACK); @@ -324,13 +379,12 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor); } - // Center colon dots + // same colon treatment as the bare packages: sized and placed off the digit, not in pixels double colonX = bezelX + bezelW / 2.0; - double colonY1 = bezelY + bezelH / 2.0 - 8.0; - double colonY2 = bezelY + bezelH / 2.0 + 6.0; + double dotD = dw / GLYPH_WIDTH * 2.0; g2d.setColor(ledColor); - g2d.fill(new Ellipse2D.Double(colonX - 1.5, colonY1 - 1.5, 3.0, 3.0)); - g2d.fill(new Ellipse2D.Double(colonX - 1.5, colonY2 - 1.5, 3.0, 3.0)); + g2d.fill(new Ellipse2D.Double(colonX - dotD / 2.0, dy + dh * 0.35 - dotD / 2.0, dotD, dotD)); + g2d.fill(new Ellipse2D.Double(colonX - dotD / 2.0, dy + dh * 0.65 - dotD / 2.0, dotD, dotD)); } } @@ -375,102 +429,58 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { } /** - * Draws an authentic 7-segment digit matching exact engineering drawing specifications: - * - 8.0° italic forward slant - * - Realistic polygon segment geometry (trapezoids/hexagons with clean junction bevels) - * - 1.40mm segment thickness, 0.35mm inter-segment gaps without overlap - * - Unlit segments rendered in dim silhouette and active segments lit in onColor - * - 1.40mm Decimal point circle at bottom-right + * The seven segments as a unit-grid glyph: a 10 x 18 box with segments two units thick, mitred + * at 45 degrees. Neighbours meet at a shared vertex rather than being held apart by a gap, and + * the visible separation between them comes from outlining each segment in the background + * colour. Index order is A through G, matching the SEG_* bits. + */ + private static final double[][][] SEGMENT_POINTS = new double[][][] { + {{1, 1}, {2, 0}, {8, 0}, {9, 1}, {8, 2}, {2, 2}}, // A, top + {{9, 1}, {10, 2}, {10, 8}, {9, 9}, {8, 8}, {8, 2}}, // B, upper right + {{9, 9}, {10, 10}, {10, 16}, {9, 17}, {8, 16}, {8, 10}}, // C, lower right + {{9, 17}, {8, 18}, {2, 18}, {1, 17}, {2, 16}, {8, 16}}, // D, bottom + {{1, 17}, {0, 16}, {0, 10}, {1, 9}, {2, 10}, {2, 16}}, // E, lower left + {{1, 9}, {0, 8}, {0, 2}, {1, 1}, {2, 2}, {2, 8}}, // F, upper left + {{1, 9}, {2, 8}, {8, 8}, {9, 9}, {8, 10}, {2, 10}} // G, middle + }; + + /** Width of the glyph grid above, in its own units. */ + private static final double GLYPH_WIDTH = 10.0d; + + /** Height of the glyph grid above, in its own units. */ + private static final double GLYPH_HEIGHT = 18.0d; + + /** + * Draws one upright digit into the box at {@code x, y, w, h}, with the segment shapes taken from + * {@link #SEGMENT_POINTS} scaled to that box. Unlit segments are drawn first as a dim + * silhouette, then the lit ones, and finally every segment is outlined in the face colour so + * that neighbours which share a vertex still read as separate segments. */ private void drawSevenSegmentDigit(Graphics2D g2d, double x, double y, double w, double h, String charToDisplay, Color onColor) { AffineTransform orig = g2d.getTransform(); - // Exact 8.0° forward italic slant from engineering drawing - g2d.translate(x + w / 2.0, y + h / 2.0); - g2d.shear(-Math.tan(Math.toRadians(SLANT_DEGREES)), 0); - g2d.translate(-w / 2.0, -h / 2.0); + g2d.translate(x, y); - double t = Math.max(3.0, SEGMENT_THICKNESS.convertToPixels() * (h / DIGIT_HEIGHT.convertToPixels())); - double g = Math.max(1.0, SEGMENT_GAP.convertToPixels() * (h / DIGIT_HEIGHT.convertToPixels())); - double segW = w - t * 0.8; + double sx = w / GLYPH_WIDTH; + double sy = h / GLYPH_HEIGHT; - double x0 = 0; - double x1 = segW; - double y0 = 0; - double ym = h / 2.0; - double y1 = h; + Shape[] segments = new Shape[SEGMENT_POINTS.length]; + for (int i = 0; i < SEGMENT_POINTS.length; i++) { + double[][] points = SEGMENT_POINTS[i]; + Path2D.Double path = new Path2D.Double(); + path.moveTo(points[0][0] * sx, points[0][1] * sy); + for (int p = 1; p < points.length; p++) { + path.lineTo(points[p][0] * sx, points[p][1] * sy); + } + path.closePath(); + segments[i] = path; + } - // Segment A (Top horizontal) - Path2D pathA = new Path2D.Double(); - pathA.moveTo(x0 + t * 0.6 + g, y0); - pathA.lineTo(x1 - t * 0.6 - g, y0); - pathA.lineTo(x1 - g, y0 + t * 0.45); - pathA.lineTo(x1 - t - g, y0 + t); - pathA.lineTo(x0 + t + g, y0 + t); - pathA.lineTo(x0 + g, y0 + t * 0.45); - pathA.closePath(); + // the decimal point is not part of the glyph grid, so it is placed in the same units: one + // segment thickness across, sitting on the baseline just clear of the digit + double dpD = 2.0 * sx; + Shape dpShape = new Ellipse2D.Double(GLYPH_WIDTH * sx + 0.5 * sx, 18.0 * sy - dpD, dpD, dpD); - // Segment B (Top-Right vertical) - Path2D pathB = new Path2D.Double(); - pathB.moveTo(x1, y0 + t * 0.5 + g); - pathB.lineTo(x1 - t * 0.35, y0 + g); - pathB.lineTo(x1 - t, y0 + t + 1.4 * g); - pathB.lineTo(x1 - t, ym - t * 0.5 - 0.8 * g); - pathB.lineTo(x1, ym - 0.8 * g); - pathB.closePath(); - - // Segment C (Bottom-Right vertical) - Path2D pathC = new Path2D.Double(); - pathC.moveTo(x1, ym + 0.8 * g); - pathC.lineTo(x1 - t, ym + t * 0.5 + 0.8 * g); - pathC.lineTo(x1 - t, y1 - t - 1.4 * g); - pathC.lineTo(x1 - t * 0.35, y1 - g); - pathC.lineTo(x1, y1 - t * 0.5 - g); - pathC.closePath(); - - // Segment D (Bottom horizontal) - Path2D pathD = new Path2D.Double(); - pathD.moveTo(x0 + t + g, y1 - t); - pathD.lineTo(x1 - t - g, y1 - t); - pathD.lineTo(x1 - g, y1 - t * 0.45); - pathD.lineTo(x1 - t * 0.6 - g, y1); - pathD.lineTo(x0 + t * 0.6 + g, y1); - pathD.lineTo(x0 + g, y1 - t * 0.45); - pathD.closePath(); - - // Segment E (Bottom-Left vertical) - Path2D pathE = new Path2D.Double(); - pathE.moveTo(x0, ym + 0.8 * g); - pathE.lineTo(x0 + t, ym + t * 0.5 + 0.8 * g); - pathE.lineTo(x0 + t, y1 - t - 1.4 * g); - pathE.lineTo(x0 + t * 0.35, y1 - g); - pathE.lineTo(x0, y1 - t * 0.5 - g); - pathE.closePath(); - - // Segment F (Top-Left vertical) - Path2D pathF = new Path2D.Double(); - pathF.moveTo(x0, y0 + t * 0.5 + g); - pathF.lineTo(x0 + t * 0.35, y0 + g); - pathF.lineTo(x0 + t, y0 + t + 1.4 * g); - pathF.lineTo(x0 + t, ym - t * 0.5 - 0.8 * g); - pathF.lineTo(x0, ym - 0.8 * g); - pathF.closePath(); - - // Segment G (Middle horizontal hexagon) - Path2D pathG = new Path2D.Double(); - pathG.moveTo(x0 + 1.4 * g, ym); - pathG.lineTo(x0 + t * 0.6 + 1.4 * g, ym - t * 0.48); - pathG.lineTo(x1 - t * 0.6 - 1.4 * g, ym - t * 0.48); - pathG.lineTo(x1 - 1.4 * g, ym); - pathG.lineTo(x1 - t * 0.6 - 1.4 * g, ym + t * 0.48); - pathG.lineTo(x0 + t * 0.6 + 1.4 * g, ym + t * 0.48); - pathG.closePath(); - - // Decimal Point (DP) - double dpD = t * 0.95; - Shape dpShape = new Ellipse2D.Double(x1 + t * 0.25, y1 - dpD, dpD, dpD); - - Shape[] segments = new Shape[] { pathA, pathB, pathC, pathD, pathE, pathF, pathG }; int mask = getSegmentMask(charToDisplay.isEmpty() ? '8' : charToDisplay.charAt(0)); // 1. Draw unlit segment silhouettes (dim background) @@ -495,6 +505,15 @@ public class SevenSegmentDisplay extends AbstractMakerBoard { g2d.fill(dpShape); } + // The source drawing outlines every segment in its own background colour, which is what stops + // neighbours that share a vertex from fusing into one shape. The background here is the + // display face, so the outline takes that colour rather than the white the drawing uses. + g2d.setColor(FACE_BLACK); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke((float) (0.25 * (sx + sy) / 2.0))); + for (Shape segment : segments) { + g2d.draw(segment); + } + g2d.setTransform(orig); } diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java index 43e32036..01c12d50 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java @@ -1,23 +1,23 @@ /* - * + * * 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 . - * + * */ package org.diylc.components.displays; @@ -49,20 +49,38 @@ 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", -// instanceNamePrefix = "DISP", zOrder = IDIYComponent.BOARD, -// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, -// enableCache = true) +@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", + instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) public class TFTDisplay extends AbstractMakerBoard { private static final long serialVersionUID = 1L; public static Color TFT_RED = Color.decode("#C0392B"); public static Color SCREEN_BG = Color.decode("#111111"); + public static Color GLASS_COLOR = Color.decode("#1A1A1A"); - public static Size BOARD_WIDTH = new Size(86.0d, SizeUnit.mm); - public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm); + // 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)", @@ -83,13 +101,23 @@ public class TFTDisplay extends AbstractMakerBoard { return "Pin " + (index + 1); } + /** Left edge of the board, derived so that the pin row sits centred on the top edge. */ + private double getBoardX(double x) { + double spacing = PIN_SPACING.convertToPixels(); + return x - (BOARD_WIDTH.convertToPixels() - (PIN_NAMES.length - 1) * spacing) / 2.0; + } + + private double getBoardY(double y) { + return y - HEADER_OFFSET.convertToPixels(); + } + @Override protected void updateControlPoints() { Point2D firstPoint = controlPoints[0]; double spacing = PIN_SPACING.convertToPixels(); - double[][] relativeOffsets = new double[14][2]; - for (int i = 0; i < 14; i++) { + double[][] relativeOffsets = new double[PIN_NAMES.length][2]; + for (int i = 0; i < PIN_NAMES.length; i++) { relativeOffsets[i][0] = i * spacing; relativeOffsets[i][1] = 0; } @@ -100,13 +128,9 @@ public class TFTDisplay extends AbstractMakerBoard { @Override public Shape getBodyShape() { Point2D p0 = controlPoints[0]; - double x = p0.getX(); - double y = p0.getY(); - double boardW = BOARD_WIDTH.convertToPixels(); - double boardH = BOARD_HEIGHT.convertToPixels(); - double boardX = x - 70; - double boardY = y - 20; - return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10); + double radius = CORNER_RADIUS.convertToPixels(); + return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), + BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), radius, radius); } @Override @@ -127,8 +151,8 @@ public class TFTDisplay extends AbstractMakerBoard { double boardW = BOARD_WIDTH.convertToPixels(); double boardH = BOARD_HEIGHT.convertToPixels(); - double boardX = x - 70; - double boardY = y - 20; + double boardX = getBoardX(x); + double boardY = getBoardY(y); Shape boardShape = getBodyShape(); @@ -144,33 +168,34 @@ public class TFTDisplay extends AbstractMakerBoard { g2d.draw(boardShape); if (!outlineMode) { - // 4 Mounting holes in corners - MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + 20, 20); - MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + boardH - 20, 20); - MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + 20, 20); - MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + boardH - 20, 20); + 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); - // Color Screen Area - double screenMarginX = 35; - double screenW = boardW - 2 * screenMarginX - 25; - double screenH = boardH - 65; - double screenX = boardX + screenMarginX; - double screenY = boardY + 45; + // 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)); + + double screenW = SCREEN_WIDTH.convertToPixels(); + double screenH = SCREEN_LENGTH.convertToPixels(); + double screenX = boardX + (boardW - screenW) / 2.0; + double screenY = glassY + (glassH - screenH) / 2.0; g2d.setColor(SCREEN_BG); g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH)); g2d.setColor(Color.DARK_GRAY); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); g2d.draw(new Rectangle2D.Double(screenX, screenY, screenW, screenH)); - - // Display Demo UI - g2d.setColor(Color.decode("#2980B9")); - g2d.fillRect((int) screenX, (int) screenY, (int) screenW, 25); - g2d.setColor(Color.WHITE); - g2d.setFont(SILK_FONT); - StringUtils.drawCenteredText(g2d, "TFT 2.8\" 320x240 SPI", screenX + screenW / 2.0, screenY + 12, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); - - // SD Card holder on right edge - MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - 28, boardY + 100, 36, 120, "SD"); } g2d.setTransform(oldTx); @@ -183,18 +208,16 @@ public class TFTDisplay extends AbstractMakerBoard { @Override public void drawIcon(Graphics2D g2d, int width, int height) { g2d.setColor(TFT_RED); - g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(8, 1, width - 16, height - 2, 3, 3)); g2d.setColor(TFT_RED.darker()); - g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.draw(new RoundRectangle2D.Double(8, 1, width - 16, height - 2, 3, 3)); - // Screen g2d.setColor(SCREEN_BG); - g2d.fillRect(6, 9, width - 12, height - 16); - g2d.setColor(Color.decode("#2980B9")); - g2d.fillRect(6, 9, width - 12, 5); + g2d.fillRect(10, 8, width - 20, height - 12); g2d.setColor(Color.WHITE); g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); - StringUtils.drawCenteredText(g2d, "TFT", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "TFT", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, + VerticalAlignment.CENTER); } } diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java index c360abc2..801088a9 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java @@ -23,15 +23,12 @@ package org.diylc.components.displays; import java.awt.Color; import java.awt.Composite; -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; import org.diylc.awt.StringUtils; import org.diylc.common.HorizontalAlignment; @@ -39,6 +36,7 @@ import org.diylc.common.ObjectCache; import org.diylc.common.Orientation; import org.diylc.common.VerticalAlignment; import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; import org.diylc.core.ComponentState; import org.diylc.core.IDIYComponent; import org.diylc.core.IDrawingObserver; @@ -51,19 +49,22 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -// @ComponentDescriptor(name = "NeoPixel Ring (WS2812B)", category = "Displays & Outputs", -// author = "Branislav Stojkovic", description = "Addressable RGB WS2812B NeoPixel Ring (12/16/24 LEDs)", -// instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, -// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, -// enableCache = true) +@ComponentDescriptor(name = "NeoPixel Ring (WS2812B)", category = "Displays & Outputs", + author = "Branislav Stojkovic", description = "Addressable RGB WS2812B NeoPixel Ring (12/16/24 LEDs)", + instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) public class WS2812BRing extends AbstractMakerBoard { private static final long serialVersionUID = 1L; public enum RingSize { - _12_LED("12 LEDs (37mm OD)", 12, 37.0, 27.0), - _16_LED("16 LEDs (44.5mm OD)", 16, 44.5, 32.0), - _24_LED("24 LEDs (66mm OD)", 24, 66.0, 52.0); + // Outer and inner diameters are the figures Adafruit publishes for each ring. They are not + // decoration: getMidRadius() averages them, and that radius places the pads, the LEDs and the + // control points, so an inner diameter that is out by a millimetre moves every one of them. + _12_LED("12 LEDs (36.8mm OD)", 12, 36.8, 23.3), + _16_LED("16 LEDs (44.5mm OD)", 16, 44.5, 31.7), + _24_LED("24 LEDs (65.5mm OD)", 24, 65.5, 52.3); private final String label; private final int ledCount; @@ -83,15 +84,7 @@ public class WS2812BRing extends AbstractMakerBoard { public double getInnerDiameterMm() { return innerDiameterMm; } } - // Colors shared with WS2812BStick public static Color NEO_BLACK = Color.decode("#111111"); - public static Color LED_PACKAGE = Color.decode("#FDFEFE"); - public static Color LED_BORDER = Color.decode("#D0D3D4"); - public static Color LED_DIFFUSER = Color.decode("#EAECEE"); - public static Color DIFFUSER_BORDER = Color.decode("#BDC3C7"); - public static Color CHIP_DOT_COLOR = Color.decode("#333333"); - public static Color SOLDER_PAD_COLOR = Color.decode("#D4AC0D"); - public static Color SOLDER_PAD_BORDER = Color.decode("#9A7D0A"); private static final String[] PIN_NAMES = {"DIN", "+5V", "GND", "DOUT"}; @@ -114,6 +107,12 @@ public class WS2812BRing extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + RingSize ringSize = getRingSize(); + return ringSize == null ? null : ringSize.toString(); + } + @Override public String getControlPointNodeName(int index) { if (index >= 0 && index < PIN_NAMES.length) { @@ -210,32 +209,12 @@ public class WS2812BRing extends AbstractMakerBoard { if (!outlineMode) { int ledCount = ringSize.getLedCount(); double ledSize = Math.max(8.0, Math.min(15.0, (2 * Math.PI * midR / ledCount) * 0.60)); - double halfLed = ledSize / 2.0; // Draw all LEDs around the circular ring for (int i = 0; i < ledCount; i++) { double angle = 2 * Math.PI * i / ledCount - Math.PI / 2.0; - double lx = cx + midR * Math.cos(angle); - double ly = cy + midR * Math.sin(angle); - - // 5050 White Package - g2d.setColor(LED_PACKAGE); - g2d.fill(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2)); - g2d.setColor(LED_BORDER); - g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f)); - g2d.draw(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2)); - - // Milky phosphor lens - double diffR = halfLed * 0.7; - g2d.setColor(LED_DIFFUSER); - g2d.fill(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2)); - g2d.setColor(DIFFUSER_BORDER); - g2d.draw(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2)); - - // Tiny IC dot - g2d.setColor(CHIP_DOT_COLOR); - double dotS = Math.max(2.0, diffR * 0.4); - g2d.fill(new Rectangle2D.Double(lx - dotS / 2.0, ly - dotS / 2.0, dotS, dotS)); + MakerBoardPainter.drawAddressableLed(g2d, cx + midR * Math.cos(angle), + cy + midR * Math.sin(angle), ledSize); } // Center silkscreen text @@ -244,17 +223,8 @@ public class WS2812BRing extends AbstractMakerBoard { StringUtils.drawCenteredText(g2d, "NeoPixel", cx, cy - 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); StringUtils.drawCenteredText(g2d, ringSize.getLedCount() + "x LED", cx, cy + 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); - // Silkscreen labels next to the 4 solder pads on the ring - g2d.setColor(Color.WHITE); - g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); - String[] labels = new String[] {"DIN", "5V", "GND", "DOUT"}; - for (int i = 0; i < 4; i++) { - double theta = getPadAngle(i); - double labelR = innerR + 5; - double px = cx + labelR * Math.cos(theta); - double py = cy + labelR * Math.sin(theta); - StringUtils.drawCenteredText(g2d, labels[i], px, py, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); - } + // The four pads sit one pin spacing apart along the arc, which leaves each of them far less + // room than its name needs, so the names are left to the node tooltips and the netlist. } g2d.setTransform(oldTx); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java index 2c0d4f25..ea90294f 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java @@ -23,7 +23,6 @@ package org.diylc.components.displays; import java.awt.Color; import java.awt.Composite; -import java.awt.Font; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; @@ -32,12 +31,10 @@ import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; -import org.diylc.awt.StringUtils; -import org.diylc.common.HorizontalAlignment; import org.diylc.common.ObjectCache; import org.diylc.common.Orientation; -import org.diylc.common.VerticalAlignment; import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; import org.diylc.core.ComponentState; import org.diylc.core.IDIYComponent; import org.diylc.core.IDrawingObserver; @@ -49,30 +46,28 @@ import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; -// @ComponentDescriptor(name = "NeoPixel Stick (8x WS2812B)", category = "Displays & Outputs", -// author = "Branislav Stojkovic", description = "8-LED Addressable RGB WS2812B NeoPixel Stick", -// instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, -// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, -// enableCache = true) +@ComponentDescriptor(name = "NeoPixel Stick (8x WS2812B)", category = "Displays & Outputs", + author = "Branislav Stojkovic", description = "8-LED Addressable RGB WS2812B NeoPixel Stick", + instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) public class WS2812BStick extends AbstractMakerBoard { private static final long serialVersionUID = 1L; public static Color NEO_BLACK = Color.decode("#111111"); - public static Color LED_PACKAGE = Color.decode("#FDFEFE"); - public static Color LED_BORDER = Color.decode("#D0D3D4"); - public static Color LED_DIFFUSER = Color.decode("#EAECEE"); - public static Color DIFFUSER_BORDER = Color.decode("#BDC3C7"); - public static Color CHIP_DOT_COLOR = Color.decode("#333333"); - public static Font PIN_LABEL_FONT = new Font("SansSerif", Font.BOLD, 9); - - public static Size BOARD_WIDTH = new Size(53.0d, SizeUnit.mm); - public static Size BOARD_HEIGHT = new Size(10.2d, SizeUnit.mm); + public static Size BOARD_WIDTH = new Size(51.1d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(10.22d, SizeUnit.mm); + public static Size LED_SIZE = new Size(5.0d, SizeUnit.mm); + public static Size PAD_INSET = new Size(2.0d, SizeUnit.mm); + // Each end carries four pads in the order GND, data, power, GND. Both grounds on an end are the + // same net on the board, so they are numbered only to keep the node names distinct; the + // silkscreen prints "GND" for all four. private static final String[] PIN_NAMES = { - "DIN", "+5V (In)", "GND (In)", "GND (In)", - "DOUT", "+5V (Out)", "GND (Out)", "GND (Out)" + "GND_1", "DIN", "+5V_1", "GND_2", + "GND_3", "DOUT", "+5V_2", "GND_4" }; public WS2812BStick() { @@ -93,33 +88,36 @@ public class WS2812BStick extends AbstractMakerBoard { protected void updateControlPoints() { Point2D firstPoint = controlPoints[0]; double spacing = PIN_SPACING.convertToPixels(); - double w = BOARD_WIDTH.convertToPixels(); + // the two pad columns sit the same distance in from each end of the board + double columnSpacing = BOARD_WIDTH.convertToPixels() - 2 * PAD_INSET.convertToPixels(); - // 8 pins total: - // Left input header (4 pins): 0 = DIN, 1 = +5V, 2 = GND, 3 = GND - // Right output header (4 pins): 4 = DOUT, 5 = +5V, 6 = GND, 7 = GND - double[][] relativeOffsets = new double[][] { - { 0, 0 }, - { 0, spacing }, - { 0, spacing * 2 }, - { 0, spacing * 3 }, - { w - 20, 0 }, - { w - 20, spacing }, - { w - 20, spacing * 2 }, - { w - 20, spacing * 3 } - }; + double[][] relativeOffsets = new double[PIN_NAMES.length][2]; + for (int i = 0; i < 4; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + relativeOffsets[4 + i][0] = columnSpacing; + relativeOffsets[4 + i][1] = i * spacing; + } rotatePoints(firstPoint, relativeOffsets); } + /** Left edge of the board; the first pad column sits {@code PAD_INSET} in from it. */ + private double getBoardX(double x) { + return x - PAD_INSET.convertToPixels(); + } + + /** Top edge of the board, derived so that the four-pad column is centred across its width. */ + private double getBoardY(double y) { + double padSpan = 3 * PIN_SPACING.convertToPixels(); + return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0; + } + @Override public Shape getBodyShape() { Point2D p0 = controlPoints[0]; - double x = p0.getX(); - double y = p0.getY(); - double boardW = BOARD_WIDTH.convertToPixels(); - double boardH = BOARD_HEIGHT.convertToPixels(); - return new RoundRectangle2D.Double(x - 10, y - 10, boardW, boardH, 4, 4); + return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), + BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), 4, 4); } @Override @@ -140,8 +138,8 @@ public class WS2812BStick extends AbstractMakerBoard { double boardW = BOARD_WIDTH.convertToPixels(); double boardH = BOARD_HEIGHT.convertToPixels(); - double boardX = x - 10; - double boardY = y - 10; + double boardX = getBoardX(x); + double boardY = getBoardY(y); Shape boardShape = getBodyShape(); @@ -157,49 +155,14 @@ public class WS2812BStick extends AbstractMakerBoard { g2d.draw(boardShape); if (!outlineMode) { - // 8x 5050 SMD WS2812B RGB LEDs (centered between left and right pin areas) - double spacing = PIN_SPACING.convertToPixels(); - double ledAreaStartX = boardX + 68; - double ledAreaEndX = boardX + boardW - 68; - double ledPitch = (ledAreaEndX - ledAreaStartX) / 7.0; + // Eight 5mm packages in a row, centred along the board. The pad names are printed on the back + // of the real board, so this face carries no silkscreen. + double ledSize = LED_SIZE.convertToPixels(); + double fieldInset = (boardW - 8 * ledSize) / 2.0; for (int i = 0; i < 8; i++) { - double lx = ledAreaStartX + i * ledPitch; - double ly = boardY + boardH / 2.0; - - // 5050 White Package (28x28) - g2d.setColor(LED_PACKAGE); - g2d.fill(new RoundRectangle2D.Double(lx - 14, ly - 14, 28, 28, 3, 3)); - g2d.setColor(LED_BORDER); - g2d.draw(new RoundRectangle2D.Double(lx - 14, ly - 14, 28, 28, 3, 3)); - - // Circular milky phosphor/lens - g2d.setColor(LED_DIFFUSER); - g2d.fill(new Ellipse2D.Double(lx - 10, ly - 10, 20, 20)); - g2d.setColor(DIFFUSER_BORDER); - g2d.draw(new Ellipse2D.Double(lx - 10, ly - 10, 20, 20)); - - // Internal tiny IC dot - g2d.setColor(CHIP_DOT_COLOR); - g2d.fill(new Rectangle2D.Double(lx - 2.5, ly - 2.5, 5, 5)); - } - - // Silk Screen Pin Labels — all 4 pins labeled on both sides with clean spacing - g2d.setColor(Color.WHITE); - g2d.setFont(PIN_LABEL_FONT); - - // Left Input Header Labels (DIN, 5V, GND, GND) - String[] inLabels = {"DIN", "5V", "GND", "GND"}; - for (int i = 0; i < 4; i++) { - StringUtils.drawCenteredText(g2d, inLabels[i], boardX + 23, y + i * spacing, - HorizontalAlignment.LEFT, VerticalAlignment.CENTER); - } - - // Right Output Header Labels (DOUT, 5V, GND, GND) - String[] outLabels = {"DOUT", "5V", "GND", "GND"}; - for (int i = 0; i < 4; i++) { - StringUtils.drawCenteredText(g2d, outLabels[i], boardX + boardW - 23, y + i * spacing, - HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + MakerBoardPainter.drawAddressableLed(g2d, boardX + fieldInset + (i + 0.5) * ledSize, + boardY + boardH / 2.0, ledSize); } } diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java index f166f0c8..8b46f178 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java @@ -262,6 +262,11 @@ public class ArduinoNano extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + return getVersion().toString(); + } + private String[] getPinNames() { switch (getVersion()) { case EVERY: diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java index d96dbe8c..7bc3a5cc 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java @@ -231,6 +231,11 @@ public class ArduinoUno extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + return getVersion().toString(); + } + /** * True for the Renesas RA4M1 generation, which shares a USB-C connector and the RA4M1 in place of * the ATmega328P. diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java index d077b377..82565695 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java @@ -354,6 +354,11 @@ public class ESP32DevKit extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + return getVersion().toString(); + } + @EditableProperty(name = "Headers") public boolean getHeaders() { return headers; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java index 68a19e24..0e7164d0 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java @@ -113,6 +113,12 @@ public class RaspberryPi extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + RaspberryPiVersion version = getVersion(); + return version == null ? null : version.toString(); + } + public RaspberryPi() { super(); this.bodyColor = RPI_GREEN; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java index 3fd993ce..4934c997 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java @@ -139,6 +139,11 @@ public class RaspberryPiPico extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + return getVersion().toString(); + } + /** * True for the wireless variants, which carry the CYW43439 radio and move the SWD pads out of * the bottom edge to make room for it. diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java index da4a839e..81bde0ed 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java @@ -115,6 +115,11 @@ public class RaspberryPiZero extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + return getVersion().toString(); + } + @EditableProperty(name = "Headers") public boolean getHeaders() { return headers; diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java index 04e91696..ce2934c2 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java @@ -238,6 +238,11 @@ public class Teensy extends AbstractMakerBoard { invalidateCache(); } + @Override + protected String getVariantLabel() { + return getVersion().toString(); + } + @EditableProperty(name = "Headers") public boolean getHeaders() { return headers; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerBoardTestSupport.java similarity index 86% rename from diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java rename to diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerBoardTestSupport.java index 6378b72f..6c672e1d 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerBoardTestSupport.java @@ -19,7 +19,7 @@ * along with DIYLC. If not, see . * */ -package org.diylc.components.micro; +package org.diylc.components.maker; import java.awt.Graphics2D; import java.awt.geom.Point2D; @@ -40,12 +40,12 @@ import org.junit.Assert; * *

What every board has in common -- naming all of its pins, keeping them apart, drawing in each * state, rotating -- is asserted once for all of them in {@code MakerComponentsTest}, but only for - * the version each board starts with. The tests in this package cover what is specific to a board: + * the version each board starts with. The per-board tests cover what is specific to a board: * its pinout, its geometry and the way those change from one version to the next. */ -class MakerBoardTestSupport { +public class MakerBoardTestSupport { - static final double PIN_SPACING = new Size(0.1d, SizeUnit.in).convertToPixels(); + public static final double PIN_SPACING = new Size(0.1d, SizeUnit.in).convertToPixels(); private static final Project PROJECT = new Project(); @@ -73,7 +73,7 @@ class MakerBoardTestSupport { }; /** Asserts that pins {@code from} through {@code to} form a straight row on 0.1" pitch. */ - static void assertRow(AbstractMakerBoard board, int from, int to) { + public static void assertRow(AbstractMakerBoard board, int from, int to) { assertRow(board, from, to, PIN_SPACING); } @@ -81,7 +81,7 @@ class MakerBoardTestSupport { * Asserts that pins {@code from} through {@code to} form a straight row on the given pitch. The * axis comes from the first pair, so a row that bends part way along fails. */ - static void assertRow(AbstractMakerBoard board, int from, int to, double pitch) { + public static void assertRow(AbstractMakerBoard board, int from, int to, double pitch) { boolean vertical = Math.abs( board.getControlPoint(from).getX() - board.getControlPoint(from + 1).getX()) < 0.01; for (int i = from; i < to; i++) { @@ -95,7 +95,7 @@ class MakerBoardTestSupport { } /** Asserts the distance between the first pins of two parallel rows. */ - static void assertRowSpacing(AbstractMakerBoard board, int first, int second, Size spacing) { + public static void assertRowSpacing(AbstractMakerBoard board, int first, int second, Size spacing) { Point2D p1 = board.getControlPoint(first); Point2D p2 = board.getControlPoint(second); Assert.assertEquals("Row spacing", spacing.convertToPixels(), p1.distance(p2), 0.01); @@ -103,7 +103,7 @@ class MakerBoardTestSupport { } /** Asserts the outline size, which is what a shield, a case or an enclosure has to fit. */ - static void assertBoardSize(AbstractMakerBoard board, Size width, Size length) { + public static void assertBoardSize(AbstractMakerBoard board, Size width, Size length) { Rectangle2D bounds = board.getBodyShape().getBounds2D(); Assert.assertEquals("Board width", width.convertToPixels(), bounds.getWidth(), 0.1); Assert.assertEquals("Board length", length.convertToPixels(), bounds.getHeight(), 0.1); @@ -114,7 +114,7 @@ class MakerBoardTestSupport { * control points in the same place. A revision that keeps its footprint has to keep its pin * positions too, or switching the version moves every connection in an existing file. */ - static void assertSharedFootprint(AbstractMakerBoard reference, AbstractMakerBoard variant, + public static void assertSharedFootprint(AbstractMakerBoard reference, AbstractMakerBoard variant, int sharedPins) { Assert.assertEquals("Outline", reference.getBodyShape().getBounds2D(), variant.getBodyShape().getBounds2D()); @@ -124,7 +124,7 @@ class MakerBoardTestSupport { } /** Draws the board in every state and orientation; anything that throws fails the test. */ - static void assertDrawsCleanly(AbstractMakerBoard board) { + public static void assertDrawsCleanly(AbstractMakerBoard board) { BufferedImage image = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = image.createGraphics(); Orientation original = board.getOrientation(); diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java index e6d574f5..28605921 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java @@ -72,6 +72,15 @@ public class MakerComponentsTest { ESP8266NodeMCU.class, WemosD1Mini.class, + // Displays & Outputs + CharacterLCD.class, + OLEDDisplay.class, + SevenSegmentDisplay.class, + LEDMatrix8x8.class, + TFTDisplay.class, + WS2812BStick.class, + WS2812BRing.class, + // Electro-Mechanical BatteryHolder18650.class ); @@ -90,15 +99,6 @@ public class MakerComponentsTest { TCRT5000Sensor.class, IRReceiverModule.class, - // Displays & Outputs - CharacterLCD.class, - OLEDDisplay.class, - SevenSegmentDisplay.class, - LEDMatrix8x8.class, - TFTDisplay.class, - WS2812BStick.class, - WS2812BRing.class, - // Modules & Breakouts LogicLevelConverter.class, L298NMotorDriver.class, diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java index 3eca17e6..9d735e06 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java @@ -23,6 +23,8 @@ package org.diylc.components.micro; import java.awt.geom.Point2D; +import org.diylc.components.maker.MakerBoardTestSupport; + import org.junit.Assert; import org.junit.Test; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java index b5bfd164..4a7e678d 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java @@ -21,6 +21,7 @@ */ package org.diylc.components.micro; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.ArduinoNano.NanoVersion; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java index 7ee3c6a3..a1903d37 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java @@ -24,6 +24,7 @@ package org.diylc.components.micro; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.ArduinoUno.ArduinoUnoVersion; import org.junit.Assert; import org.junit.Test; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java index 66f3429c..6df9b231 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java @@ -24,6 +24,7 @@ package org.diylc.components.micro; import java.awt.geom.Rectangle2D; import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.ESP32DevKit.DevKitVersion; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java index ec7ab4d0..1477d230 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java @@ -23,6 +23,7 @@ package org.diylc.components.micro; import java.awt.geom.Rectangle2D; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java index 4e49f713..5283f1bd 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java @@ -24,6 +24,7 @@ package org.diylc.components.micro; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.RaspberryPiPico.PicoVersion; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java index 7c92ce0a..4fe2c9b3 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java @@ -24,6 +24,7 @@ package org.diylc.components.micro; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.RaspberryPi.RaspberryPiVersion; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java index 8a946f04..46068db6 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java @@ -24,6 +24,7 @@ package org.diylc.components.micro; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.RaspberryPiZero.ZeroVersion; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java index d1f0f36d..61698fd3 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java @@ -21,6 +21,7 @@ */ package org.diylc.components.micro; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.components.micro.Teensy.TeensyVersion; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java index 374a67ce..463eee09 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java @@ -23,6 +23,7 @@ package org.diylc.components.micro; import java.awt.geom.Rectangle2D; +import org.diylc.components.maker.MakerBoardTestSupport; import org.diylc.core.measures.Size; import org.diylc.core.measures.SizeUnit; diff --git a/diylc/docs/plans/maker-peripherals.md b/diylc/docs/plans/maker-peripherals.md new file mode 100644 index 00000000..5de13d56 --- /dev/null +++ b/diylc/docs/plans/maker-peripherals.md @@ -0,0 +1,513 @@ +# Maker Peripherals + +Releasing the staged `Displays & Outputs` components, and closing the gaps that would stop +someone drawing a common display. + +Status: proposed +Scope: `diylc-library`, package `org.diylc.components.displays` +Companion to: `maker-board-roadmap.md` (which covers `org.diylc.components.micro` only) + +## 1. Goal + +The first maker slice shipped ten controller boards in 6.5.0. Forty-one further components sit in +the tree with their `@ComponentDescriptor` annotations commented out, across four packages: +`displays` (7), `sensors` (9), `modules` (21) and `robotics` (4). None of them is discoverable, and +`MakerComponentsTest` lists all forty-one in `unreleasedMakerComponentClasses`. + +This document plans the release of the **seven display components**, the fixes they need first, and +the additions that make the category cover the market rather than sample it. Sensors, modules and +robotics are deliberately deferred; §10 records why. + +## 2. Decisions that govern this slice + +| # | Decision | Choice | Rationale | +|---|---|---|---| +| D1 | Which package ships second | **`displays` only.** | Displays are archetype-complete in a way sensors are not: the hobby world has roughly seven display archetypes and the drafts already cover all seven, whereas the nine drafted sensors are a sample of hundreds with no canonical members. Displays also reach the *existing* guitar and amp audience — 7-segment counters, character LCDs and addressable strips appear in builds with no microcontroller anywhere. | +| D2 | How coverage grows inside an archetype | **A `Version`-style enum on the existing class where the footprint family is shared; a new class where it is not.** | Same rule as the board roadmap's D2. `TFTDisplay` and `LEDMatrix8x8` currently have no variant enum at all, which is the largest structural gap in the package. | +| D3 | Where dimension and pin data live | **Hardcoded `Size` constants and `String[]` arrays in the component class,** per-variant where a variant changes them. | Matches every shipped board. No pin-definition resource is introduced. | +| D4 | Screen content | **A neutral dark panel, with at most a small part-number silk.** No faked user interface. | Four drafts currently render English demo text ("HELLO WORLD! 16x2", "OLED DISPLAY") at fixed font sizes, which does not scale with the board, bakes a language into a drawing, and dates quickly. A dark glass panel is what the part looks like when it is not powered, which is how every other component is drawn. | +| D5 | Shared LED rendering | **One helper in `MakerBoardPainter`,** used by every addressable-RGB part. | `WS2812BRing` and `WS2812BStick` already contain two copies of the 5050-package-plus-diffuser-plus-IC-dot drawing, one scaled and one in raw pixels. A strip and a panel would make four. | +| D6 | Backward compatibility | Nothing here is a compatibility risk. | These classes have never been discoverable, so no `.diy` file references them. Field names, control-point counts and defaults can still be changed freely — **this slice is the last chance to do so.** | +| D7 | Geometry expressed in `Size` | **Every dimension derives from a `Size` constant.** | The drafts place bezels, screens and mounting holes with raw pixel offsets (`boardX = x - 60`, `bezelY = boardY + 45`), so nothing moves if a board dimension is corrected. The shipped boards do not do this. | + +### Explicitly out of scope + +- Sensors, modules and robotics (§10). +- The `AbstractMotor` / `AbstractMakerBoard` duplication, which only `robotics` suffers from. +- The board roadmap's open decisions on BOM variant treatment and a uniform `Headers` property set + (§11 restates them, because this slice adds variants that make them more pressing). +- Bumping the project version in the POMs. + +## 3. Current coverage + +Seven classes, all `category = "Displays & Outputs"`, all `bomPolicy = SHOW_ONLY_TYPE_NAME`, all +`enableCache = true`, all extending `AbstractMakerBoard`. + +| Class | Variants today | Assessment | +|---|---|---| +| `SevenSegmentDisplay` | 1-digit 10-pin, 4-digit 12-pin, TM1637 module | **Best-*written* file in the set**, which is not the same as the best-sourced one. The segment-mask table is genuinely good, the 10-pin structure is corroborated, and the glyph itself is now the standard unit-grid construction: an upright 10 × 18 box with segments two units thick, mitred at 45 degrees so neighbours meet at a shared vertex, each outlined in the face colour so that they still read as separate segments. That replaced a set of hand-tuned polygons and retired both `SEGMENT_GAP` (the construction separates by outline rather than by gap) and `SLANT_DEGREES` (it is drawn upright, as the source is). What remains unchecked is everything *around* the glyph. The per-package dimensions now live on the `DisplayType` constants rather than in scattered branches, and the enum has been split so that the two bare four-digit packages — 0.36" and 0.56" — are separate variants sharing one pin array. The 0.56" set is what this class has always carried and remains unverified; the 0.36" set is now largely maintainer-supplied — body 30 × 14 mm, 7.5 mm digit pitch, 0.4" row spacing — leaving only its digit width inferred. See §11.10 and §11.11. | +| `WS2812BRing` | 12 / 16 / 24 LED | **Good.** Genuine polar maths placing pads on the mid-radius arc, mm-based OD/ID per size, LED count driving the rendering, `drawSolderPads` rather than a header — correct for a ring. | +| `CharacterLCD` | 16x2 / 20x4, I2C backpack / 16-pin parallel | Correct HD44780 and PCF8574 pin data, correct 80 × 36 and 98 × 60 mm bodies. All screen and bezel geometry in raw pixels, including an eyeballed `bezelMarginX = (16x2) ? 45 : 40`. Allocates a `Font` inside `draw()`. | +| `OLEDDisplay` | I2C 4-pin / SPI 7-pin | Correct pin names and a correct 27 mm square body, but **the glass is drawn near-square** when the 0.96" SSD1306 active area is a roughly 2:1 letterbox. No size variants. | +| `LEDMatrix8x8` | none | Plausible single MAX7219 module with correct cascade headers, but a hardcoded `-44 mm` output-header offset and pixel-placed matrix and chip. **Missing the 4-in-1 32x8 module.** | +| `TFTDisplay` | none | **Weakest file.** No variant enum. Name and descriptor say 240x320 while the on-screen silk says `320x240`. See the footprint bug below. | +| `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. | + +### 3.1 Two defects that are bugs, not style + +**`TFTDisplay` header edge.** The 14-pin header is laid out along the 86 mm edge, but on the real +2.8" ILI9341 module that header runs along the short 50 mm edge — 14 pins on 0.1" pitch is 33 mm, +which fits in 50 mm and not in 86 mm. The module is portrait and the draft draws it landscape. The +board outline, the header edge and the screen aspect all need re-deriving together from the module +drawing. + +**`WS2812BStick` duplicate node names.** `PIN_NAMES` declares `GND (In)` twice and `GND (Out)` +twice, so each end of the board carries two identically named nodes. This is the same duplicate-node +problem that was just fixed for building-block terminals, and it will reach the netlist. The real +pad count per end has to come from the product drawing; the draft's four-per-end with a repeated +ground is almost certainly wrong. + +### 3.2 Cross-cutting items + +1. **`zOrder` splits the package.** `CharacterLCD` and `TFTDisplay` declare `IDIYComponent.BOARD`; + the other five declare `COMPONENT`. A display sits on top of a board rather than being one, so + `COMPONENT` is right for all seven. Same class of oversight as the board roadmap's §8.1 Teensy + item. +2. **No file uses the shared silk machinery.** `drawPinLabels` / `getSilkPinLabel` exist on + `AbstractMakerBoard` and four shipped boards use them; here `WS2812BStick` and `WS2812BRing` + hand-roll label arrays that duplicate their own `PIN_NAMES` and will drift from them. +3. **Fonts allocated inside `draw()`** in `CharacterLCD` (`new Font("Monospaced", ...)`) and + `WS2812BRing` (`new Font("SansSerif", BOLD, 8)`). `draw()` runs on every repaint; the convention + is a cached static. +4. **Non-ASCII characters** in `SevenSegmentDisplay` and `WS2812BStick`. Source encoding is + ISO-8859-1 and all ten shipped `micro` classes are clean, so this arrived with the drafts. +5. **Unused `java.awt.Font` import** in `LEDMatrix8x8`. +6. **The NeoPixel Ring's pad labels collide** — **resolved by dropping them.** They were drawn at + `innerR + 5`, and because the four pads sit one pin spacing apart along the arc, each label had + roughly 15 px of arc for 24-30 px of text. No radius or font size fixes that: a larger radius + spreads the labels but they are anchored to pads that do not move, and a smaller font stops + being legible long before it stops overlapping. Moving them away from their pads would defeat + the point of a pad label, so the names are left to the node tooltips and the netlist, as on the + Stick. The array also duplicated `PIN_NAMES` and had already drifted from it, printing `5V` + where the node name says `+5V`. +7. **Connectors are not centred on their edge.** In `CharacterLCD` and `TFTDisplay` the header + starts at the control-point origin while the board is placed by a raw pixel offset (−60 and −70 + respectively), so the connector sits in a corner instead of centred where the product has it. + `OLEDDisplay` was wrongly included here at first: its `boardX` formula does centre the pin row, + and only its vertical offset was a raw pixel value. This is the visible face of D7, and it + resolves when the geometry is re-derived from `Size` constants in item 6. + +8. **The Ring's pad arc and LED arc coincide.** Both the solder pads and the LEDs are placed on the + mid-radius circle, so the four pads sit on top of LEDs at the bottom of the ring. Nothing is + hidden — `drawSolderPads` runs last, after the transform is restored, so the pads draw over the + LEDs — but on the real board the pads occupy a gap in the LED sequence rather than sharing it. + Whether the LED ring should skip the pad positions is a fidelity question for item 10. + +9. **`LEDMatrix8x8`'s `OUT` label sits inside the matrix block.** The output header row is 3.46 mm + below the top edge and the matrix starts 3.8 mm below it, so the strip between them is too + narrow for the label at any offset: placing it below the row puts it 1.66 mm inside the matrix, + and placing it above the row runs it into the board edge. This predates the `Size`-constant + rewrite — the previous raw offset put it 0.5 mm inside the same block — so the rewrite preserved + the overlap rather than causing it. Fixing it needs the real module's silkscreen layout relative + to its matrix, which is item 10. + +10. **Every one of the Ring's six diameters was wrong.** The enum carried 37.0/27.0, 44.5/32.0 and + 66.0/52.0 for the 12, 16 and 24 LED rings; Adafruit publishes 36.8/23.3, 44.5/31.7 and + 65.5/52.3. The outer diameters were near-misses but the inner ones were not — the 12-LED ring + was out by 3.7 mm on a 36.8 mm part. That is not cosmetic: `getMidRadius()` averages the two, + and that radius places the pads, the LEDs and the control points, so the 12-ring's geometry sat + about a millimetre out everywhere. This was the first substantive error item 10 turned up, and + it is the argument for finishing the sweep rather than trusting the remaining drafts. + +Items 6 through 9 were found by rendering the components, not by reading them; item 10 was found by +checking a vendor page. Since no regression +baseline exists, an offscreen render harness — instantiate by class name, draw into a +`BufferedImage` sized from `getCachingBounds()`, write a PNG per component — is the cheapest way to +see this class of defect, and it works whether or not the descriptors are enabled. Note that moving +such a component requires shifting **every** control point: `setControlPoint(p, 0)` moves only that +point and does not re-run `updateControlPoints()`, so the body follows point 0 while the remaining +pins stay behind. + +## 4. Shared mechanics + +The board roadmap's §4 applies unchanged and is not repeated: component discovery is a classpath +annotation scan with no registry to edit, the `Version` enum pattern and its null-guard, XStream +serialising enum constants by name, and the sourcing rules in §4.5. What differs for this slice: + +**Test support has to be promoted.** `MakerBoardTestSupport` is package-private in +`org.diylc.components.micro`, so a `displays` test package cannot see its `assertRow`, +`assertRowSpacing`, `assertBoardSize` and `assertSharedFootprint` helpers or its shared no-op +`IDrawingObserver`. Move it to `org.diylc.components.maker` beside `MakerComponentsTest` and make it +public, updating the ten `micro` tests' imports. Do this first; everything in §7 depends on it. + +**One new painter helper.** `MakerBoardPainter.drawAddressableLed(g2d, cx, cy, size)` drawing the +5050 white package, the milky diffuser and the internal IC dot, all derived from `size`. Port +`WS2812BRing` onto it (its version is already correct, so its rendering must not change), then +`WS2812BStick`, before any new addressable part is written. + +**Release mechanics.** Uncommenting a `@ComponentDescriptor` is what ships a component. Each release +also needs the class moved from `unreleasedMakerComponentClasses` to `releasedMakerComponentClasses` +in `MakerComponentsTest`, so that the discovery and icon assertions start covering it. + +## 5. Prep pass on the existing seven + +Ordered by risk, not by file size. None of these is a compatibility concern (D6). + +| # | Work | Files | Rough effort | +|---|---|---|---| +| 1 | Promote `MakerBoardTestSupport` to `org.diylc.components.maker`, make it public, fix the ten `micro` imports — **done** | 11 test files | 1 h | +| 2 | Add `MakerBoardPainter.drawAddressableLed`; port `Ring` (no visual change) then `Stick` — **done** | 3 | 2-3 h | +| 3 | Re-derive the `TFTDisplay` footprint from the module drawing: outline, header edge, screen aspect, SD-slot position (§3.1) — **done**: portrait 50 × 86 mm, header centred on the short edge, screen derived from the glass, four holes placed from the vendor drawing and the glass centred between them. The front-face SD slot was removed rather than moved, because the reader is on the back | 1 | 3-4 h | +| 4 | Fix `WS2812BStick` pad names and count from the product drawing; rebuild its geometry from `Size` constants (§3.1) — **done**, with one correction to the premise: the **count was already right**. Four pads per end is what the board has, and the doubled ground is real hardware on one net. Only the *order* was wrong, so the pads were reordered to GND, data, power, GND and renamed `GND_1`…`GND_4` for uniqueness. Board corrected to 51.1 × 10.22 mm with real 5 mm packages | 1 | 2-3 h | +| 5 | Fix the `OLEDDisplay` glass aspect to the real active area — **done**: panel 26.7 × 19.3 mm centred between the hole rows, with the 21.744 × 10.864 mm lit area drawn inside it | 1 | 1 h | +| 6 | Replace raw pixel geometry with `Size` constants (D7) — **done for `OLEDDisplay`, `TFTDisplay`, `LEDMatrix8x8` and `WS2812BStick`; blocked for `CharacterLCD`**, see §11.8 | `CharacterLCD`, `OLEDDisplay`, `TFTDisplay`, `LEDMatrix8x8`, `WS2812BStick` | 4-6 h | +| 7 | Replace demo screen text with a neutral panel (D4) — **done**: all three now draw an unpowered panel, and `CharacterLCD`'s `SCREEN_TEXT` colour went with its lettering | `CharacterLCD`, `OLEDDisplay`, `TFTDisplay` | 1-2 h | +| 8 | Route silk labels through `getSilkPinLabel` / `drawPinLabels` — **done**: the Stick's now come from `getSilkPinLabel` and the Ring's are dropped; removing the Ring's inline `new Font(...)` also settles half of item 9 | `WS2812BStick`, `WS2812BRing` | 1-2 h | +| 9 | `zOrder` to `COMPONENT` on `CharacterLCD` and `TFTDisplay`; cache the two `draw()` fonts; strip non-ASCII; drop the unused import — **done**: all seven are `COMPONENT`, both `draw()` fonts left with items 7 and 8, `SevenSegmentDisplay`'s degree signs were the last non-ASCII, and the orphaned imports plus `CharacterLCD.SCREEN_TEXT` are gone. The three surviving `new Font(...)` calls are all in `drawIcon`, which runs per toolbox icon rather than per repaint, so they are deliberately left | 5 | 1 h | +| 10 | Cross-check every surviving dimension and pin array against vendor documentation — **partly done.** Closed: `TFTDisplay` (outline, active area, header edge, hole positions, glass placement), `WS2812BStick` (board, 5050 package, pad count and order), `OLEDDisplay` (panel, lit area, and a 24 mm centre-to-centre hole pattern in both directions, modelled as spacing rather than an edge inset since that is how it is specified and what a builder drills), and `WS2812BRing`'s diameters — **all six of which were wrong**, see §3.2 item 10. Open: the Ring's pad count (§11.9), `SevenSegmentDisplay`'s pin arrays (§11.10), `CharacterLCD`'s part identity (§11.8), `LEDMatrix8x8`'s silk placement (§3.2 item 9), and the Ring's pad/LED arc sharing (§3.2 item 8) | all 7 | the bulk of the slice | + +Items 1 and 2 have landed. The five shared 5050-package colours now live on `AbstractMakerBoard` as +`RGB_LED_*`, and the duplicated copies are gone from both NeoPixel classes along with two dead +constants (`SOLDER_PAD_COLOR` / `SOLDER_PAD_BORDER`) that nothing referenced. `WS2812BRing` renders +identically, because the helper reproduces its geometry exactly; `WS2812BStick` changed very +slightly — corner radius, lens diameter and border stroke now match the ring, which is the intended +consequence of there being one renderer. The stick's LED size is still the literal 28 px it always +was, since deriving it from the real 5.0 mm package changes the drawing and belongs to item 4. + +Items 3 and 4 have largely landed, against sourced figures. The TFT module is **50 × 86 mm +portrait** with a **43.2 × 57.6 mm** active area, and its fourteen pin names and their order already +matched the product, so only the geometry was wrong: the outline is now portrait, the header is +centred on the short top edge, the screen is derived from the glass rather than from pixel offsets, +and the front-face microSD block is gone because the reader is on the back. The NeoPixel Stick is +**51.1 × 10.22 mm** with **5.0 mm** packages, and its four-pads-per-end turned out to be correct — +the order was not. Each end reads GND, data, power, GND, and the doubled ground is real hardware on +one net, so the pads are now named `GND_1`…`GND_4` and the silkscreen prints "GND" for all four via +`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 +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. + +The glass turned out to be centred between the two rows of mounting holes rather than measured from +an edge, which resolves the apparent conflict: its midpoint is 44.96 mm from the top, so a 69.1 mm +glass spans 10.41 mm to 79.51 mm and clears both pairs. The drawing derives it that way rather than +storing another offset constant, so it stays correct if a hole position is ever revised. + +Item 10 is the real cost and cannot be shortcut. These classes were drafted without sources, so +**every number in them is an estimate until checked**, including the ones this document quotes as +correct. What works as a source: Adafruit product pages and their EagleCAD/Fritzing footprints for +the NeoPixel parts; the SSD1306, SH1106, ILI9341, ST7735, ST7789, PCD8544, MAX7219 and TM1637 +datasheets for controllers; generic module drawings from the AliExpress or Waveshare listing for the +red SPI TFT and the I2C backpack, which have no canonical vendor. + +## 6. Gap implementation plan + +The archetypes are covered; the gaps are inside them. Each item below is an independent commit. + +### 6.1 NeoPixel Strip — new class `WS2812BStrip` + +The most-used addressable form factor, absent entirely, and the one addition that also serves the +existing guitar and amp audience. + +1. New class in `displays` extending `AbstractMakerBoard`, GPL header from `HEADER.txt`, descriptor + `name = "NeoPixel Strip (WS2812B)"`, `zOrder = COMPONENT`, `enableCache = true`. +2. `Density` enum carrying LEDs per metre and the resulting pitch: `_30("30 LED/m")`, + `_60("60 LED/m")`, `_144("144 LED/m")`. Strip width comes from the density (10 mm and 12 mm are + the common tapes; 144/m is usually 12 mm). +3. `LedCount` as an `@EditableProperty` integer, defaulting to 8, with a sane upper bound so a + careless value cannot allocate an enormous control-point array. Four control points regardless of + LED count: `DIN`, `+5V`, `GND` at one end and `DOUT` at the other — the LEDs are drawn, not + wired, exactly as the ring does it. +4. `getBodyShape()` is a rounded rectangle whose length is `ledCount * pitch`, so the part grows + with the property. +5. `draw()` fills the tape, then loops `drawAddressableLed` at the pitch, then `drawSolderPads` at + each end. Silk via `getSilkPinLabel`. +6. `drawIcon()` draws a short horizontal tape with four coloured dots, in the style the `Stick` + icon already uses. + +Tests: `assertBoardSize` at two densities, control-point count fixed at 4 across LED counts, a +length-scales-with-count assertion, draw smoke at both densities. **~1 day including sourcing.** + +### 6.2 `TFTDisplay` variants — `Controller` enum + +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. + +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. +2. Per-variant body size, pin count and pin array. The ILI9341 module is 14 pins with the touch and + SD lines; the ST7735 and ST7789 boards are 8 pins (`GND VCC SCL SDA RES DC CS BLK`); the GC9A01 + is 7 or 8 depending on the board. +3. `updateControlPoints()` sizes the array from the per-variant pin list, as `ESP32DevKit` does. +4. `getBodyShape()` branches: rounded rectangle for three of them, a **circle with a tab** for the + GC9A01, which is the one genuinely new outline. +5. `draw()` branches on the screen aspect and on whether the SD-slot and touch hardware exist — only + the ILI9341 has them. +6. The round variant is worth the effort beyond its own popularity: it is what gauge, VU and clock + projects reach for, which overlaps the existing audience. + +Tests: per-variant `assertBoardSize` and pin-count, a round-outline assertion, draw smoke across all +four. **~1.5-2 days, the largest item here.** + +### 6.3 `OLEDDisplay` variants — `Version` enum + +Do this in the same pass as the §5 item 5 glass fix. + +1. `Version` enum: `SSD1306_0_96("0.96\" SSD1306 128x64")`, `SSD1306_0_91("0.91\" SSD1306 128x32")`, + `SH1106_1_3("1.3\" SH1106 128x64")`. Default is the 0.96, so nothing about the current look + changes. +2. Per-variant board size — the 0.91" module is a small wide board, the 1.3" is larger than the + 0.96" — and per-variant glass size and position, taken from the active-area dimensions in the + controller datasheets rather than guessed as a fraction of the board. +3. Pin arrays are unchanged: the interface property already covers I2C 4-pin and SPI 7-pin, and it + is orthogonal to the size. Both properties coexist; guard the combinations that are not sold + (the 0.91" is I2C only) by branching in `draw()`, not by removing the property. +4. `setVersion()` calls `updateControlPoints()` and `invalidateCache()`. + +Tests: per-variant size, glass-aspect assertion, the interface property round-tripping against each +version. **~0.5 day.** + +### 6.4 `LEDMatrix8x8` cascade — `Modules` enum + +1. `Modules` enum: `_1("Single 8x8 (32x32mm)")`, `_4("4-in-1 32x8 (128x32mm)")`. The 4-in-1 is + probably the more commonly bought of the two. +2. Board length scales with the module count; the input and output headers stay at the two ends. +3. `draw()` loops the matrix block and its MAX7219 once per module, and draws a demo pattern across + the full width rather than repeating an 8x8 pattern four times. +4. Rename nothing: the class name `LEDMatrix8x8` is now slightly narrow, but it has to stay, and + the descriptor `name` can be widened to "LED Matrix (MAX7219)" freely because no file + references it yet (D6). +5. While in the file, replace the hardcoded `-44 mm` header offset with a derived value. + +Tests: per-variant size, header positions at both ends in both variants, draw smoke. +**~0.5-1 day.** + +### 6.5 Nokia 5110 LCD — new class `Nokia5110LCD` + +Still in every starter kit, a distinct 8-pin footprint, and simple geometry: a board with a large +glass area and a single 1x8 header. PCD8544 controller, 84x48 pixels, `RST CE DC DIN CLK VCC BL GND`. +One class, no variants. **~0.5 day.** + +### 6.6 LED Bar Graph — new class, base class to be decided + +Ten segments in a 20-pin DIP outline. Nothing in the library covers it: `LED`, `LEDSymbol` and +`PilotLampHolder` are the closest, and none is a segmented bar. It is the cheapest item here and the +only one that pays off entirely within the **existing** audience, for VU and level meters. + +The base class is an open question (§11.3). `AbstractMakerBoard` is the wrong fit — this is not a +board — and the established shape for a dual-row DIP part is `DIL_IC`, which extends +`AbstractLabeledComponent`, carries a `PinCount` enum and points at a dedicated +`DIL_ICTransformer`. Recommendation: follow `DIL_IC`, with a fixed 20-pin geometry rather than a pin +count property, a `Segments` enum for the 10-segment and 8-segment variants, and a `Color` property +for the lit colour. Category `Displays & Outputs` even though the neighbours in shape live in +`Semiconductors`, because that is where someone will look for it. + +**~0.5-1 day once the base class is settled.** + +### 6.7 Cheap additions + +- **`WS2812BRing`**: append `_8_LED` and a `_7_LED` Jewel constant to `RingSize` with their OD/ID. + Two lines plus sourcing. **~1 h.** +- **NeoPixel panel 8x8** as its own class, reusing `drawAddressableLed` and the ring's pad handling. + Worth doing only after §6.1, whose helper it depends on. **~0.5 day.** + +### 6.8 Deliberately not in this slice + +E-paper (1.54" / 2.9" SSD1680) — growing but still niche, and the flexible-cable mounting is real +new drawing work. Also: 14- and 16-segment alphanumeric, HT16K33 backpacks, 1.2" and 2.3" large +digits, 3.5" ILI9488, VFD, DotStar and APA102, and the RGB-backlight 1602. + +## 7. Testing + +Per-class tests under `diylc-library/src/test/java/org/diylc/components/displays/`, named +`*Test.java`, GPL header like any other file, following the `micro` pattern and using the promoted +`MakerBoardTestSupport`: + +- `testControlPointCountAndNames()` — count, no empty names, spot checks, and for + `WS2812BStick` an explicit **no duplicate node names** assertion given §3.1. +- `testDimensionsAndGeometry()` — `assertBoardSize` against the sourced dimensions, header edge and + pitch via `assertRow`. +- `testVariantProperty()` — default, labels, round-trip through the setter, for every class that + gains an enum in §6. +- a drawing smoke test over every variant in both header states and all three draw modes. + +`MakerComponentsTest` keeps its generic coverage; move the seven (then nine) classes into +`releasedMakerComponentClasses` as they ship, and add the new classes to the list as they are +written. + +## 8. Regression suite and release + +**There is no rendering-regression coverage for any maker component at all.** No `.diy` file among +the 1418 in `diylc-regression-data` references anything in `org.diylc.components.micro`, so batch 1 +shipped with none either. This slice should add a small number of sample projects — one per display +class with a couple of variants each, ideally wired to a controller from batch 1 — and regenerate +the PNG and netlist baselines. That is the only mechanism that would catch a silent rendering or +node-naming change later, and the netlist baseline is what would have caught §3.1's duplicate +grounds. + +Release notes go in a **new `6.7.0` block** in `diylc-core/src/main/resources/update.xml`. 6.6.0 is +already dated and released, and released blocks are not edited. One `NEW_FEATURE` entry per palette +entry, following the 6.5.0 wording for the controllers ("Arduino Nano - ATmega328, 33 BLE, ...": +part name, then the variant list). + +## 9. Sequencing + +1. ~~**§5 items 1 and 2**~~ — **done**: test support promotion and the shared LED helper. Everything + else builds on them. +2. **§5 items 3-9** — the prep pass, finishing with the sweep in item 9. +3. **§5 item 10 and §6.2-6.4** — sourcing, folded into the variant work for the three classes that + gain enums, since both touch the same constants. +4. **§6.1, §6.5, §6.6, §6.7** — the new classes, each an independent commit. +5. **§7** tests alongside each of the above, not after. +6. **§8** regression samples and the `update.xml` block, last. + +Nine palette entries at the end of it: the seven existing, plus the strip and the bar graph, plus +the Nokia and the panel if appetite holds. + +## 10. Later slices, and why they are later + +**`sensors` (9 drafts).** The archetype argument fails here: there are hundreds of sensor breakouts +and no canonical member of most functions, so nine drafted parts are a sample rather than coverage. +Four of them — HC-SR04 ultrasonic, DHT11/22, PIR HC-SR501, MPU6050 — are genuinely the unrivalled +answer in their function and appear in every beginner kit; the other five (LDR, soil moisture, +TCRT5000, IR receiver KY-022, BME280) are commodity KY-style three-pin boards, visually near +identical, carrying most of the per-part sourcing cost for the least distinctiveness. When this +slice happens, ship the four and leave the rest. Do **not** collapse the commodity tail into a +parameterised "three-pin sensor module": that is the shape of the generic configurable board that +was already built and reverted, and it fails for the same reason — people search the palette for a +product name. + +**`modules` (21 drafts).** The largest and least homogeneous batch — motor drivers, buck and boost +converters, chargers, radios, RTCs — and the worst geometry drift in the tree (`ULN2003Driver` has +24 raw pixel offsets, `PCA9685ServoDriver` 22, `L298NMotorDriver` 18). Worth splitting by function +rather than shipping as one category. One known overlap to settle first: the drafted +`ActiveBuzzerModule` (KY-012) against the existing `misc/Buzzer` ("PCB Buzzer, active or passive"). + +**`robotics` (4 drafts).** Blocked on an architectural decision, not on part selection. +`AbstractMotor` is a parallel fork of `AbstractMakerBoard`, re-implementing `rotatePoints`, +`drawPins`, `getFinalBorderColor` and `getCachingBounds` plus its own copies of `PIN_COLOR`, +`PIN_SIZE` and the silk fonts. Four components do not justify a second hierarchy; either lift the +shared behaviour into a common base or take the duplication deliberately. Either way it should not +gate the displays. + +## 11. Open decisions + +1. **BOM treatment of variants** — **decided; the board roadmap's §8.2 is settled by it too.** Every + maker board uses `bomPolicy = SHOW_ONLY_TYPE_NAME` and returns `null` from `getValue()`, which + left the BOM's value column empty. That was not merely a missing label: `BomMaker` groups rows on + `typeName + "|" + value`, so every variant of a class **collapsed into one row** — two Unos of + different versions printed as a single line of quantity two, and the BOM could not say which part + to buy. + + A new `BomPolicy` constant was considered and rejected. The policy only chooses what goes in the + *name* field (`SHOW_ALL_NAMES ? name : typeName`); it has no bearing on `value` or on the grouping + key, folding the variant into the name column would break the two-column convention every other + component follows (`R1, R2` / `10k`), and filling it would mean `diylc-core` reaching into + concrete component types. + + Instead `AbstractMakerBoard` grows a `protected String getVariantLabel()` hook and overrides + `getValueForDisplay()` to return it. The variant lands in the value column, which is already + rendered, already exported and already part of the grouping key, so the rows split correctly with + **no change to `BomMaker` at all**. Eleven classes override the hook; the six boards sold in a + single form inherit the `null` default and keep an empty value. + + One consequence to remember: `getValueForDisplay()` also feeds component search in `Presenter` and + the node labels in `AbstractNetlistAnalyzer`, so netlist output gains the variant for any project + containing a maker board. No regression sample contains one today, which is precisely why this is + cheaper to do now than after §8's samples are written. +2. **A uniform property set** — the board roadmap's §8.3. This slice adds `Version`-style enums to + three more classes; deciding now whether a "Show Pin Labels" toggle and a consistent `Headers` + property belong on every maker component avoids retrofitting nine more classes later. +3. **The bar graph's base class** (§6.6) — follow `DIL_IC` on `AbstractLabeledComponent` with a + transformer, or extend `AbstractMakerBoard` for consistency with its category neighbours. The + recommendation is `DIL_IC`, because the part is a DIP package and rotation for DIP parts already + has a transformer. +4. **Whether `LedCount` on the strip should be a free integer or an enum** (§6.1). A free integer + matches how someone thinks about a tape they will cut; an enum keeps the property editor + predictable and bounds the control-point maths. The plan above assumes a bounded integer. +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. +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 — + eight 5 mm packages centred on the 51.1 mm board — and the field inset is derived from that + rather than tuned to leave text room, so `LED_FIELD_INSET` is gone. +7. **i18n** — component display names and enum labels are plain strings, absent from + `diylc-swing/src/main/resources/lang/*.txt`, as they are for the shipped boards. No translation + work is required here; noted for a future pass. +8. **`CharacterLCD`'s geometry cannot be sourced consistently** — which is why item 6 is done for + every other file and blocked for this one. "1602" names a character format, not a mechanical + part, and vendors ship several outlines under it: the enum encodes an 80 × 36 mm module, while + Raystar's RC1602A is 84 × 44 mm. The display window is worse, because sources quote different + features under similar names — 64.5 × 16 mm and 66.0 × 16 mm as "viewing area" against + 56.2 × 11.5 mm as "active area" for the 1602, and 84 × 31 mm against 76 × 26 mm for the 2004. + **No mounting-hole positions were found for any of them.** The render confirms this is a visible + defect rather than a tidiness one: the bezel currently occupies roughly 70% of the board height + when a 16 mm window on a 36 mm board should be under half. What would unblock it is picking one + specific part — as the TFT slice settled on the MSP2807 — and taking the outline, window and + hole positions from that one drawing, rather than assembling them from whichever vendor answers + a search first. +9. **How many pads does a NeoPixel Ring have?** — **and this one has a deadline.** The component + models four (`DIN`, `+5V`, `GND`, `DOUT`), and the NeoPixel guide confirms those labels are + right. But the 16-LED ring's own changelog records that Adafruit "added an extra ground and + power breakout" in July 2014, so the shipping part almost certainly has six. Three Adafruit + sources — the product pages, the rings guide and the basic-connections guide — all decline to + state the count. **Pad count is control-point count.** Under D6 that is free to change today, + because nothing shipped references these classes; once 6.7.0 is released it becomes a + file-format migration. So this is the one open item that gets materially more expensive by + waiting, and the cheapest way to close it is a look at a physical ring or its EagleCAD files. +10. **`SevenSegmentDisplay`'s pin arrays are unverified, and one of them is suspect.** The 10-pin + structure is corroborated — a 0.56" single digit such as the 5161AS has ten pins, two of them + common — but no source consulted would give an *ordered* pin list, so `PIN_NAMES_1DIGIT` and + `PIN_NAMES_4DIGIT` remain unchecked. The specific worry is the 12-pin array: it follows the + 3641AS, which is a **0.36-inch** part, while `DIGIT_HEIGHT` declares 14.2 mm, i.e. 0.56". The + 0.56" four-digit equivalent is the 5641AS, and if its pinout differs then the component pairs + one part's pins with another part's dimensions. Four attempts failed to settle it: both xlitx + HTML pages carry only electro-optical tables, and both linked datasheets are image-encoded PDFs + of the kind that also defeated the MSP2807 manual. **This carries the same D6 deadline as §11.9 + and is worse in consequence** — a wrong dimension is a wrong picture, a wrong pin array is a + wrong netlist, and it is silent. Closing it needs a readable datasheet or a part in hand. + + Note that replacing the digit glyph with the standard unit-grid construction did **not** touch + this. The glyph governs how a lit segment is shaped; the pin arrays govern which physical pin + that segment is wired to. The first is now on a documented footing and the second is not, and + it would be easy to read the file and assume otherwise. + + The enum split does not touch it either, but it does improve the odds of settling it. Both bare + four-digit packages share `PIN_NAMES_4DIGIT`, on the assumption that a family differs in size + rather than in pin function — so **one datasheet now corrects both variants at once**. If that + assumption turns out to be wrong, the arrays have to separate, and that is itself worth knowing. + The suspicion recorded above, that the array follows the 0.36" part while the dimensions + described the 0.56" one, is precisely what having both variants is meant to resolve. +11. **What the enum split left unsourced.** The split itself is sound; what follows is geometry that + should not be mistaken for measurement. + + *The 0.36" variant is now mostly sourced.* Its body (30 × 14 mm), digit pitch (7.5 mm) and row + spacing (0.4", 10.16 mm) all came from the maintainer, and its digit height is definitional, + because 9.14 mm is what 0.36 inches means. **One figure remains invented: the digit width + (5.2 mm)**, derived by applying the 0.56" part's 8.1 : 14.2 ratio to a 9.14 mm digit — and those + 0.56" figures are themselves unverified, so it rests on an unchecked foundation. It is at least + not contradicted by the rest: four digits on 7.5 mm centres span 22.5 mm, which with one digit + width leaves about 1.15 mm clear at each end of a 30 mm body. Worth replacing with a measurement + rather than leaving as an inference. + + For the record, the values this replaced were mine and were fitted rather than measured — a + 30.2 × 14.0 body, an 8.0 mm pitch, and a 12.7 mm row spacing chosen only so the rows would sit + inside the body after the shared 0.6" value left them hanging past its edges. + + *The TM1637's header position is fixed.* Its single row of four pins had been centred on the + body, putting it over the display bezel; it now sits 2.5 mm above the bottom edge on bare board, + via `HEADER_EDGE_OFFSET`. Worth knowing that many real modules carry that header on the **left + edge, vertical**, which would mean laying the single-row case out vertically rather than + horizontally — a larger change that was considered and not made. The centring predated the + split; what made it visible was rendering each variant rather than only the default one.