displays tweaks

This commit is contained in:
Bane Stojkovic
2026-09-30 17:05:19 +02:00
parent fc934647ad
commit 2447135697
30 changed files with 1186 additions and 485 deletions
@@ -496,5 +496,49 @@
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
<org.diylc.appframework.update.Version>
<versionNumber>
<major>6</major>
<minor>7</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-09-30 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>Character LCD - 16x2 and 20x4, with I2C backpack or 16-pin parallel interface</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>0.96" OLED Display (SSD1306) - I2C and SPI</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>7-Segment Display - 1-digit 0.56 inch, 4-digit in 0.36 inch and 0.56 inch, and TM1637 driver module</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>8x8 LED Matrix (MAX7219)</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>TFT Touch Screen (ILI9341) - 2.8 inch 240x320 with touch and SD card slot</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Ring (WS2812B) - 12, 16 and 24 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>NEW_FEATURE</changeType>
<description>NeoPixel Stick (WS2812B) - 8 LEDs</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Bill of Materials now lists the selected variant of microcontroller boards and displays, so different versions of the same board are no longer merged into a single row</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
</a>
</java.util.Arrays_-ArrayList>
@@ -90,6 +90,11 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
public static Color BUTTON_ACTUATOR_COLOR = Color.decode("#A0A0A0");
public static Color DARK_PLASTIC_COLOR = Color.decode("#181818");
public static Color DARK_PLASTIC_BORDER = Color.decode("#333333");
public static Color RGB_LED_BODY_COLOR = Color.decode("#FDFEFE");
public static Color RGB_LED_BODY_BORDER = Color.decode("#D0D3D4");
public static Color RGB_LED_LENS_COLOR = Color.decode("#EAECEE");
public static Color RGB_LED_LENS_BORDER = Color.decode("#BDC3C7");
public static Color RGB_LED_CHIP_COLOR = Color.decode("#333333");
public static Color SILK_COLOR = Color.WHITE;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
@@ -223,6 +228,26 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
@Override
public void setValue(Void value) {}
/**
* The variant this board is set to, or {@code null} for a board that is sold in only one form.
* Boards name their variant property differently -- {@code Version}, {@code Size}, {@code Type} --
* so each one that has one answers here for itself.
*/
protected String getVariantLabel() {
return null;
}
/**
* These boards have no value in the usual sense, so the BOM's value column carries the variant
* instead. It is not cosmetic: {@code BomMaker} groups rows on type name plus value, so without
* it every variant of a board collapses into a single row and a BOM cannot say which one to buy.
*/
@Override
public String getValueForDisplay() {
String variant = getVariantLabel();
return variant == null ? "" : variant;
}
protected abstract void updateControlPoints();
protected void invalidateCache() {}
@@ -239,4 +239,33 @@ public class MakerBoardPainter {
g2d.setColor(AbstractMakerBoard.BUTTON_ACTUATOR_COLOR);
g2d.fill(new Ellipse2D.Double(x + (w - actuatorD) / 2.0, y + (h - actuatorD) / 2.0, actuatorD, actuatorD));
}
/**
* Helper to draw an addressable RGB LED in a 5050 package -- the white body, the milky phosphor
* lens over it and the driver die visible through the lens. Shared by every WS2812B part, so
* strips, rings and sticks all render the same LED.
*
* @param g2d Graphics2D context
* @param cx centre X coordinate of the package
* @param cy centre Y coordinate of the package
* @param size edge length of the square package; lens and die scale with it
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size) {
double half = size / 2.0;
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(cx - half, cy - half, size, size, 2, 2));
g2d.setColor(AbstractMakerBoard.RGB_LED_BODY_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f));
g2d.draw(new RoundRectangle2D.Double(cx - half, cy - half, size, size, 2, 2));
double lensR = half * 0.7;
g2d.setColor(AbstractMakerBoard.RGB_LED_LENS_COLOR);
g2d.fill(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
g2d.setColor(AbstractMakerBoard.RGB_LED_LENS_BORDER);
g2d.draw(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
double dieSize = Math.max(2.0, lensR * 0.4);
g2d.setColor(AbstractMakerBoard.RGB_LED_CHIP_COLOR);
g2d.fill(new Rectangle2D.Double(cx - dieSize / 2.0, cy - dieSize / 2.0, dieSize, dieSize));
}
}
@@ -50,11 +50,11 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "Character LCD (16x2 / 20x4)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "HD44780-Compatible Character LCD Display (Parallel / I2C Backpack)",
// instanceNamePrefix = "LCD", zOrder = IDIYComponent.BOARD,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "Character LCD (16x2 / 20x4)", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "HD44780-Compatible Character LCD Display (Parallel / I2C Backpack)",
instanceNamePrefix = "LCD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class CharacterLCD extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
@@ -93,9 +93,11 @@ public class CharacterLCD extends AbstractMakerBoard {
@Override public String toString() { return label; }
}
// clearance from the top edge to the pin row
public static Size HEADER_OFFSET = new Size(2.54d, SizeUnit.mm);
public static Color PCB_GREEN = Color.decode("#1B5E20");
public static Color SCREEN_BG = Color.decode("#1E88E5");
public static Color SCREEN_TEXT = Color.decode("#FFFFFF");
public static Color BEZEL_COLOR = Color.decode("#212121");
public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SDA", "SCL"};
@@ -137,6 +139,17 @@ public class CharacterLCD extends AbstractMakerBoard {
invalidateCache();
}
// Both properties decide which part you buy, so the BOM carries them together.
@Override
protected String getVariantLabel() {
LCDSize lcdSize = getLcdSize();
if (lcdSize == null) {
return null;
}
LCDInterface lcdInterface = getLcdInterface();
return lcdInterface == null ? lcdSize.toString() : lcdSize + ", " + lcdInterface;
}
@EditableProperty(name = "Backlight Color")
public Color getScreenColor() {
return screenColor;
@@ -157,15 +170,36 @@ public class CharacterLCD extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
private int getPinCount() {
return lcdInterface == LCDInterface.I2C_Backpack ? PIN_NAMES_I2C.length
: PIN_NAMES_PARALLEL.length;
}
private double getBoardWidth() {
return new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
}
private double getBoardLength() {
return new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
}
/** Left edge of the board, derived so that the pin row sits centred on the top edge. */
private double getBoardX(double x) {
double spacing = PIN_SPACING.convertToPixels();
return x - (getBoardWidth() - (getPinCount() - 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();
int pinCount = (lcdInterface == LCDInterface.I2C_Backpack) ? 4 : 16;
double[][] relativeOffsets = new double[pinCount][2];
for (int i = 0; i < pinCount; i++) {
double[][] relativeOffsets = new double[getPinCount()][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
@@ -176,13 +210,8 @@ public class CharacterLCD extends AbstractMakerBoard {
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
double boardH = new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
double boardX = x - 60;
double boardY = y - 20;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10);
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getBoardWidth(),
getBoardLength(), 10, 10);
}
@Override
@@ -201,11 +230,10 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
double boardH = new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
double boardX = x - 60;
double boardY = y - 20;
double boardW = getBoardWidth();
double boardH = getBoardLength();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -246,14 +274,6 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setColor(screenColor);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
// Render character boxes
g2d.setColor(SCREEN_TEXT);
g2d.setFont(new Font("Monospaced", Font.BOLD, (lcdSize == LCDSize._16x2) ? 14 : 11));
String line1 = (lcdSize == LCDSize._16x2) ? "HELLO WORLD! 16x2" : "DIYLC LCD MODULE";
String line2 = (lcdSize == LCDSize._16x2) ? "DIY LAYOUT CREATOR" : "20x4 CHARACTER LCD";
StringUtils.drawCenteredText(g2d, line1, screenX + screenW / 2.0, screenY + screenH / 3.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, line2, screenX + screenW / 2.0, screenY + screenH * 2.0 / 3.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
g2d.setTransform(oldTx);
@@ -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;
@@ -50,11 +49,11 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "8x8 LED Matrix (MAX7219)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "MAX7219 Dot LED Matrix Display Module (Cascadable SPI)",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "8x8 LED Matrix (MAX7219)", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "MAX7219 Dot LED Matrix Display Module (Cascadable SPI)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class LEDMatrix8x8 extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
@@ -66,6 +65,16 @@ public class LEDMatrix8x8 extends AbstractMakerBoard {
public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm);
// the matrix block is very nearly as wide as the board it sits on
public static Size MATRIX_SIZE = new Size(30.0d, SizeUnit.mm);
public static Size MATRIX_TOP_MARGIN = new Size(3.8d, SizeUnit.mm);
// clearance from the bottom edge to the input pin row; the output row sits a board apart from it
public static Size HEADER_OFFSET = new Size(2.54d, SizeUnit.mm);
public static Size HEADER_SPACING = new Size(44.0d, SizeUnit.mm);
public static Size CHIP_MARGIN_X = new Size(2.54d, SizeUnit.mm);
public static Size CHIP_LENGTH = new Size(5.7d, SizeUnit.mm);
public static Size CHIP_GAP = new Size(1.9d, SizeUnit.mm);
public static Size SILK_OFFSET = new Size(2.0d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
// Input Header (0..4)
@@ -105,16 +114,13 @@ public class LEDMatrix8x8 extends AbstractMakerBoard {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[10][2];
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
// Input header (0..4) at bottom
// the input row sits near the bottom edge and the output row the same distance from the top
double topY = -HEADER_SPACING.convertToPixels();
for (int i = 0; i < 5; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
// Output header (5..9) at top
double topY = -new Size(44.0d, SizeUnit.mm).convertToPixels();
for (int i = 0; i < 5; i++) {
relativeOffsets[5 + i][0] = i * spacing;
relativeOffsets[5 + i][1] = topY;
}
@@ -122,16 +128,21 @@ public class LEDMatrix8x8 extends AbstractMakerBoard {
rotatePoints(firstPoint, relativeOffsets);
}
/** Left edge of the board, derived so that the five-pin rows sit centred across it. */
private double getBoardX(double x) {
double spacing = PIN_SPACING.convertToPixels();
return x - (BOARD_WIDTH.convertToPixels() - 4 * spacing) / 2.0;
}
private double getBoardY(double y) {
return y - BOARD_HEIGHT.convertToPixels() + HEADER_OFFSET.convertToPixels();
}
@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 - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 20;
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8);
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), 8, 8);
}
@Override
@@ -152,8 +163,8 @@ public class LEDMatrix8x8 extends AbstractMakerBoard {
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - boardH + 20;
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -169,10 +180,9 @@ public class LEDMatrix8x8 extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// 8x8 LED Matrix block (square 32mm)
double matrixSize = boardW - 10;
double matrixX = boardX + 5;
double matrixY = boardY + 30;
double matrixSize = MATRIX_SIZE.convertToPixels();
double matrixX = boardX + (boardW - matrixSize) / 2.0;
double matrixY = boardY + MATRIX_TOP_MARGIN.convertToPixels();
g2d.setColor(MATRIX_BODY);
g2d.fill(new RoundRectangle2D.Double(matrixX, matrixY, matrixSize, matrixSize, 6, 6));
@@ -196,14 +206,21 @@ public class LEDMatrix8x8 extends AbstractMakerBoard {
}
}
// MAX7219 IC
MakerBoardPainter.drawChip(g2d, boardX + 20, matrixY + matrixSize + 15, boardW - 40, 45, "MAX7219");
double chipMarginX = CHIP_MARGIN_X.convertToPixels();
MakerBoardPainter.drawChip(g2d, boardX + chipMarginX,
matrixY + matrixSize + CHIP_GAP.convertToPixels(), boardW - 2 * chipMarginX,
CHIP_LENGTH.convertToPixels(), "MAX7219");
// Silkscreen
// each label sits just inside the board from the row it names
g2d.setColor(Color.WHITE);
g2d.setFont(SILK_FONT_SMALL);
StringUtils.drawCenteredText(g2d, "IN", x + 2 * PIN_SPACING.convertToPixels(), y - 16, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "OUT", x + 2 * PIN_SPACING.convertToPixels(), y - boardH + 34, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
double silkOffset = SILK_OFFSET.convertToPixels();
double silkX = x + 2 * PIN_SPACING.convertToPixels();
StringUtils.drawCenteredText(g2d, "IN", silkX, y - silkOffset, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER);
StringUtils.drawCenteredText(g2d, "OUT", silkX,
y - HEADER_SPACING.convertToPixels() + silkOffset, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER);
}
g2d.setTransform(oldTx);
@@ -50,11 +50,11 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "0.96\" OLED Display (SSD1306)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "0.96\" Monochrome 128x64 OLED Display Module (I2C / SPI)",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "0.96\" OLED Display (SSD1306)", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "0.96\" Monochrome 128x64 OLED Display Module (I2C / SPI)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class OLEDDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
@@ -70,10 +70,25 @@ public class OLEDDisplay extends AbstractMakerBoard {
public static Color OLED_BLUE = Color.decode("#004488");
public static Color GLASS_COLOR = Color.decode("#0D1B2A");
public static Color GLASS_BORDER_COLOR = Color.decode("#334E68");
public static Color ACTIVE_AREA_COLOR = Color.decode("#060D15");
public static Color PIXEL_BLUE = Color.decode("#00D4FF");
public static Color PIXEL_YELLOW = Color.decode("#FFD700");
public static Size BOARD_SIZE = new Size(27.0d, SizeUnit.mm);
// The panel is nearly as wide as the board but much shorter, which leaves bare PCB above it for
// the header and below it for the lower mounting holes.
public static Size GLASS_WIDTH = new Size(26.7d, SizeUnit.mm);
public static Size GLASS_LENGTH = new Size(19.3d, SizeUnit.mm);
// 128 x 64 pixels on a 0.17mm pitch. The lit area is much smaller than the panel around it and
// roughly twice as wide as it is tall, which is what makes the part read as a display.
public static Size ACTIVE_WIDTH = new Size(21.744d, SizeUnit.mm);
public static Size ACTIVE_LENGTH = new Size(10.864d, SizeUnit.mm);
// Centre-to-centre in both directions, which is how the hole pattern is specified and what a
// builder drills to; the inset from the board edge falls out of it.
public static Size MOUNTING_HOLE_SPACING = new Size(24.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
// clearance from the top edge to the pin row
public static Size HEADER_OFFSET = new Size(1.8d, SizeUnit.mm);
public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SCL", "SDA"};
public static final String[] PIN_NAMES_SPI = new String[] {"GND", "VCC", "D0 (CLK)", "D1 (MOSI)", "RES", "DC", "CS"};
@@ -97,6 +112,12 @@ public class OLEDDisplay extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
OLEDInterface oledInterface = getOledInterface();
return oledInterface == null ? null : oledInterface.toString();
}
@Override
public String getControlPointNodeName(int index) {
if (oledInterface == OLEDInterface.I2C_4Pin) {
@@ -107,15 +128,27 @@ public class OLEDDisplay extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
private int getPinCount() {
return oledInterface == OLEDInterface.I2C_4Pin ? PIN_NAMES_I2C.length : PIN_NAMES_SPI.length;
}
/** 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_SIZE.convertToPixels() - (getPinCount() - 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();
int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7;
double[][] relativeOffsets = new double[count][2];
for (int i = 0; i < count; i++) {
double[][] relativeOffsets = new double[getPinCount()][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
@@ -126,13 +159,9 @@ public class OLEDDisplay extends AbstractMakerBoard {
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardSizePx = BOARD_SIZE.convertToPixels();
int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7;
double boardX = x - (boardSizePx - (count - 1) * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - 14;
return new RoundRectangle2D.Double(boardX, boardY, boardSizePx, boardSizePx, 8, 8);
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), boardSizePx,
boardSizePx, 8, 8);
}
@Override
@@ -152,9 +181,8 @@ public class OLEDDisplay extends AbstractMakerBoard {
}
double boardSizePx = BOARD_SIZE.convertToPixels();
int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7;
double boardX = x - (boardSizePx - (count - 1) * PIN_SPACING.convertToPixels()) / 2.0;
double boardY = y - 14;
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -170,31 +198,34 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// 4 Corner mounting holes
MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16);
MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardSizePx - 16, 16);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 16, boardY + 16, 16);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 16, boardY + boardSizePx - 16, 16);
double holeInset = (boardSizePx - MOUNTING_HOLE_SPACING.convertToPixels()) / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardSizePx - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - holeInset,
boardY + boardSizePx - holeInset, holeSize);
// Glass OLED Panel
double glassMarginX = 18;
double glassW = boardSizePx - 2 * glassMarginX;
double glassH = boardSizePx - 65;
double glassX = boardX + glassMarginX;
double glassY = boardY + 45;
// The two rows of holes are inset equally, so centring the panel in the board is what centres
// it between them and keeps it clear of all four.
double glassW = GLASS_WIDTH.convertToPixels();
double glassH = GLASS_LENGTH.convertToPixels();
double glassX = boardX + (boardSizePx - glassW) / 2.0;
double glassY = boardY + (boardSizePx - glassH) / 2.0;
g2d.setColor(GLASS_COLOR);
g2d.fill(new RoundRectangle2D.Double(glassX, glassY, glassW, glassH, 4, 4));
g2d.setColor(Color.decode("#334E68"));
g2d.setColor(GLASS_BORDER_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(glassX, glassY, glassW, glassH, 4, 4));
// Display demo graphics (yellow top banner + blue body)
g2d.setColor(PIXEL_YELLOW);
g2d.setFont(SILK_FONT_SMALL);
StringUtils.drawCenteredText(g2d, "SSD1306 128x64", glassX + glassW / 2.0, glassY + 15, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
g2d.setColor(PIXEL_BLUE);
StringUtils.drawCenteredText(g2d, "OLED DISPLAY", glassX + glassW / 2.0, glassY + glassH / 2.0 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
double activeW = ACTIVE_WIDTH.convertToPixels();
double activeH = ACTIVE_LENGTH.convertToPixels();
g2d.setColor(ACTIVE_AREA_COLOR);
g2d.fill(new RoundRectangle2D.Double(glassX + (glassW - activeW) / 2.0,
glassY + (glassH - activeH) / 2.0, activeW, activeH, 2, 2));
}
g2d.setTransform(oldTx);
@@ -51,23 +51,70 @@ import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "7-Segment Display", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "7-Segment LED Display (1-Digit, 4-Digit, or TM1637 I2C Driver Module)",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "7-Segment Display", category = "Displays & Outputs",
author = "Branislav Stojkovic", description = "7-Segment LED Display (1-Digit, 4-Digit, or TM1637 I2C Driver Module)",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class SevenSegmentDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
/**
* The packages this component can draw, each carrying its own dimensions in millimetres.
*
* <p>Provenance 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);
}
@@ -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 <http://www.gnu.org/licenses/>.
*
*
*/
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);
}
}
@@ -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);
@@ -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);
}
}
@@ -262,6 +262,11 @@ public class ArduinoNano extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
private String[] getPinNames() {
switch (getVersion()) {
case EVERY:
@@ -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.
@@ -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;
@@ -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;
@@ -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.
@@ -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;
@@ -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;
@@ -19,7 +19,7 @@
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
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;
*
* <p>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();
@@ -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,
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+513
View File
@@ -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.