Controllers (#1080)

* initial

* more components

* fixes

* Update TreePanel.java

* fix boost converter

* Improve wrapping of component names

* Update ULN2003Driver.java

* Update NRF24L01Transceiver.java

* imp

* updates

* improvements

* more components

* arduino

* uno

* icon

* cleanup

* mega

* mega

* improveent

* nano

* esp boards

* esp

* esp32

* esp32

* Update ESP8266NodeMCU.java

* d1

* Update WemosD1Mini.java

* pi

* consolidate esp32

* pi pico updates

* pi zero

* Update RaspberryPiZero.java

* pi pico

* cleanup

* pi improvements

* consolidate pins

* antenna consolidation

* improve solder pads

* teensy

* headers

* teensy fix, test cleanup

* s3

* Update ESP32DevKit.java

* usb standardization

* pico w

* button standardization

* arduino improvements

* Update AbstractMakerBoard.java

* Update RaspberryPiZero.java

* Update RaspberryPi.java

* pi 3

* pi 5 improvements

* Update RaspberryPiZero.java

* fix tests

* esp pinout fix

* Update ESP32DevKit.java

* pin name improvements

* Create tier-1-modern-boards.md

* new versions

* pinouts

* nano improvements

* sanitize node names

* label improvements

* minima fixes

* arduino uno labels

* nano labels

* Update ArduinoNano.java

* mega

* mega

* comment out future stuff

* release prep

* Update RaspberryPiPico.java

* Refactor abstract class

* labeling improvement

* label offset fixes

* Update Teensy.java

* consolidate tests

* micro variants

* Update RaspberryPi.java

* displays tweaks

* display improvements

* neopix

* displays

* improvements

* led matrix

* strip

* led bar graph

* jewel

* tft

* tft

* Update maker-peripherals.md

* nokia

* led bar

* labels

* tft

* cleanup

* oled

* cleanup

* display enhancement

* displays

* neopixel

* displays

* displays

* new tft

* displays

* icon

* variants

* 2-digit 0.56 inch 7 segment

* Added ability to hide component categories in the component tree and toolbar from the Config menu

* led bar

* Update variants.xml

* color selection for bar graph

* Added 1.8" sd

* tft fixes

* Update WS2812BRing.java

* neopixel improvements

* netlist and node name improvements

* oled tweak

* cleanup

* mockito fix

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Bane Stojkovic
2026-10-09 09:38:09 +02:00
committed by GitHub
parent f62b3ddf04
commit a743de4024
87 changed files with 11343 additions and 1933 deletions
@@ -0,0 +1,74 @@
/*
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;
import java.awt.Color;
import org.diylc.core.annotations.EditableProperty;
/**
* Base for the boards built out of addressable 5050 LEDs -- the rings, the jewel, the stick, the
* strip, the matrix and the bare breakout.
*
* <p>Every one of them is sold fitted with any of the packages {@link LedType} lists, on the same
* PCB with the same footprint, so the package is a property of the board rather than a board of
* its own. What it changes is what the LEDs can emit, which is the palette here, and what the BOM
* has to ask for, which each board folds into its own variant label.
*
* @author Branislav Stojkovic
*/
public abstract class AbstractAddressableLedBoard extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
protected LedType ledType = LedType.RGB;
// Rebuilt only when the count or the package changes, because draw() runs on every repaint.
private transient Color[] ledColors;
@EditableProperty(name = "LED Type")
public LedType getLedType() {
if (ledType == null) {
ledType = LedType.RGB;
}
return ledType;
}
public void setLedType(LedType ledType) {
this.ledType = ledType;
this.ledColors = null;
invalidateCache();
}
/**
* The colours {@code count} LEDs are drawn lit with. These parts are drawn lit because unlit
* they are a dark board with white specks on it, which reads as no particular component; running
* the palette around the part also says at a glance which package it is fitted with, a WWA one
* having no colour to show.
*/
protected Color[] getLedColors(int count) {
if (ledColors == null || ledColors.length != count) {
ledColors = buildLedGradient(count, getLedType().getGradient());
}
return ledColors;
}
}
@@ -24,6 +24,7 @@ package org.diylc.components;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
@@ -90,12 +91,28 @@ 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");
// The colour wheel addressable RGB parts are drawn lit with. Treated as a loop, so a gradient
// spread over any LED count closes without a seam.
public static Color[] RGB_LED_GRADIENT = new Color[] {
Color.decode("#FFEE00"), Color.decode("#FF8800"), Color.decode("#FF1122"),
Color.decode("#AA22EE"), Color.decode("#2255FF"), Color.decode("#22DD44")};
// A WWA package carries no colour die, so a part fitted with one cannot show the wheel above.
// Its three anchors are the three dies themselves: warm white, white and amber.
public static Color[] WWA_LED_GRADIENT = new Color[] {
Color.decode("#FFE0BD"), Color.decode("#FFFFFF"), Color.decode("#FFAA00")};
public static Color SILK_COLOR = Color.WHITE;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
public static Size PAD_SIZE = new Size(0.065d, SizeUnit.in);
public static Size HOLE_SIZE = new Size(0.7d, SizeUnit.mm);
// the 5050 package shared by every addressable RGB part on a maker board
public static Size RGB_LED_SIZE = new Size(5.0d, SizeUnit.mm);
public static Size NOTCH_SIZE = new Size(0.9d, SizeUnit.mm);
public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm);
public static Size ANTENNA_LENGTH = new Size(7.0d, SizeUnit.mm);
@@ -149,6 +166,14 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
// labels along a row run across the board rather than down a column of pins, so they can afford
// a size the tightly stacked column labels cannot
public static Font PIN_ROW_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 9);
// a label lying along its row has only the pin pitch to fit into, so it runs a size down from
// the rotated ones and sits closer to the pin, which is all the room a shallow strip has
public static Font PIN_ROW_FLAT_FONT = new Font(SILK_FONT_FAMILY, Font.PLAIN, 8);
public static Size PIN_ROW_FLAT_LABEL_OFFSET = new Size(2.04d, SizeUnit.mm);
// Clearance kept between text printed on a lit panel and the panel's edge. A readout is drawn
// inside the glass rather than on board material, so unlike a silk label it has no neighbour to
// collide with; the margin only stops the longest line from touching the frame.
public static Size SCREEN_TEXT_MARGIN = new Size(0.6d, SizeUnit.mm);
protected Orientation orientation = Orientation.DEFAULT;
protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
@@ -223,13 +248,31 @@ 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() {}
/**
* Helper to rotate an array of relative point offsets around the origin (first point) by orientation.
*/
/** Rotates relative point offsets around the first point by the board's orientation. */
protected void rotatePoints(Point2D firstPoint, double[][] relativeOffsets) {
controlPoints = new Point2D[relativeOffsets.length];
controlPoints[0] = firstPoint;
@@ -242,9 +285,7 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
}
/**
* Helper to draw standard pin header pins at given control point indices.
*/
/** Draws standard pin header pins at the given control point indices. */
protected void drawPinHeader(Graphics2D g2d, int startIndex, int count, boolean outlineMode,
IDrawingObserver drawingObserver) {
if (outlineMode) return;
@@ -268,12 +309,12 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to format a pin name for silkscreen display (e.g. "3V3_1" -> "3V3", "A0 (ADC0)" -> "A0").
* Formats a pin name for silkscreen display, e.g. "3V3_1" to "3V3" and "A0 (ADC0)" to "A0".
*
* <p>Stripping the parenthesized annotation is the same operation the netlist performs, so it is
* delegated to {@link Node#sanitizeNodeName(String)} rather than duplicated. The silkscreen goes
* one step further and also drops the trailing disambiguator, because a board prints "GND" on
* every ground pin even though the netlist has to tell "GND_1" from "GND_2".
* delegated to {@link Node#sanitizeNodeName(String)}. The silkscreen also drops the trailing
* disambiguator, because a board prints "GND" on every ground pin even though the netlist has to
* tell "GND_1" from "GND_2".
*/
public static String getDisplayPinLabel(String name) {
String sanitized = Node.sanitizeNodeName(name);
@@ -286,17 +327,15 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
/**
* Silkscreen text printed next to a pin. Defaults to the node name with its annotation and
* disambiguator stripped, which is right whenever the node name is the silkscreen name plus
* extra detail. A board whose silkscreen genuinely differs from its node names -- the ESP
* DevKits print bare GPIO numbers while their nodes carry the full function list -- overrides
* this to return its own silkscreen label.
* disambiguator stripped. A board whose silkscreen genuinely differs -- the ESP DevKits print
* bare GPIO numbers while their nodes carry the full function list -- overrides this.
*/
protected String getSilkPinLabel(int index) {
return getDisplayPinLabel(getControlPointNodeName(index));
}
/**
* Helper to draw rotated control point / pin names next to the pins for dual-row DIP/header boards.
* Draws rotated pin names beside the pins of a dual-row DIP or header board.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated top-left pin X coordinate (P0.getX())
@@ -351,9 +390,8 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
/**
* Variant for boards too crowded for labels that stand square to their row. Each label is turned
* 45 degrees and tucked into the corner of its pad on the side the row reads towards - the top
* right corner for the left row and the top left corner for the right one - which is the trick
* the Teensy 4.1 uses to fit its pad names between two rows a tenth of an inch apart.
* 45 degrees and tucked into the corner of its pad on the side the row reads towards, which is
* how the Teensy 4.1 fits its pad names between two rows a tenth of an inch apart.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated top-left pin X coordinate (P0.getX())
@@ -394,9 +432,9 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw rotated pin names next to a horizontal row of pins, the way Arduino-style boards
* print their headers. Labels read top to bottom, are centered on the pin they belong to and
* start {@link #PIN_ROW_LABEL_OFFSET} away from it, growing away from the row.
* Draws rotated pin names beside a horizontal row, the way Arduino-style boards print theirs.
* Labels read top to bottom, centred on their pin and starting {@link #PIN_ROW_LABEL_OFFSET}
* away from it.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated first control point X coordinate (P0.getX())
@@ -435,15 +473,63 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw a complete PCB terminal block (green body + inner compartment lines + screw lugs)
* that matches the visuals of the PCBTerminalBlock component.
* Baseline for labels that have to sit on one line. {@link StringUtils}'s CENTER alignment
* centres each string on its own glyph box, so a name with a descender rides higher than the
* names beside it. Measuring one reference glyph gives every label the same baseline.
*/
private static double getLabelBaseline(Graphics2D g2d, double centreY) {
Rectangle bounds = g2d.getFont()
.createGlyphVector(g2d.getFontRenderContext(), "X").getVisualBounds().getBounds();
return centreY - bounds.height / 2.0 - bounds.y;
}
/**
* Draws pin names lying flat along a horizontal row, for a board whose strip beside the header is
* shallower than the longest name is long. {@link #drawRowPinLabels} stands its labels on end and
* has the whole depth of the board to grow into; these have only the pin pitch, so they run in a
* smaller font.
*
* @param g2d Graphics2D context (already transformed for board orientation)
* @param x Unrotated first control point X coordinate (P0.getX())
* @param y Unrotated first control point Y coordinate (P0.getY())
* @param offsets Array of [x, y] relative offsets for all control points
* @param startIndex Index of the first pin of the row
* @param count Number of pins in the row
* @param below True to print the labels below the row, false to print them above it
* @param silkColor Silkscreen text color
*/
protected void drawFlatRowPinLabels(Graphics2D g2d, double x, double y, double[][] offsets,
int startIndex, int count, boolean below, Color silkColor) {
if (offsets == null || offsets.length == 0) {
return;
}
double labelOffset = PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels();
g2d.setColor(silkColor);
g2d.setFont(PIN_ROW_FLAT_FONT);
for (int i = startIndex; i < startIndex + count && i < offsets.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double pinX = x + offsets[i][0];
double pinY = y + offsets[i][1] + (below ? labelOffset : -labelOffset);
// TOP passes the y straight through as the baseline, which is what keeps the row level
StringUtils.drawCenteredText(g2d, label, pinX, getLabelBaseline(g2d, pinY),
HorizontalAlignment.CENTER, VerticalAlignment.TOP);
}
}
/**
* Draws a PCB terminal block matching the visuals of the PCBTerminalBlock component.
*
* @param g2d Graphics2D context
* @param startIndex First control point index
* @param count Number of positions in this terminal block
* @param isHorizontal true if terminal block runs horizontally (along X), false if vertically (along Y)
* @param wireEntryOffset Direction/offset from pins to the wire entry edge (positive or negative)
* @param blockDepth Total depth of the green body (e.g. 50.0px for 5.08mm blocks, or 35.0px for compact)
* @param isHorizontal true if the block runs horizontally (along X), false if vertically
* @param wireEntryOffset Direction/offset from pins to the wire entry edge
* @param blockDepth Total depth of the green body in pixels
* @param outlineMode Outline mode flag
* @param drawingObserver Observer
*/
@@ -486,7 +572,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
innerCompartment = new Rectangle2D.Double(minX - pitchPx / 2.0, by, pitchPx, bh);
}
// Draw green body and compartment
g2d.setColor(SCREW_TERMINAL_COLOR);
g2d.fill(outerBody);
g2d.setColor(SCREW_TERMINAL_BORDER);
@@ -494,13 +579,11 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.draw(innerCompartment);
g2d.draw(outerBody);
// Draw screw contacts
drawScrewTerminals(g2d, startIndex, count, pitchPx, outlineMode, drawingObserver);
}
/**
* Helper to draw screw terminal block contacts at given control points.
* Visuals match the PCBTerminalBlock component: light metal screw circles with diagonal slot.
* Draws screw terminal contacts matching the PCBTerminalBlock component.
*/
protected void drawScrewTerminals(Graphics2D g2d, int startIndex, int count, double pitchPx, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
@@ -511,11 +594,9 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f));
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
// Light metal screw head circle
g2d.setColor(SCREW_CIRCLE_COLOR);
g2d.fillOval((int) Math.round(p.getX() - circleDiameter / 2.0), (int) Math.round(p.getY() - circleDiameter / 2.0),
circleDiameter, circleDiameter);
// Diagonal screw slot line
g2d.setColor(SCREW_CIRCLE_COLOR.darker());
g2d.drawLine((int) (p.getX() + Math.cos(Math.PI / 4) * circleDiameter / 2.0),
(int) (p.getY() + Math.sin(Math.PI / 4) * circleDiameter / 2.0),
@@ -526,40 +607,38 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw rectangular tinned solder pads with drill holes at given control points.
* Spreads {@link #RGB_LED_GRADIENT} evenly over {@code count} LEDs, treating the anchors as a
* loop so the last LED runs back into the first. Called once per palette at class load.
*/
protected void drawSolderPads(Graphics2D g2d, int startIndex, int count, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
double padW = 22.0;
double padH = 16.0;
double holeD = 7.0;
protected static Color[] buildLedGradient(int count) {
return buildLedGradient(count, RGB_LED_GRADIENT);
}
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
RoundRectangle2D pad = new RoundRectangle2D.Double(p.getX() - padW / 2.0, p.getY() - padH / 2.0, padW, padH, 2, 2);
g2d.setColor(LIGHT_METAL_COLOR);
g2d.fill(pad);
g2d.setColor(LIGHT_METAL_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
// Central through-hole / drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD));
/** As above, over a palette of the caller's choosing. */
protected static Color[] buildLedGradient(int count, Color[] anchors) {
Color[] colors = new Color[count];
int anchorCount = anchors.length;
for (int i = 0; i < count; i++) {
double position = (double) i * anchorCount / count;
int anchor = (int) position;
double blend = position - anchor;
Color from = anchors[anchor];
Color to = anchors[(anchor + 1) % anchorCount];
colors[i] = new Color(
(int) Math.round(from.getRed() + (to.getRed() - from.getRed()) * blend),
(int) Math.round(from.getGreen() + (to.getGreen() - from.getGreen()) * blend),
(int) Math.round(from.getBlue() + (to.getBlue() - from.getBlue()) * blend));
}
drawingObserver.stopTrackingContinuityArea();
return colors;
}
/**
* Helper to draw standard PCB through-hole solder pads (gold/copper pads with drill holes, square for Pin 1).
* Draws through-hole solder pads with drill holes.
*
* @param g2d Graphics2D context
* @param startIndex First control point index
* @param count Number of pads to draw
* @param squarePin1 If true, the first pad (index == startIndex) is drawn as a square pad
* @param squarePin1 If true, the first pad is drawn square
* @param outlineMode Outline mode flag
* @param drawingObserver Observer
*/
@@ -572,14 +651,12 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (squarePin1 && i == startIndex) {
// Pin 1 is a square solder pad
g2d.setColor(PAD_COLOR);
g2d.fill(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
} else {
// Round solder pads
g2d.setColor(PAD_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
g2d.setColor(PAD_COLOR.darker());
@@ -587,7 +664,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.draw(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter));
}
// Central drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(p.getX() - holeDiameter / 2.0, p.getY() - holeDiameter / 2.0, holeDiameter, holeDiameter));
g2d.setColor(PAD_COLOR.darker());
@@ -596,6 +672,95 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
drawingObserver.stopTrackingContinuityArea();
}
/**
* Draws rectangular surface pads: bare copper with no drill through it, which is what a part
* meant to be soldered to directly carries rather than the plated holes
* {@link #drawPcbSolderPads} draws. Unlike a round pad a rectangular one has an orientation, so
* each is turned with the board.
*
* @param padWidth Pad size across the board, which for an edge pad is its reach inward
* @param padLength Pad size along the row of pads
*/
protected void drawSurfacePads(Graphics2D g2d, int startIndex, int count, Size padWidth,
Size padLength, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) {
return;
}
double w = padWidth.convertToPixels();
double h = padLength.convertToPixels();
double theta = orientation.toRadians();
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (theta != 0) {
g2d.rotate(theta, p.getX(), p.getY());
}
Rectangle2D pad = new Rectangle2D.Double(p.getX() - w / 2.0, p.getY() - h / 2.0, w, h);
g2d.setColor(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
// turned back rather than saving the transform, as the LED painter does
if (theta != 0) {
g2d.rotate(-theta, p.getX(), p.getY());
}
}
drawingObserver.stopTrackingContinuityArea();
}
/**
* Draws the pads a cut leaves on a part sold by the length. The uncut pad is a pill lying along
* the part with the cut through its middle, so each end keeps half of one: flat against the
* edge, rounded inward. Built as the pill and then cut rather than drawn as a half, so that it
* stays right at both extremes -- half of a pill no longer than it is wide is a semicircle, with
* no special case.
*
* <p>Both sizes are in pixels rather than {@link Size}, a part that caps its pad against the room
* a variant leaves having already done the arithmetic.
*
* @param padWidth Pad size across the part
* @param padLength Pad size along the part, before the cut -- half of this is drawn
* @param reachesRight Whether the surviving half reaches to the right of the cut
*/
protected void drawBisectedPads(Graphics2D g2d, int startIndex, int count, double padWidth,
double padLength, boolean reachesRight, boolean outlineMode,
IDrawingObserver drawingObserver) {
if (outlineMode) {
return;
}
double w = padWidth;
double depth = padLength / 2.0;
double theta = orientation.toRadians();
drawingObserver.startTrackingContinuityArea(true);
for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) {
Point2D p = controlPoints[i];
if (theta != 0) {
g2d.rotate(theta, p.getX(), p.getY());
}
Area pad = new Area(new RoundRectangle2D.Double(p.getX() - depth, p.getY() - w / 2.0,
depth * 2, w, w, w));
pad.intersect(new Area(new Rectangle2D.Double(
reachesRight ? p.getX() : p.getX() - depth, p.getY() - w / 2.0, depth, w)));
g2d.setColor(PAD_COLOR);
g2d.fill(pad);
g2d.setColor(PAD_COLOR.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(pad);
if (theta != 0) {
g2d.rotate(-theta, p.getX(), p.getY());
}
}
drawingObserver.stopTrackingContinuityArea();
}
/**
* Pad, drill-hole and edge-notch diameters used by {@link #drawCastellatedPads} and
* {@link #subtractCastellationNotches}. A board whose castellations differ from the package
@@ -618,12 +783,12 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to draw one column of castellated edge pads: a round through-hole pad joined to the
* board edge by a stub, with a semicircular notch bitten out of the edge itself. Pair it with
* Draws one column of castellated edge pads: a round through-hole pad joined to the board edge by
* a stub, with a semicircular notch bitten out of the edge. Pair it with
* {@link #subtractCastellationNotches} so the outline and the pads agree.
*
* <p>Unlike the control-point based pad helpers this one works in unrotated board coordinates,
* so it has to be called while the board rotation is still applied to {@code g2d}.
* <p>Unlike the control-point based pad helpers this works in unrotated board coordinates, so it
* has to be called while the board rotation is still applied to {@code g2d}.
*
* @param g2d Graphics2D context, already transformed for board orientation
* @param pinX Unrotated X coordinate of the pad column
@@ -662,7 +827,7 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(padArea);
// Inner through-hole drill hole (white circle matching Zero and perfboard)
// the drill hole
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD));
g2d.setColor(padColor.darker());
@@ -672,8 +837,8 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
/**
* Helper to bite the semicircular castellation notches for one column of pads out of a board
* outline, matching what {@link #drawCastellatedPads} draws along the same edge.
* Bites the semicircular castellation notches for one column of pads out of a board outline,
* matching what {@link #drawCastellatedPads} draws along the same edge.
*/
protected void subtractCastellationNotches(Area boardArea, double pinY, int count, double spacing,
double edgeX) {
@@ -685,9 +850,6 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
}
}
/**
* Helper to get outline border color.
*/
protected Color getFinalBorderColor(ComponentState componentState, boolean outlineMode) {
if (outlineMode) {
Theme theme = Constants.DEFAULT_THEME;
@@ -744,4 +906,33 @@ public abstract class AbstractMakerBoard extends AbstractTransparentComponent<Vo
int margin = 50;
return new Rectangle2D.Double(minX - margin, minY - margin, (maxX - minX) + 2 * margin, (maxY - minY) + 2 * margin);
}
/**
* Which addressable package a part is fitted with. The three are pin-compatible and share the
* 5050 footprint, so this changes only what the LEDs can emit and what the BOM asks for.
*
* <p>RGBW adds a fourth, white die to the RGB three and is sold by the tint of that die; WWA
* replaces the colour dies altogether with warm white, white and amber.
*/
public enum LedType {
RGB("RGB"),
RGBW_WARM("RGBW Warm White"),
RGBW_NATURAL("RGBW Natural White"),
RGBW_COOL("RGBW Cool White"),
WWA("WWA");
private final String label;
LedType(String label) {
this.label = label;
}
@Override public String toString() { return label; }
/** True for the packages that carry a white die beside the colour ones. */
public boolean hasWhiteDie() { return this != RGB && this != WWA; }
/** The wheel a part fitted with this package is drawn lit with. */
public Color[] getGradient() { return this == WWA ? WWA_LED_GRADIENT : RGB_LED_GRADIENT; }
}
}
@@ -22,13 +22,16 @@
package org.diylc.components;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.List;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.VerticalAlignment;
@@ -48,9 +51,6 @@ public class MakerBoardPainter {
private MakerBoardPainter() {}
/**
* Helper to draw a mounting hole.
*/
public static void drawMountingHole(Graphics2D g2d, double cx, double cy, double diameter) {
g2d.setColor(Constants.CANVAS_COLOR);
g2d.fill(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter));
@@ -59,9 +59,6 @@ public class MakerBoardPainter {
g2d.draw(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter));
}
/**
* Helper to draw an IC chip on the board.
*/
public static void drawChip(Graphics2D g2d, double x, double y, double w, double h, String label) {
g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 4, 4));
@@ -80,9 +77,7 @@ public class MakerBoardPainter {
}
}
/**
* Helper to draw an FPC / ribbon cable connector (e.g. MIPI CSI/DSI, PCIe FPC).
*/
/** An FPC / ribbon cable connector, such as a MIPI CSI/DSI or PCIe socket. */
public static void drawFpcConnector(Graphics2D g2d, double x, double y, double w, double h, boolean vertical, String label) {
g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 2, 2));
@@ -111,37 +106,23 @@ public class MakerBoardPainter {
}
}
/**
* Helper to draw a standard Micro-USB connector.
*/
public static void drawMicroUsb(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a standard USB Type-C connector.
*/
public static void drawUsbC(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a standard USB Type-A connector.
*/
public static void drawUsbA(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a standard USB Type-B connector.
*/
public static void drawUsbB(Graphics2D g2d, double x, double y, double w, double h, String label) {
drawMetalConnector(g2d, x, y, w, h, label);
}
/**
* Helper to draw a metal connector / shield (e.g. RF shield cans, SD card slots, HDMI).
*/
/** A metal shield or connector body: an RF can, an SD card slot, an HDMI socket. */
public static void drawMetalConnector(Graphics2D g2d, double x, double y, double w, double h, String label) {
g2d.setColor(AbstractMakerBoard.USB_METAL_COLOR);
g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 3, 3));
@@ -156,20 +137,18 @@ public class MakerBoardPainter {
}
/**
* Helper to draw a standard ESP-style PCB meander antenna with a dark (#1E1E1E) substrate rectangle underneath.
* An ESP-style PCB meander antenna over a dark substrate rectangle.
*
* @param g2d Graphics2D context
* @param x top-left X coordinate of the antenna substrate rectangle
* @param y top-left Y coordinate of the antenna substrate rectangle
* @param width width of the antenna substrate rectangle (e.g. 15.0mm, matching main chip width)
* @param height height of the antenna substrate rectangle (e.g. 7.0mm)
* @param width width of the antenna substrate rectangle
* @param height height of the antenna substrate rectangle
*/
public static void drawPcbAntenna(Graphics2D g2d, double x, double y, double width, double height) {
// Dark rectangle underneath (#1E1E1E)
g2d.setColor(AbstractMakerBoard.ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(x, y, width, height));
// Serpentine antenna trace (gold/copper)
g2d.setColor(AbstractMakerBoard.ANTENNA_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
Path2D.Double antPath = new Path2D.Double();
@@ -212,15 +191,13 @@ public class MakerBoardPainter {
g2d.draw(antPath);
}
/**
* Helper to draw a standard ESP-style PCB meander antenna using default 15.0mm x 7.0mm dimensions.
*/
/** As above, at the default 15.0 x 7.0 mm. */
public static void drawPcbAntenna(Graphics2D g2d, double x, double y) {
drawPcbAntenna(g2d, x, y, AbstractMakerBoard.ANTENNA_WIDTH.convertToPixels(), AbstractMakerBoard.ANTENNA_LENGTH.convertToPixels());
}
/**
* Helper to draw a standard SMD tactile push button (housing + circular actuator).
* An SMD tactile push button: housing plus circular actuator.
*
* @param g2d Graphics2D context
* @param x Top-left X coordinate of the button body
@@ -239,4 +216,149 @@ 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));
}
/**
* 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.
*
* @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) {
drawAddressableLed(g2d, cx, cy, size, null);
}
/**
* Draws one 5050 addressable RGB package. A {@code litColor} shows the LED emitting that colour;
* {@code null} draws it dark, which is how an unpowered part looks sitting on a layout.
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor) {
drawAddressableLed(g2d, cx, cy, size, litColor, 0);
}
/**
* As above, turned by {@code rotation} radians about its own centre, for parts laid out on a
* circle and mounted facing outwards. The package is square, so the turn only tells modulo a
* quarter turn -- still enough to distinguish a radial arrangement from an axis-aligned one.
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor, double rotation) {
drawAddressableLed(g2d, cx, cy, size, litColor, rotation, AbstractMakerBoard.LedType.RGB);
}
/**
* As above, for a package other than the plain RGB one. An RGBW is told from an RGB on sight
* only by the white die beside the colour dies, so that is what is drawn; WWA has the same
* three-die layout and is told apart by what it emits, which is the caller's palette.
*/
public static void drawAddressableLed(Graphics2D g2d, double cx, double cy, double size,
Color litColor, double rotation, AbstractMakerBoard.LedType ledType) {
if (rotation != 0) {
g2d.rotate(rotation, cx, cy);
}
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(litColor == null ? AbstractMakerBoard.RGB_LED_LENS_COLOR : litColor);
g2d.fill(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_LENS_BORDER : litColor.darker());
g2d.draw(new Ellipse2D.Double(cx - lensR, cy - lensR, lensR * 2, lensR * 2));
double dieSize = Math.max(2.0, lensR * 0.4);
// the colour dies sit centred on their own, and shoulder aside to make room for a white one
double dieOffset = ledType != null && ledType.hasWhiteDie() ? dieSize * 0.7 : 0;
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_CHIP_COLOR : litColor.brighter());
g2d.fill(new Rectangle2D.Double(cx - dieOffset - dieSize / 2.0, cy - dieSize / 2.0, dieSize,
dieSize));
if (dieOffset > 0) {
g2d.setColor(litColor == null ? AbstractMakerBoard.RGB_LED_CHIP_COLOR : Color.WHITE);
g2d.fill(new Rectangle2D.Double(cx + dieOffset - dieSize / 2.0, cy - dieSize / 2.0, dieSize,
dieSize));
}
// turned back rather than saving the transform, so nothing is allocated per LED
if (rotation != 0) {
g2d.rotate(-rotation, cx, cy);
}
}
/**
* Prints a display's own description on its lit area, wrapped and centred.
*
* <p>What it prints is what the component already answers for the BOM, so the glass cannot fall
* out of step with the part list. Nothing is drawn unless the whole block fits, so a panel too
* small for its description shows a bare screen rather than a crop -- the same rule the pin-name
* silkscreen follows.
*
* <p>The area is the part of the panel the text may occupy rather than the panel itself, so a
* round display hands over the square inscribed in its glass.
*/
public static void drawScreenText(Graphics2D g2d, Rectangle2D area, Color inkColor,
Display display, String name, String value) {
String text = screenText(display, name, value);
if (text == null) {
return;
}
g2d.setFont(AbstractMakerBoard.SILK_FONT);
FontMetrics metrics = g2d.getFontMetrics();
double margin = AbstractMakerBoard.SCREEN_TEXT_MARGIN.convertToPixels();
double maxWidth = area.getWidth() - 2 * margin;
double maxHeight = area.getHeight() - 2 * margin;
double lineHeight = metrics.getHeight();
List<String> lines = StringUtils.wrap(text, metrics, (int) maxWidth);
if (lines.isEmpty() || lines.size() * lineHeight > maxHeight) {
return;
}
// a single word wider than the area cannot be broken, so the width is checked after wrapping
// rather than trusted to it
for (String line : lines) {
if (metrics.stringWidth(line) > maxWidth) {
return;
}
}
g2d.setColor(inkColor);
double y = area.getCenterY() - (lines.size() - 1) * lineHeight / 2.0;
for (String line : lines) {
StringUtils.drawCenteredText(g2d, line, area.getCenterX(), y, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER);
y += lineHeight;
}
}
/**
* What each {@link Display} setting puts on the glass, or {@code null} for a screen that prints
* nothing. The variant string reads "size, interface", and breaking it at the comma gives one
* property per line, which keeps the longest line well inside the glass instead of spanning it.
*/
private static String screenText(Display display, String name, String value) {
boolean hasValue = value != null && !value.trim().isEmpty();
String variant = hasValue ? value.replace(", ", "\n") : null;
if (display == null) {
return variant;
}
switch (display) {
case NONE:
return null;
case NAME:
return name;
case BOTH:
return variant == null ? name : name + "\n" + variant;
default:
return variant;
}
}
}
@@ -32,6 +32,7 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
@@ -48,66 +49,54 @@ import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.CalcUtils;
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", 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;
public enum LCDSize {
_16x2("16x2 (80x36mm)", 16, 2, 80.0, 36.0),
_20x4("20x4 (98x60mm)", 20, 4, 98.0, 60.0);
private final String label;
private final int cols;
private final int rows;
private final double widthMm;
private final double heightMm;
LCDSize(String label, int cols, int rows, double widthMm, double heightMm) {
this.label = label;
this.cols = cols;
this.rows = rows;
this.widthMm = widthMm;
this.heightMm = heightMm;
}
@Override public String toString() { return label; }
public int getCols() { return cols; }
public int getRows() { return rows; }
public double getWidthMm() { return widthMm; }
public double getHeightMm() { return heightMm; }
}
public enum LCDInterface {
I2C_Backpack("I2C Backpack (4-Pin)"),
Parallel_16Pin("Parallel HD44780 (16-Pin)");
private final String label;
LCDInterface(String label) { this.label = label; }
@Override public String toString() { return label; }
}
public static Size HEADER_OFFSET = new Size(2.54d, SizeUnit.mm);
// The parallel row is not centred: its leftmost pin starts this far in from the left edge.
public static Size PARALLEL_HEADER_INSET = new Size(10.0d, SizeUnit.mm);
// The I2C backpack's four pins stand in a column against the left edge, centred on the height.
public static Size I2C_HEADER_INSET = new Size(2.5d, SizeUnit.mm);
// A 3 mm hole centred 2.5 mm in leaves only 1 mm of board outside it, which is what the part
// does.
public static Size MOUNTING_HOLE_INSET = new Size(2.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, 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");
// Both inks are real: the blue-backlit module shows light characters and the yellow-green one
// dark, so the ink follows the screen colour.
public static Color SCREEN_INK_LIGHT = Color.decode("#F5F5F5");
public static Color SCREEN_INK_DARK = Color.decode("#1B2631");
public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SDA", "SCL"};
public static final String[] PIN_NAMES_PARALLEL = new String[] {
"VSS (GND)", "VDD (+5V)", "V0 (Contrast)", "RS", "RW", "E",
"VSS (GND)", "VCC (+5V)", "VEE (Contrast)", "RS", "RW", "E",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7",
"A (Backlight +)", "K (Backlight -)"
};
// The bare designations the module prints. The supply and backlight pins carry their function
// in the node name instead, there being no room for it along a 0.1" pitch.
public static final String[] SILK_NAMES_PARALLEL = new String[] {
"VSS", "VCC", "VEE", "RS", "RW", "E",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "A", "K"
};
private LCDSize lcdSize = LCDSize._16x2;
private LCDInterface lcdInterface = LCDInterface.I2C_Backpack;
private Color screenColor = SCREEN_BG;
private Display screen = Display.VALUE;
public CharacterLCD() {
super();
@@ -137,6 +126,26 @@ public class CharacterLCD extends AbstractMakerBoard {
invalidateCache();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
@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;
@@ -158,31 +167,76 @@ public class CharacterLCD extends AbstractMakerBoard {
}
@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++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
protected String getSilkPinLabel(int index) {
if (lcdInterface == LCDInterface.Parallel_16Pin && index >= 0
&& index < SILK_NAMES_PARALLEL.length) {
return SILK_NAMES_PARALLEL[index];
}
return super.getSilkPinLabel(index);
}
rotatePoints(firstPoint, relativeOffsets);
private int getPinCount() {
return lcdInterface == LCDInterface.I2C_Backpack ? PIN_NAMES_I2C.length
: PIN_NAMES_PARALLEL.length;
}
/** The ink that contrasts with the backlight the user has chosen. */
private Color getScreenInk() {
return CalcUtils.calculateLuminance(screenColor) < 128d ? SCREEN_INK_LIGHT : SCREEN_INK_DARK;
}
private double getBoardWidth() {
return new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
}
private double getBoardLength() {
return new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels();
}
/**
* Neither header is centred: the parallel row starts a fixed distance in from the left, and the
* I2C column stands against that same edge.
*/
private double getBoardX(double x) {
Size inset =
lcdInterface == LCDInterface.I2C_Backpack ? I2C_HEADER_INSET : PARALLEL_HEADER_INSET;
return x - inset.convertToPixels();
}
/**
* The parallel row hangs below the top edge by a fixed clearance; the I2C column is centred on
* the board's height instead.
*/
private double getBoardY(double y) {
if (lcdInterface == LCDInterface.I2C_Backpack) {
double columnSpan = (getPinCount() - 1) * PIN_SPACING.convertToPixels();
return y - (getBoardLength() - columnSpan) / 2.0;
}
return y - HEADER_OFFSET.convertToPixels();
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
boolean vertical = lcdInterface == LCDInterface.I2C_Backpack;
double[][] relativeOffsets = new double[getPinCount()][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = vertical ? 0 : i * spacing;
relativeOffsets[i][1] = vertical ? i * spacing : 0;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@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 +255,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();
@@ -221,39 +274,45 @@ public class CharacterLCD 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 holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
// Metal Bezel around screen
double bezelMarginX = (lcdSize == LCDSize._16x2) ? 45 : 40;
double bezelW = boardW - 2 * bezelMarginX;
double bezelX = boardX + bezelMarginX;
double bezelY = boardY + 45;
double bezelH = boardH - 65;
// The bezel's size is the module's own, not a margin taken off the board, so it stays put
// when the board changes.
double bezelW = new Size(lcdSize.getBezelWidthMm(), SizeUnit.mm).convertToPixels();
double bezelH = new Size(lcdSize.getBezelHeightMm(), SizeUnit.mm).convertToPixels();
double bezelX = boardX + (boardW - bezelW) / 2.0;
double bezelY = boardY + (boardH - bezelH) / 2.0;
g2d.setColor(BEZEL_COLOR);
g2d.fill(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 6, 6));
// LCD Screen area
double screenMargin = 12;
double screenW = bezelW - 2 * screenMargin;
double screenH = bezelH - 2 * screenMargin;
double screenX = bezelX + screenMargin;
double screenY = bezelY + screenMargin;
double screenW = new Size(lcdSize.getDisplayWidthMm(), SizeUnit.mm).convertToPixels();
double screenH = new Size(lcdSize.getDisplayHeightMm(), SizeUnit.mm).convertToPixels();
double screenX = bezelX + (bezelW - screenW) / 2.0;
double screenY = bezelY + (bezelH - screenH) / 2.0;
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);
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(screenX, screenY, screenW, screenH), getScreenInk(), getScreen(),
getName(), getValueForDisplay());
// Only the parallel row has anywhere to print. The backpack's column sits 2.5 mm from the
// left edge with the bezel 1.4 mm further in, so a label beside it would lie across the metal
// frame; the backpack prints its names on its own PCB behind the module, not drawn here.
if (lcdInterface == LCDInterface.Parallel_16Pin) {
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, controlPoints.length, true,
SILK_COLOR);
}
}
g2d.setTransform(oldTx);
@@ -270,7 +329,6 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setColor(PCB_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3));
// Screen
g2d.setColor(SCREEN_BG);
g2d.fillRect(6, 10, width - 12, height - 20);
@@ -278,4 +336,48 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setFont(new Font("SansSerif", Font.BOLD, 6));
StringUtils.drawCenteredText(g2d, "LCD", width / 2, height / 2 + 1, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum LCDSize {
// Labels reach the BOM's value column, so they stay short; the dimensions are carried by the
// fields beside them. The bezel and lit area are measured off the module rather than derived as
// margins off the board, which made the window grow with the board.
_16x2("16x2", 80.0, 35.0, 72.2, 24.1, 64.5, 14.5),
_20x4("20x4", 98.0, 60.0, 96.8, 39.3, 77.0, 25.2);
private final String label;
private final double widthMm;
private final double heightMm;
private final double bezelWidthMm;
private final double bezelHeightMm;
private final double displayWidthMm;
private final double displayHeightMm;
LCDSize(String label, double widthMm, double heightMm, double bezelWidthMm,
double bezelHeightMm, double displayWidthMm, double displayHeightMm) {
this.label = label;
this.widthMm = widthMm;
this.heightMm = heightMm;
this.bezelWidthMm = bezelWidthMm;
this.bezelHeightMm = bezelHeightMm;
this.displayWidthMm = displayWidthMm;
this.displayHeightMm = displayHeightMm;
}
@Override public String toString() { return label; }
public double getWidthMm() { return widthMm; }
public double getHeightMm() { return heightMm; }
public double getBezelWidthMm() { return bezelWidthMm; }
public double getBezelHeightMm() { return bezelHeightMm; }
public double getDisplayWidthMm() { return displayWidthMm; }
public double getDisplayHeightMm() { return displayHeightMm; }
}
public enum LCDInterface {
I2C_Backpack("I2C Backpack"),
Parallel_16Pin("Parallel HD44780");
private final String label;
LCDInterface(String label) { this.label = label; }
@Override public String toString() { return label; }
}
}
@@ -0,0 +1,589 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
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.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractLabeledComponent;
import org.diylc.components.transform.LEDBarGraphTransformer;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.VisibilityPolicy;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
/**
* A segmented LED bar in a dual-in-line package, as used for VU and level meters.
*
* <p>Not an {@code AbstractMakerBoard} like the rest of this category: it is a DIP-outline part,
* so it follows {@code DIL_IC} on {@link AbstractLabeledComponent} and carries its own
* transformer.
*
* <p>It differs from a DIP IC in the way that governs the whole geometry: an IC's leads bend out
* of the sides of the plastic, so its body sits <em>between</em> the pin rows, while a bar
* graph's pins leave the underside within the footprint and the package spans the rows. The pins
* are nonetheless drawn on top, because a pin that cannot be seen cannot be lined up against a
* board.
*
* @author Branislav Stojkovic
*/
@ComponentDescriptor(name = "LED Bar Graph", author = "Branislav Stojkovic",
category = "Displays & Outputs", instanceNamePrefix = "BAR",
description = "Segmented LED bar in a DIP package, for VU and level meters",
zOrder = IDIYComponent.COMPONENT, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME,
keywordPolicy = KeywordPolicy.SHOW_VALUE, transformer = LEDBarGraphTransformer.class,
enableCache = true)
public class LEDBarGraph extends AbstractLabeledComponent<String> {
private static final long serialVersionUID = 1L;
public static Color BODY_COLOR = Color.decode("#2B2B2B");
public static Color BORDER_COLOR = Color.decode("#1A1A1A");
public static Color PIN_COLOR = METAL_COLOR;
public static Color PIN_BORDER_COLOR = PIN_COLOR.darker();
// A bar graph is a scale, not a lamp: green most of the way up, yellow for the last two and
// red for the topmost, as on a level or VU meter.
public static Color SEGMENT_GREEN = Color.decode("#43A047");
public static Color SEGMENT_YELLOW = Color.decode("#FDD835");
public static Color SEGMENT_RED = Color.decode("#E53935");
public static Color SEGMENT_BLUE = Color.decode("#1E88E5");
public static Color SEGMENT_ORANGE = Color.decode("#FB8C00");
public static Color SEGMENT_WHITE = Color.decode("#EEEEEE");
public static int EDGE_RADIUS = 4;
public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in);
public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in);
// The package being wider than the row spacing is the whole difference from a DIP IC: 10.3
// against 7.62 leaves about 1.34 mm of body outboard of each row, so no pin shows from above.
public static Size ROW_SPACING = new Size(7.62d, SizeUnit.mm);
public static Size PACKAGE_WIDTH = new Size(10.3d, SizeUnit.mm);
// Sizes of their own rather than the package less a margin, so a segment stays the size it is
// when the package or the pin pitch changes.
public static Size SEGMENT_WIDTH = new Size(4.8d, SizeUnit.mm);
public static Size SEGMENT_LENGTH = new Size(1.7d, SizeUnit.mm);
private String value = "";
private Orientation orientation = Orientation.DEFAULT;
private Segments segments = Segments._10;
private BarColor barColor = BarColor.GREEN_YELLOW_RED;
private Color bodyColor = BODY_COLOR;
private Color borderColor = BORDER_COLOR;
private Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)};
transient private Area body;
public LEDBarGraph() {
super();
updateControlPoints();
}
/**
* Nothing draws this -- unlike {@code DIL_IC}, which renders it as the chip's part number -- so
* it reaches only the BOM and keyword search.
*/
@EditableProperty
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@EditableProperty
public Orientation getOrientation() {
return orientation;
}
public void setOrientation(Orientation orientation) {
this.orientation = orientation;
updateControlPoints();
body = null;
}
@EditableProperty(name = "Segments")
public Segments getSegments() {
if (segments == null) {
segments = Segments._10;
}
return segments;
}
/** Changing this changes the pin count, so the control points are rebuilt with it. */
public void setSegments(Segments segments) {
this.segments = segments;
updateControlPoints();
body = null;
}
@EditableProperty(name = "Color")
public BarColor getBarColor() {
if (barColor == null) {
barColor = BarColor.GREEN_YELLOW_RED;
}
return barColor;
}
public void setBarColor(BarColor barColor) {
this.barColor = barColor;
}
/**
* The colour of one segment, counted from the top of the scale. Delegates to the selected
* {@link BarColor}.
*/
private Color getSegmentColor(int index) {
return getBarColor().getSegmentColor(index, getSegments().getCount());
}
@EditableProperty(name = "Body")
public Color getBodyColor() {
if (bodyColor == null) {
bodyColor = BODY_COLOR;
}
return bodyColor;
}
public void setBodyColor(Color bodyColor) {
this.bodyColor = bodyColor;
}
@EditableProperty(name = "Border")
public Color getBorderColor() {
if (borderColor == null) {
borderColor = BORDER_COLOR;
}
return borderColor;
}
public void setBorderColor(Color borderColor) {
this.borderColor = borderColor;
}
/**
* Puts the segment count and colour in the BOM's value column, which otherwise groups every
* variant into one row: {@code BomMaker} groups on type name and value, and the inherited value
* is the part number and usually blank. {@code AbstractMakerBoard.getVariantLabel} does this for
* the boards in this category; the bar graph is on a different base class, so it is written out
* here. A part number the user has typed is kept and appended.
*/
@Override
public String getValueForDisplay() {
String variant = getSegments().toString();
if (getBarColor() != BarColor.GREEN_YELLOW_RED) {
variant += ", " + getBarColor();
}
String partNumber = getValue();
return partNumber == null || partNumber.trim().isEmpty() ? variant
: variant + ", " + partNumber.trim();
}
@Override
public int getControlPointCount() {
return controlPoints.length;
}
@Override
public Point2D getControlPoint(int index) {
return controlPoints[index];
}
@Override
public boolean isControlPointSticky(int index) {
return true;
}
@Override
public VisibilityPolicy getControlPointVisibilityPolicy(int index) {
return VisibilityPolicy.NEVER;
}
/**
* Mutates the existing point rather than replacing it, as {@code DIL_IC} does, since callers may
* hold the instance.
*/
@Override
public void setControlPoint(Point2D point, int index) {
controlPoints[index].setLocation(point);
body = null;
}
@Override
public boolean canPointMoveFreely(int pointIndex) {
return false;
}
/** Pin 1 is first on the near row and numbering runs round the package, as on any DIP outline. */
@Override
public String getControlPointNodeName(int index) {
int pinCount = getSegments().getPinCount();
int pin = index + 1;
if (pin > pinCount / 2) {
return Integer.toString(pinCount * 3 / 2 - pin + 1);
}
return Integer.toString(pin);
}
private void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
int pinCount = getSegments().getPinCount();
Point2D[] points = new Point2D[pinCount];
points[0] = firstPoint;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
for (int i = 0; i < pinCount / 2; i++) {
double dx1;
double dy1;
double dx2;
double dy2;
switch (orientation) {
case DEFAULT:
dx1 = 0;
dy1 = i * pinSpacing;
dx2 = rowSpacing;
dy2 = i * pinSpacing;
break;
case _90:
dx1 = -i * pinSpacing;
dy1 = 0;
dx2 = -i * pinSpacing;
dy2 = rowSpacing;
break;
case _180:
dx1 = 0;
dy1 = -i * pinSpacing;
dx2 = -rowSpacing;
dy2 = -i * pinSpacing;
break;
case _270:
dx1 = i * pinSpacing;
dy1 = 0;
dx2 = i * pinSpacing;
dy2 = -rowSpacing;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
points[i] = new Point2D.Double(Math.round(firstPoint.getX() + dx1),
Math.round(firstPoint.getY() + dy1));
points[i + pinCount / 2] = new Point2D.Double(Math.round(firstPoint.getX() + dx2),
Math.round(firstPoint.getY() + dy2));
}
controlPoints = points;
}
/** How far the plastic reaches past each pin row. */
private static double getSideOverhang() {
return (PACKAGE_WIDTH.convertToPixels() - ROW_SPACING.convertToPixels()) / 2.0;
}
/**
* The package outline. Unlike a DIP IC this spans the pin rows rather than sitting between them,
* and runs half a pin pitch past the end pins at each end, so the pins fall entirely underneath.
*/
public Area getBody() {
if (body == null) {
double x = controlPoints[0].getX();
double y = controlPoints[0].getY();
double width;
double height;
double pinSpacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double packageWidth = PACKAGE_WIDTH.convertToPixels();
double overhang = getSideOverhang();
double runLength = getSegments().getCount() * pinSpacing;
switch (orientation) {
case DEFAULT:
width = packageWidth;
height = runLength;
x -= overhang;
y -= pinSpacing / 2;
break;
case _90:
width = runLength;
height = packageWidth;
x -= runLength - pinSpacing / 2;
y -= overhang;
break;
case _180:
width = packageWidth;
height = runLength;
x -= rowSpacing + overhang;
y -= runLength - pinSpacing / 2;
break;
case _270:
width = runLength;
height = packageWidth;
x -= pinSpacing / 2;
y -= rowSpacing + overhang;
break;
default:
throw new RuntimeException("Unexpected orientation: " + orientation);
}
body = new Area(new RoundRectangle2D.Double(x, y, width, height, EDGE_RADIUS, EDGE_RADIUS));
}
return body;
}
/**
* Each segment is centred on the pin pair that drives it, as on the part. Taking the centre from
* the two control points rather than dividing the body also makes it orientation-proof: the
* midpoint of a pin pair is the package centreline at that segment whichever way it is turned.
*/
private Rectangle2D[] getSegmentRects() {
int count = getSegments().getCount();
double across = SEGMENT_WIDTH.convertToPixels();
double along = SEGMENT_LENGTH.convertToPixels();
boolean vertical = orientation == Orientation.DEFAULT || orientation == Orientation._180;
Rectangle2D[] rects = new Rectangle2D[count];
for (int i = 0; i < count; i++) {
Point2D near = controlPoints[i];
Point2D far = controlPoints[i + count];
double cx = (near.getX() + far.getX()) / 2.0;
double cy = (near.getY() + far.getY()) / 2.0;
if (vertical) {
rects[i] = new Rectangle2D.Double(cx - across / 2.0, cy - along / 2.0, across, along);
} else {
rects[i] = new Rectangle2D.Double(cx - along / 2.0, cy - across / 2.0, along, across);
}
}
return rects;
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Area mainArea = getBody();
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f));
Composite oldComposite = applyAlpha(g2d, componentState);
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : getBodyColor());
drawingObserver.startTracking();
g2d.fill(mainArea);
drawingObserver.stopTracking();
g2d.setComposite(oldComposite);
Color finalBorderColor =
componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING
? SELECTION_COLOR
: getBorderColor();
g2d.setColor(finalBorderColor);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(mainArea);
if (!outlineMode) {
// Drawn lit, as the Ring and the Strip are: an unlit bar graph is a black block with nothing
// to say what it is.
Rectangle2D[] segments = getSegmentRects();
for (int i = 0; i < segments.length; i++) {
g2d.setColor(getSegmentColor(i));
g2d.fill(segments[i]);
g2d.setColor(getSegmentColor(i).darker());
g2d.draw(segments[i]);
}
// Over the package and the segments: on the real part they are underneath and invisible from
// above, but a pin you cannot see is a pin you cannot line up. Round, because a bar graph's
// leads are drawn wire rather than an IC's stamped leadframe.
double pinSize = (int) PIN_SIZE.convertToPixels() / 2 * 2;
for (Point2D point : controlPoints) {
Ellipse2D pin = new Ellipse2D.Double(point.getX() - pinSize / 2,
point.getY() - pinSize / 2, pinSize, pinSize);
g2d.setColor(PIN_COLOR);
drawingObserver.startTracking();
g2d.fill(pin);
drawingObserver.stopTracking();
g2d.setColor(PIN_BORDER_COLOR);
g2d.draw(pin);
}
}
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(BODY_COLOR);
g2d.fillRoundRect(width / 4, 2, width / 2, height - 4, 2, 2);
g2d.setColor(BORDER_COLOR);
g2d.drawRoundRect(width / 4, 2, width / 2, height - 4, 2, 2);
// compressed to five bands so the icon reads at toolbox size
for (int i = 0; i < 5; i++) {
g2d.setColor(i == 0 ? SEGMENT_RED : i == 1 ? SEGMENT_YELLOW : SEGMENT_GREEN);
g2d.fillRect(width / 4 + 2, 4 + i * (height - 8) / 5, width / 2 - 3, (height - 8) / 5 - 1);
}
}
@Override
public Rectangle2D getCachingBounds() {
double minX = Integer.MAX_VALUE;
double maxX = Integer.MIN_VALUE;
double minY = Integer.MAX_VALUE;
double maxY = Integer.MIN_VALUE;
int margin = 50;
for (int i = 0; i < getControlPointCount(); i++) {
Point2D p = getControlPoint(i);
if (p.getX() < minX) {
minX = p.getX();
}
if (p.getX() > maxX) {
maxX = p.getX();
}
if (p.getY() < minY) {
minY = p.getY();
}
if (p.getY() > maxY) {
maxY = p.getY();
}
}
return new Rectangle2D.Double(minX - margin, minY - margin, maxX - minX + 2 * margin,
maxY - minY + 2 * margin);
}
/**
* Decides the rest of the package: each segment has its own anode and cathode, so the pin count
* is twice the segment count and the body lengthens by one pin pitch per segment.
*/
public enum Segments {
_4("4 Segment", 4),
_5("5 Segment", 5),
_6("6 Segment", 6),
_8("8 Segment", 8),
_10("10 Segment", 10),
_12("12 Segment", 12);
private final String label;
private final int count;
Segments(String label, int count) {
this.label = label;
this.count = count;
}
@Override public String toString() { return label; }
public int getCount() { return count; }
/** Anode and cathode per segment, split evenly between the two rows. */
public int getPinCount() { return count * 2; }
}
public enum BarColor {
GREEN_YELLOW_RED("Green-Yellow-Red"),
BLUE_GREEN_YELLOW_RED("Blue-Green-Yellow-Red"),
RED("Red"),
GREEN("Green"),
YELLOW("Yellow"),
BLUE("Blue"),
ORANGE("Orange"),
WHITE("White");
private final String label;
BarColor(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
public Color getSegmentColor(int index, int totalSegments) {
switch (this) {
case RED:
return SEGMENT_RED;
case GREEN:
return SEGMENT_GREEN;
case YELLOW:
return SEGMENT_YELLOW;
case BLUE:
return SEGMENT_BLUE;
case ORANGE:
return SEGMENT_ORANGE;
case WHITE:
return SEGMENT_WHITE;
case BLUE_GREEN_YELLOW_RED:
int redCount;
int yellowCount;
int blueCount;
if (totalSegments >= 12) {
redCount = 2;
yellowCount = 3;
blueCount = 2;
} else if (totalSegments >= 10) {
redCount = 2;
yellowCount = 3;
blueCount = 1;
} else if (totalSegments >= 8) {
redCount = 1;
yellowCount = 2;
blueCount = 1;
} else {
redCount = 1;
yellowCount = 1;
blueCount = 1;
}
if (index < redCount) {
return SEGMENT_RED;
}
if (index < redCount + yellowCount) {
return SEGMENT_YELLOW;
}
if (index >= totalSegments - blueCount) {
return SEGMENT_BLUE;
}
return SEGMENT_GREEN;
case GREEN_YELLOW_RED:
default:
if (index == 0) {
return SEGMENT_RED;
}
if (index <= 2) {
return SEGMENT_YELLOW;
}
return SEGMENT_GREEN;
}
}
}
}
@@ -0,0 +1,374 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
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;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "LED Matrix", 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 LEDMatrix extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_BLUE = Color.decode("#0055A5");
public static Color MATRIX_BODY = Color.decode("#1A1A1A");
public static Color LED_RED = Color.decode("#E53935");
public static Color LED_OFF = Color.decode("#333333");
// The modules are flush with each other and with the board, so the dot pitch runs unbroken
// across the whole strip.
public static Size MATRIX_SIZE = new Size(32.0d, SizeUnit.mm);
// Clearance from a board edge to the pin row nearest it. Both headers keep the same distance
// from their own edge, so the spacing between the rows follows from the board size.
public static Size HEADER_OFFSET = new Size(2.54d, 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 final String[] PIN_NAMES = new String[] {
"VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN",
"VCC_OUT", "GND_OUT", "DOUT", "CS_OUT", "CLK_OUT"
};
private Modules modules = Modules.Single_8x8;
private Color dotColor = LED_RED;
public LEDMatrix() {
super();
this.bodyColor = PCB_BLUE;
updateControlPoints();
}
@EditableProperty(name = "Modules")
public Modules getModules() {
if (modules == null) {
modules = Modules.Single_8x8;
}
return modules;
}
public void setModules(Modules modules) {
this.modules = modules;
updateControlPoints();
invalidateCache();
}
@Override
protected String getVariantLabel() {
Modules modules = getModules();
return modules == null ? null : modules.toString();
}
@EditableProperty(name = "Dot Color")
public Color getDotColor() {
return dotColor;
}
public void setDotColor(Color dotColor) {
this.dotColor = dotColor;
invalidateCache();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return "Pin " + (index + 1);
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
// The rows lie across the original single's height and stand on end on the boards that bring
// their headers out on the short edges.
boolean vertical = getModules().hasVerticalHeaders();
double far = vertical ? getHeaderSpacing() : -getHeaderSpacing();
for (int i = 0; i < 5; i++) {
relativeOffsets[i][0] = vertical ? 0 : i * spacing;
relativeOffsets[i][1] = vertical ? i * spacing : 0;
relativeOffsets[5 + i][0] = vertical ? far : i * spacing;
relativeOffsets[5 + i][1] = vertical ? i * spacing : far;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
private double getBoardWidth() {
return new Size(getModules().getBoardWidthMm(), SizeUnit.mm).convertToPixels();
}
private double getBoardLength() {
return new Size(getModules().getBoardHeightMm(), SizeUnit.mm).convertToPixels();
}
/** The span a five-pin row occupies. */
private static double getPinRunLength() {
return 4 * PIN_SPACING.convertToPixels();
}
/**
* The board along the axis the rows face each other across, less the clearance each keeps from
* its own edge.
*/
private double getHeaderSpacing() {
double span = getModules().hasVerticalHeaders() ? getBoardWidth() : getBoardLength();
return span - 2 * HEADER_OFFSET.convertToPixels();
}
/**
* The original single centres its horizontal row across the width; a board with vertical
* headers stands its row against the left edge, one clearance in.
*/
private double getBoardX(double x) {
if (getModules().hasVerticalHeaders()) {
return x - HEADER_OFFSET.convertToPixels();
}
return x - (getBoardWidth() - getPinRunLength()) / 2.0;
}
/**
* Control point 0 is an input pin: on the single it sits one clearance up from the bottom edge,
* with vertical headers it is the top pin of a column centred on the height.
*/
private double getBoardY(double y) {
if (getModules().hasVerticalHeaders()) {
return y - (getBoardLength() - getPinRunLength()) / 2.0;
}
return y - getBoardLength() + HEADER_OFFSET.convertToPixels();
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getBoardWidth(),
getBoardLength(), 8, 8);
}
/** The demo pattern, in the coordinates of a single 8x8 module. */
private static boolean isDotLit(int row, int col) {
return (row == 0 && (col == 2 || col == 5))
|| (row == 1 && (col == 1 || col == 3 || col == 4 || col == 6))
|| (row == 2 && col >= 1 && col <= 6)
|| (row == 3 && col >= 1 && col <= 6)
|| (row == 4 && col >= 2 && col <= 5)
|| (row == 5 && col >= 3 && col <= 4);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = getBoardWidth();
double boardH = getBoardLength();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
// Any header the modules cover is drawn first, so that they cover it. These are drawn and
// painted over rather than skipped, because drawPinHeader is what registers the pins as a
// conductive area and leaving a call out would quietly drop five pins from continuity. The
// header works in unrotated control-point coordinates, so the board rotation comes off for it.
boolean coversBothHeaders = getModules().hasVerticalHeaders();
g2d.setTransform(oldTx);
drawPinHeader(g2d, 5, 5, outlineMode, drawingObserver);
if (coversBothHeaders) {
drawPinHeader(g2d, 0, 5, outlineMode, drawingObserver);
}
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
if (!outlineMode) {
Modules modules = getModules();
double matrixSize = MATRIX_SIZE.convertToPixels();
double matrixX = boardX + (boardW - modules.getCount() * matrixSize) / 2.0;
double matrixY = boardY;
// the board's corners are rounded and the modules are not, so the outline clips them
Shape oldClip = g2d.getClip();
g2d.clip(boardShape);
g2d.setColor(MATRIX_BODY);
g2d.fill(new Rectangle2D.Double(matrixX, matrixY, modules.getCount() * matrixSize,
matrixSize));
// One unbroken grid rather than a tiled one, and centred across the full width: a cascade
// shows one image, not the same image once per module.
double dotPitch = matrixSize / 8.0;
double dotR = dotPitch * 0.7;
int cols = 8 * modules.getCount();
int patternOffset = (cols - 8) / 2;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < cols; col++) {
double dx = matrixX + col * dotPitch + (dotPitch - dotR) / 2.0;
double dy = matrixY + row * dotPitch + (dotPitch - dotR) / 2.0;
g2d.setColor(isDotLit(row, col - patternOffset) ? dotColor : LED_OFF);
g2d.fill(new Ellipse2D.Double(dx, dy, dotR, dotR));
}
}
g2d.setClip(oldClip);
// Only the tall single exposes its driver; hiding it is what makes the others tileable.
if (modules == Modules.Single_8x8) {
double chipMarginX = CHIP_MARGIN_X.convertToPixels();
MakerBoardPainter.drawChip(g2d, boardX + chipMarginX,
matrixY + matrixSize + CHIP_GAP.convertToPixels(), boardW - 2 * chipMarginX,
CHIP_LENGTH.convertToPixels(), "MAX7219");
}
// No IN/OUT silkscreen: with the modules flush there is no strip left for the OUT label, and
// labelling one of a matched pair reads worse than labelling neither.
//
// The single does have bare board for its pin names. They go above its input row rather than
// below, where a label would overrun the 2.54 mm to the board edge. Every other row has a
// module over it, so those pins are named by their tooltips alone.
if (modules == Modules.Single_8x8) {
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, 5, false, SILK_COLOR);
}
}
g2d.setTransform(oldTx);
// Where the modules cover both rows the input row went down with the output row before them.
if (!coversBothHeaders) {
drawPinHeader(g2d, 0, 5, outlineMode, drawingObserver);
}
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_BLUE);
g2d.fill(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
g2d.setColor(PCB_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
g2d.setColor(MATRIX_BODY);
g2d.fillRect(6, 4, width - 12, height - 12);
g2d.setColor(LED_RED);
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
g2d.fillOval(9 + c * 4, 7 + r * 4, 2, 2);
}
}
}
/**
* Which board the modules sit on, which is not the same question as how many there are: two of
* these carry one module each. The original single puts its driver and both headers on a strip
* below the module; the compact one fits the driver underneath, leaving a board that is only
* the module and can therefore be butted to others, which the 4-in-1 and 8-in-1 are.
*
* <p>The cascaded boards carry no dimensions of their own: a strip of flush 32 mm modules is
* 32 mm tall and as wide as it has modules.
*/
public enum Modules {
Single_8x8("Single 8x8 (32x50mm)", 1, 32.0d, 50.0d, false),
Compact_8x8("Compact 8x8 (32x32mm)", 1, 32.0d, 32.0d, true),
FourInOne_32x8("4-in-1 32x8 (128x32mm)", 4, 128.0d, 32.0d, true),
EightInOne_64x8("8-in-1 64x8 (256x32mm)", 8, 256.0d, 32.0d, true);
private final String label;
private final int count;
private final double boardWidthMm;
private final double boardHeightMm;
private final boolean verticalHeaders;
Modules(String label, int count, double boardWidthMm, double boardHeightMm,
boolean verticalHeaders) {
this.label = label;
this.count = count;
this.boardWidthMm = boardWidthMm;
this.boardHeightMm = boardHeightMm;
this.verticalHeaders = verticalHeaders;
}
@Override public String toString() { return label; }
public int getCount() { return count; }
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardHeightMm() { return boardHeightMm; }
/** True where the headers leave by the short edges, so their rows run down the board. */
public boolean hasVerticalHeaders() { return verticalHeaders; }
}
}
@@ -1,235 +0,0 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.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.Ellipse2D;
import java.awt.geom.Point2D;
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;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "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;
public static Color PCB_BLUE = Color.decode("#0055A5");
public static Color MATRIX_BODY = Color.decode("#1A1A1A");
public static Color LED_RED = Color.decode("#E53935");
public static Color LED_OFF = Color.decode("#333333");
public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
// Input Header (0..4)
"VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN",
// Output Header (5..9)
"VCC_OUT", "GND_OUT", "DOUT", "CS_OUT", "CLK_OUT"
};
private Color dotColor = LED_RED;
public LEDMatrix8x8() {
super();
this.bodyColor = PCB_BLUE;
updateControlPoints();
}
@EditableProperty(name = "Dot Color")
public Color getDotColor() {
return dotColor;
}
public void setDotColor(Color dotColor) {
this.dotColor = dotColor;
invalidateCache();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[10][2];
// Input header (0..4) at bottom
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;
}
rotatePoints(firstPoint, relativeOffsets);
}
@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);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
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;
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
// 8x8 LED Matrix block (square 32mm)
double matrixSize = boardW - 10;
double matrixX = boardX + 5;
double matrixY = boardY + 30;
g2d.setColor(MATRIX_BODY);
g2d.fill(new RoundRectangle2D.Double(matrixX, matrixY, matrixSize, matrixSize, 6, 6));
// Draw 64 circular LED dots
double dotPitch = matrixSize / 8.0;
double dotR = dotPitch * 0.7;
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
double dx = matrixX + col * dotPitch + (dotPitch - dotR) / 2.0;
double dy = matrixY + row * dotPitch + (dotPitch - dotR) / 2.0;
// create a demo heart or smile pattern
boolean on = (row == 0 && (col == 2 || col == 5)) ||
(row == 1 && (col == 1 || col == 3 || col == 4 || col == 6)) ||
(row == 2 && col >= 1 && col <= 6) ||
(row == 3 && col >= 1 && col <= 6) ||
(row == 4 && col >= 2 && col <= 5) ||
(row == 5 && col >= 3 && col <= 4);
g2d.setColor(on ? dotColor : LED_OFF);
g2d.fill(new Ellipse2D.Double(dx, dy, dotR, dotR));
}
}
// MAX7219 IC
MakerBoardPainter.drawChip(g2d, boardX + 20, matrixY + matrixSize + 15, boardW - 40, 45, "MAX7219");
// Silkscreen
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);
}
g2d.setTransform(oldTx);
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_BLUE);
g2d.fill(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
g2d.setColor(PCB_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3));
// Matrix block
g2d.setColor(MATRIX_BODY);
g2d.fillRect(6, 4, width - 12, height - 12);
// 4x4 sample dots
g2d.setColor(LED_RED);
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
g2d.fillOval(9 + c * 4, 7 + r * 4, 2, 2);
}
}
}
}
@@ -0,0 +1,291 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
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;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.CalcUtils;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "Nokia 5110 LCD", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "84x48 graphic LCD module with a PCD8544 controller and an SPI interface",
instanceNamePrefix = "LCD", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class Nokia5110LCD extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Color PCB_BLUE = Color.decode("#15548C");
public static Color BEZEL_COLOR = LIGHT_METAL_COLOR;
public static Color BEZEL_BORDER_COLOR = LIGHT_METAL_COLOR.darker();
public static Color LCD_COLOR = Color.decode("#6B7668");
public static Color LCD_INK_LIGHT = Color.decode("#F5F5F5");
public static Color LCD_INK_DARK = Color.decode("#2B2B2B");
public static Size BOARD_WIDTH = new Size(43.8d, SizeUnit.mm);
public static Size BOARD_LENGTH = new Size(45.8d, SizeUnit.mm);
// The metal frame holding the glass, centred across the board and measured down from the top
// edge; the pin row takes the strip left at the bottom.
public static Size BEZEL_WIDTH = new Size(40.0d, SizeUnit.mm);
public static Size BEZEL_LENGTH = new Size(35.0d, SizeUnit.mm);
public static Size BEZEL_TOP_OFFSET = new Size(4.5d, SizeUnit.mm);
// The glass sits lower in the frame than centred, so it is placed from the board's top edge.
public static Size DISPLAY_WIDTH = new Size(37.0d, SizeUnit.mm);
public static Size DISPLAY_LENGTH = new Size(27.0d, SizeUnit.mm);
public static Size DISPLAY_TOP_OFFSET = new Size(11.0d, SizeUnit.mm);
// Centre-to-centre, which is how the pattern is specified and what a builder drills.
public static Size MOUNTING_HOLE_SPACING_X = new Size(34.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING_Y = new Size(41.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
public static Size HEADER_OFFSET = new Size(3.5d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static Size SILK_TEXT_GAP = new Size(1.0d, SizeUnit.mm);
public static final String[] PIN_NAMES =
new String[] {"RST", "CE", "DC", "DIN", "CLK", "VCC", "BL", "GND"};
private Display screen = Display.VALUE;
private Color screenColor = LCD_COLOR;
public Nokia5110LCD() {
super();
this.bodyColor = PCB_BLUE;
updateControlPoints();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
/**
* The same module is sold with a blue, a white and a green backlight and nothing else about it
* changes, so it is a colour rather than a variant.
*/
@EditableProperty(name = "Backlight Color")
public Color getScreenColor() {
return screenColor == null ? LCD_COLOR : screenColor;
}
public void setScreenColor(Color screenColor) {
this.screenColor = screenColor;
invalidateCache();
}
/** The ink that contrasts with the backlight the user has chosen. */
private Color getScreenInk() {
return CalcUtils.calculateLuminance(getScreenColor()) < 128d ? LCD_INK_LIGHT : LCD_INK_DARK;
}
/** There is only one of these modules, so this is for the BOM's value column and the glass. */
@Override
protected String getVariantLabel() {
return "84x48 PCD8544";
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return "Pin " + (index + 1);
}
/** Left edge of the board, derived so that the pin row sits centred on the bottom edge. */
private double getBoardX(double x) {
double span = (PIN_NAMES.length - 1) * PIN_SPACING.convertToPixels();
return x - (BOARD_WIDTH.convertToPixels() - span) / 2.0;
}
/** A whole board above the pin row rather than below it. */
private double getBoardY(double y) {
return y - BOARD_LENGTH.convertToPixels() + HEADER_OFFSET.convertToPixels();
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_LENGTH.convertToPixels(), radius, radius);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_LENGTH.convertToPixels();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
double holeInsetX = (boardW - MOUNTING_HOLE_SPACING_X.convertToPixels()) / 2.0;
double holeInsetY = (boardH - MOUNTING_HOLE_SPACING_Y.convertToPixels()) / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInsetX, boardY + holeInsetY, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInsetX, boardY + holeInsetY,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInsetX, boardY + boardH - holeInsetY,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInsetX,
boardY + boardH - holeInsetY, holeSize);
// clear of the top left hole and level with it
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT_LARGE);
StringUtils.drawCenteredText(g2d, "Nokia 5110",
boardX + holeInsetX + holeSize / 2.0 + SILK_TEXT_GAP.convertToPixels(),
boardY + holeInsetY, HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
double bezelW = BEZEL_WIDTH.convertToPixels();
double bezelH = BEZEL_LENGTH.convertToPixels();
double bezelX = boardX + (boardW - bezelW) / 2.0;
double bezelY = boardY + BEZEL_TOP_OFFSET.convertToPixels();
g2d.setColor(BEZEL_COLOR);
g2d.fill(new Rectangle2D.Double(bezelX, bezelY, bezelW, bezelH));
g2d.setColor(BEZEL_BORDER_COLOR);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new Rectangle2D.Double(bezelX, bezelY, bezelW, bezelH));
double displayW = DISPLAY_WIDTH.convertToPixels();
double displayH = DISPLAY_LENGTH.convertToPixels();
double displayX = boardX + (boardW - displayW) / 2.0;
double displayY = boardY + DISPLAY_TOP_OFFSET.convertToPixels();
double displayRadius = CORNER_RADIUS.convertToPixels();
g2d.setColor(getScreenColor());
g2d.fill(new RoundRectangle2D.Double(displayX, displayY, displayW, displayH, displayRadius,
displayRadius));
g2d.setColor(getScreenColor().darker());
g2d.draw(new RoundRectangle2D.Double(displayX, displayY, displayW, displayH, displayRadius,
displayRadius));
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(displayX, displayY, displayW, displayH), getScreenInk(),
getScreen(), getName(), getValueForDisplay());
// Names lie flat along the row: the strip is 3.5 mm deep and a label stood on end needs
// 4.7 mm.
drawFlatRowPinLabels(g2d, x, y, getRelativeOffsets(), 0, PIN_NAMES.length, true,
SILK_COLOR);
}
g2d.setTransform(oldTx);
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_BLUE);
g2d.fill(new RoundRectangle2D.Double(2, 1, width - 4, height - 5, 3, 3));
g2d.setColor(PCB_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(2, 1, width - 4, height - 5, 3, 3));
g2d.setColor(BEZEL_COLOR);
g2d.fillRect(4, 3, width - 8, height - 10);
g2d.setColor(LCD_COLOR);
g2d.fill(new RoundRectangle2D.Double(5, 7, width - 10, height - 15, 2, 2));
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 4; i++) {
g2d.fillRect(width / 2 - 5 + i * 3, height - 4, 2, 2);
}
}
}
@@ -24,6 +24,7 @@ package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
@@ -32,6 +33,7 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
@@ -50,35 +52,51 @@ 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 = "OLED Display", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Monochrome OLED Display Module (SSD1306 / SH1106, I2C or 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;
public enum OLEDInterface {
I2C_4Pin("I2C (4-Pin: GND, VCC, SCL, SDA)"),
SPI_7Pin("SPI (7-Pin: GND, VCC, D0, D1, RES, DC, CS)");
private final String label;
OLEDInterface(String label) { this.label = label; }
@Override public String toString() { return label; }
}
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);
// Clearance a name keeps from the edge of its pad. Tighter than the offset
// drawFlatRowPinLabels uses, which does not fit the 2.27 mm strip on the 0.96".
public static Size LABEL_GAP = new Size(0.5d, 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"};
/**
* Clock and data keep their I2C names in both interfaces, since the same two pads carry them.
* SPI-labelled boards print SCK or CLK and MOSI, and the SSD1306 datasheet D0 and D1.
*/
public static final String[] PIN_NAMES_SPI =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS"};
/** Chip select tied low on the board instead of brought out, so the bus cannot be shared. */
public static final String[] PIN_NAMES_SPI_NO_CS =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC"};
/**
* VIN (4.8 - 5.2 V) and VDD (2.8 - 3.3 V logic) are separate rails and must not be tied
* together. The row follows the controller's pin numbering, which runs the reverse of the silk
* on the other boards.
*/
public static final String[] PIN_NAMES_SPI_SPLIT_SUPPLY =
new String[] {"SDA", "SCL", "DC", "RES", "CS", "VDD", "VIN", "GND"};
private Version version = Version.SSD1306_0_96;
private OLEDInterface oledInterface = OLEDInterface.I2C_4Pin;
private Display screen = Display.VALUE;
private Color pixelColor = PIXEL_BLUE;
public OLEDDisplay() {
super();
@@ -86,9 +104,20 @@ public class OLEDDisplay extends AbstractMakerBoard {
updateControlPoints();
}
@EditableProperty(name = "Size")
public Version getVersion() {
return version == null ? Version.SSD1306_0_96 : version;
}
public void setVersion(Version version) {
this.version = version;
updateControlPoints();
invalidateCache();
}
@EditableProperty(name = "Interface")
public OLEDInterface getOledInterface() {
return oledInterface;
return oledInterface == null ? OLEDInterface.I2C_4Pin : oledInterface;
}
public void setOledInterface(OLEDInterface oledInterface) {
@@ -97,42 +126,155 @@ public class OLEDDisplay extends AbstractMakerBoard {
invalidateCache();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
@EditableProperty(name = "Pixel Color")
public Color getPixelColor() {
return pixelColor == null ? PIXEL_BLUE : pixelColor;
}
public void setPixelColor(Color pixelColor) {
this.pixelColor = pixelColor;
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion() + ", " + getOledInterface();
}
/**
* The 0.91" is sold as two different boards, one with a column on its left edge and one with a
* row on top, so outline, header edge, holes and panel position follow from size and interface
* together rather than from the size alone.
*/
private Layout getLayout() {
return getVersion().getLayout(getOledInterface());
}
@Override
public String getControlPointNodeName(int index) {
if (oledInterface == OLEDInterface.I2C_4Pin) {
if (index >= 0 && index < PIN_NAMES_I2C.length) return PIN_NAMES_I2C[index];
} else {
if (index >= 0 && index < PIN_NAMES_SPI.length) return PIN_NAMES_SPI[index];
String[] pinNames = getOledInterface().getPinNames();
if (index >= 0 && index < pinNames.length) {
return pinNames[index];
}
return "Pin " + (index + 1);
}
private int getPinCount() {
return getOledInterface().getPinNames().length;
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/** A top-edge row is centred across the board; a left-edge column stands its clearance in. */
private double getBoardX(double x) {
Layout layout = getLayout();
if (layout.isHeaderOnLeftEdge()) {
return x - px(layout.getHeaderOffsetMm());
}
double span = (getPinCount() - 1) * PIN_SPACING.convertToPixels();
return x - (px(layout.getBoardWidthMm()) - span) / 2.0;
}
/** A top-edge row hangs below the edge by its clearance; a left-edge column is centred on the
* board's height. */
private double getBoardY(double y) {
Layout layout = getLayout();
if (layout.isHeaderOnLeftEdge()) {
double columnSpan = (getPinCount() - 1) * PIN_SPACING.convertToPixels();
return y - (px(layout.getBoardLengthMm()) - columnSpan) / 2.0;
}
return y - px(layout.getHeaderOffsetMm());
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
boolean vertical = getLayout().isHeaderOnLeftEdge();
double[][] relativeOffsets = new double[getPinCount()][2];
for (int i = 0; i < relativeOffsets.length; i++) {
relativeOffsets[i][0] = vertical ? 0 : i * spacing;
relativeOffsets[i][1] = vertical ? i * spacing : 0;
}
return relativeOffsets;
}
@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++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
rotatePoints(firstPoint, relativeOffsets);
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@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);
Layout layout = getLayout();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
px(layout.getBoardWidthMm()), px(layout.getBoardLengthMm()), 8, 8);
}
/**
* Pin names lying flat in the strip between the header and the glass. On the 0.91" I2C board the
* header is a column, so its names go to the right of the pins rather than below them.
*/
private void drawPinNames(Graphics2D g2d, double boardX, double boardY) {
Layout layout = getLayout();
double[][] offsets = getRelativeOffsets();
Point2D p0 = controlPoints[0];
double gap = LABEL_GAP.convertToPixels();
// drawPinHeader fills a square one pixel wider than PIN_SIZE either side of the pin, which is
// the edge a name has to clear rather than the pin itself.
double padHalf = Math.round(PIN_SIZE.convertToPixels()) + 1;
g2d.setFont(PIN_ROW_FLAT_FONT);
g2d.setColor(SILK_COLOR);
FontMetrics metrics = g2d.getFontMetrics();
// One reference glyph keeps a row of names on a single line. Every name here is upper case
// with no descender, so centring on the cap height lands them all alike.
double capHeight = g2d.getFont().createGlyphVector(g2d.getFontRenderContext(), "X")
.getVisualBounds().getHeight();
boolean column = layout.isHeaderOnLeftEdge();
double glassEdge = column ? boardX + px(layout.getPanelLeftMm())
: boardY + px(layout.getPanelTopMm());
for (int i = 0; i < offsets.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double pinX = p0.getX() + offsets[i][0];
double pinY = p0.getY() + offsets[i][1];
double room = glassEdge - (column ? pinX : pinY) - padHalf;
double extent = column ? metrics.stringWidth(label) : capHeight;
if (extent > room) {
continue;
}
// Centred in the strip where it is too shallow for the full gap, which the 0.96" is: 1.13 mm
// between pad and glass to print 0.73 mm of lettering in.
double offset = padHalf + Math.min(gap, (room - extent) / 2.0);
if (column) {
StringUtils.drawCenteredText(g2d, label, pinX + offset, pinY, HorizontalAlignment.LEFT,
VerticalAlignment.CENTER);
} else {
StringUtils.drawCenteredText(g2d, label, pinX, pinY + offset + capHeight / 2.0,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
}
@Override
@@ -151,10 +293,11 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
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;
Layout layout = getLayout();
double boardW = px(layout.getBoardWidthMm());
double boardH = px(layout.getBoardLengthMm());
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -170,31 +313,44 @@ 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);
if (layout.hasMountingHoles()) {
double holeInset = px(layout.getHoleInsetMm());
double holeSize = px(layout.getHoleSizeMm());
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
}
// Glass OLED Panel
double glassMarginX = 18;
double glassW = boardSizePx - 2 * glassMarginX;
double glassH = boardSizePx - 65;
double glassX = boardX + glassMarginX;
double glassY = boardY + 45;
Version version = getVersion();
double glassW = px(version.getPanelWidthMm());
double glassH = px(version.getPanelLengthMm());
double glassX = boardX + px(layout.getPanelLeftMm());
double glassY = boardY + px(layout.getPanelTopMm());
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);
// The driver's bonding region takes one end of the glass and its bottom, so the lit area is
// offset within the panel and sits high in it. Its top is measured from the board's edge.
double activeW = px(version.getActiveWidthMm());
double activeH = px(version.getActiveLengthMm());
double activeX = glassX + px(version.getActiveLeftInPanelMm());
double activeY = boardY + px(layout.getActiveTopMm());
g2d.setColor(ACTIVE_AREA_COLOR);
g2d.fill(new RoundRectangle2D.Double(activeX, activeY, activeW, activeH, 2, 2));
g2d.setColor(PIXEL_BLUE);
StringUtils.drawCenteredText(g2d, "OLED DISPLAY", glassX + glassW / 2.0, glassY + glassH / 2.0 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(activeX, activeY, activeW, activeH), getPixelColor(), getScreen(),
getName(), getValueForDisplay());
drawPinNames(g2d, boardX, boardY);
}
g2d.setTransform(oldTx);
@@ -211,7 +367,6 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.setColor(OLED_BLUE.darker());
g2d.draw(new RoundRectangle2D.Double(4, 4, width - 8, height - 8, 3, 3));
// Screen
g2d.setColor(GLASS_COLOR);
g2d.fillRect(7, 12, width - 14, height - 18);
@@ -219,10 +374,131 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.setFont(new Font("SansSerif", Font.BOLD, 5));
StringUtils.drawCenteredText(g2d, "OLED", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Pins
g2d.setColor(PIN_COLOR);
for (int i = 0; i < 4; i++) {
g2d.fillRect(10 + i * 3, 5, 2, 2);
}
}
public enum OLEDInterface {
// Labels reach the BOM's value column and are printed on the lit area, which the 0.91" has
// little of, so they stay short. Each SPI label states its pin count, the only thing that
// tells the three apart.
I2C_4Pin("I2C", PIN_NAMES_I2C),
SPI_6Pin("SPI-6", PIN_NAMES_SPI_NO_CS),
SPI_7Pin("SPI-7", PIN_NAMES_SPI),
SPI_8Pin("SPI-8", PIN_NAMES_SPI_SPLIT_SUPPLY);
private final String label;
private final String[] pinNames;
OLEDInterface(String label, String[] pinNames) {
this.label = label;
this.pinNames = pinNames;
}
@Override public String toString() { return label; }
public String[] getPinNames() { return pinNames; }
}
public enum Version {
// Panel and lit-area sizes belong to the display part and are shared by both of a size's
// boards. Everything belonging to a PCB is on the Layout, including where the lit area sits
// down the board, which the two 0.91" boards disagree on.
SSD1306_0_91("0.91\" SSD1306 128x32", 30.0d, 12.0d, 22.384d, 5.584d, 2.1d,
new Layout(35.8d, 12.0d, 1.5d, true, 0d, 0d, 5.0d, 0d, 2.35d),
new Layout(32.5d, 20.5d, 1.5d, false, 2.5d, 2.0d, 1.25d, 4.0d, 6.6d)),
SSD1306_0_96("0.96\" SSD1306 128x64", 26.7d, 19.26d, 21.74d, 11.0d, 2.48d,
new Layout(27.0d, 27.0d, 1.6d, false, 2.0d, 2.0d, 0.15d, 3.87d, 6.14d)),
SH1106_1_3("1.3\" SH1106 128x64", 35.4d, 23.0d, 29.42d, 14.7d, 2.99d,
new Layout(35.4d, 33.5d, 1.5d, false, 2.5d, 3.0d, 0d, 5.25d, 7.35d));
private final String label;
private final double panelWidthMm;
private final double panelLengthMm;
private final double activeWidthMm;
private final double activeLengthMm;
private final double activeLeftInPanelMm;
private final Layout i2cLayout;
private final Layout spiLayout;
/** For a size sold on one board, which both headers are built onto. */
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout layout) {
this(label, panelWidthMm, panelLengthMm, activeWidthMm, activeLengthMm, activeLeftInPanelMm,
layout, layout);
}
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout i2cLayout, Layout spiLayout) {
this.label = label;
this.panelWidthMm = panelWidthMm;
this.panelLengthMm = panelLengthMm;
this.activeWidthMm = activeWidthMm;
this.activeLengthMm = activeLengthMm;
this.activeLeftInPanelMm = activeLeftInPanelMm;
this.i2cLayout = i2cLayout;
this.spiLayout = spiLayout;
}
@Override public String toString() { return label; }
public double getPanelWidthMm() { return panelWidthMm; }
public double getPanelLengthMm() { return panelLengthMm; }
public double getActiveWidthMm() { return activeWidthMm; }
public double getActiveLengthMm() { return activeLengthMm; }
public double getActiveLeftInPanelMm() { return activeLeftInPanelMm; }
/** The 6-pin and 8-pin modules borrow the 7-pin board; the header is centred either way. */
public Layout getLayout(OLEDInterface oledInterface) {
return oledInterface == OLEDInterface.I2C_4Pin ? i2cLayout : spiLayout;
}
}
/**
* The PCB a size is sold on in one interface. Hole positions are an inset from each of the two
* edges nearest the hole.
*/
public static class Layout {
private final double boardWidthMm;
private final double boardLengthMm;
private final double headerOffsetMm;
private final boolean headerOnLeftEdge;
private final double holeInsetMm;
private final double holeSizeMm;
private final double panelLeftMm;
private final double panelTopMm;
private final double activeTopMm;
Layout(double boardWidthMm, double boardLengthMm, double headerOffsetMm,
boolean headerOnLeftEdge, double holeInsetMm, double holeSizeMm, double panelLeftMm,
double panelTopMm, double activeTopMm) {
this.boardWidthMm = boardWidthMm;
this.boardLengthMm = boardLengthMm;
this.headerOffsetMm = headerOffsetMm;
this.headerOnLeftEdge = headerOnLeftEdge;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
this.panelLeftMm = panelLeftMm;
this.panelTopMm = panelTopMm;
this.activeTopMm = activeTopMm;
}
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardLengthMm() { return boardLengthMm; }
public double getHoleInsetMm() { return holeInsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
public double getPanelLeftMm() { return panelLeftMm; }
public double getPanelTopMm() { return panelTopMm; }
/** Measured from the board's top edge rather than from the glass. */
public double getActiveTopMm() { return activeTopMm; }
/** Clearance from the edge the header sits on, whichever edge that is. */
public double getHeaderOffsetMm() { return headerOffsetMm; }
public boolean isHeaderOnLeftEdge() { return headerOnLeftEdge; }
public boolean hasMountingHoles() { return holeSizeMm > 0; }
}
}
@@ -51,25 +51,15 @@ 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, 2-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;
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)");
private final String label;
DisplayType(String label) { this.label = label; }
@Override public String toString() { return label; }
}
public static Color BODY_BLACK = Color.decode("#1C1C1C");
public static Color FACE_BLACK = Color.decode("#111111");
public static Color FACE_BORDER = Color.decode("#333333");
@@ -77,26 +67,50 @@ 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);
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 package's measured size. On the real part the
* digit window runs almost to the pin rows, so a faithful drawing has the pins sitting on the
* digit; this is a deliberate departure for legibility, which is why it is a separate factor
* rather than smaller digit sizes on {@link DisplayType}. It is shared, so changing it to chase
* an exact drawn size moves every package.
*/
public static double DIGIT_DRAW_SCALE = 0.88d;
/**
* Pin arrays are in DIP order: index 0 is pin 1 at the bottom left, counting right along the
* bottom row and then back right-to-left along the top, which is how {@code updateControlPoints}
* places the control points. The two commons are the same net on the part and are numbered only
* to keep the node names distinct.
*/
public static final String[] PIN_NAMES_1DIGIT = new String[] {
"e", "d", "COM1", "c", "DP", "b", "a", "COM2", "f", "g"
"E", "D", "COM1", "C", "DP", "B", "A", "COM2", "F", "G"
};
/** The two-digit member of the same family as the 5641AS, with two digit commons instead of
* four. */
public static final String[] PIN_NAMES_2DIGIT = new String[] {
"E", "D", "DP", "C", "G", "B", "D2", "D1", "F", "A"
};
/** Shared by both bare four-digit packages, which differ in size but not in pin function. */
public static final String[] PIN_NAMES_4DIGIT = new String[] {
"D1", "a", "f", "D2", "D3", "b", "e", "d", "DP", "c", "g", "D4"
"E", "D", "DP", "C", "G", "D4", "B", "D3", "D2", "F", "A", "D1"
};
public static final String[] PIN_NAMES_TM1637 = new String[] {"CLK", "DIO", "VCC", "GND"};
/** The blue of a bare PCB, matching the board under the OLED module. */
public static Color PCB_BLUE = Color.decode("#004488");
private DisplayType displayType = DisplayType.SingleDigit_10Pin;
private Common common = Common.Cathode;
private Punctuation punctuation = Punctuation.DecimalPoints;
private Color ledColor = LED_RED;
private Color boardColor = PCB_BLUE;
public SevenSegmentDisplay() {
super();
@@ -115,6 +129,54 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
invalidateCache();
}
/**
* No drawing path reads this: the two kinds share a pinout, a package and a set of dimensions,
* so it exists to split them in the BOM. Defaulted in the getter so that a project saved before
* the property existed still loads.
*/
@EditableProperty(name = "Common")
public Common getCommon() {
if (common == null) {
common = Common.Cathode;
}
return common;
}
public void setCommon(Common common) {
this.common = common;
}
/**
* Decides whether the decimal points and the colon are drawn. The colon is meaningless on the
* single-digit package and is ignored there.
*/
@EditableProperty(name = "Punctuation")
public Punctuation getPunctuation() {
if (punctuation == null) {
punctuation = Punctuation.DecimalPoints;
}
return punctuation;
}
public void setPunctuation(Punctuation punctuation) {
this.punctuation = punctuation;
invalidateCache();
}
@Override
protected String getVariantLabel() {
DisplayType displayType = getDisplayType();
if (displayType == null) {
return null;
}
// A module drives the digits itself and brings out no common pins, so polarity is not part of
// what you order -- but which punctuation it carries still is.
if (displayType.isModule()) {
return displayType + ", " + getPunctuation();
}
return displayType + ", " + getCommon() + ", " + getPunctuation();
}
@EditableProperty(name = "LED Color")
public Color getLedColor() {
return ledColor;
@@ -125,89 +187,113 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
invalidateCache();
}
/** Only the TM1637 has one; the bare packages are a moulding with pins in it. */
@EditableProperty(name = "Board Color")
public Color getBoardColor() {
return boardColor;
}
public void setBoardColor(Color boardColor) {
this.boardColor = boardColor;
invalidateCache();
}
/**
* The display device itself: the whole part on a bare package, and the module mounted on the
* board on a TM1637. Relabelled from the inherited "Board Color", which fits neither.
*/
@EditableProperty(name = "Body")
@Override
public Color getBodyColor() {
return super.getBodyColor();
}
private String[] getPinNames() {
return displayType.getPinNames();
}
@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);
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/**
* The colon sits in the gap between the two middle digits, which exists only on an even count.
*/
private boolean hasMiddlePair() {
int digitCount = displayType.getDigitCount();
return digitCount >= 2 && digitCount % 2 == 0;
}
/**
* Draws the package's digits on their own pitch, centred on {@code rowCentreX}. Bare packages
* and modules differ only in the figures they pass, which is what lets a two-, six- or
* eight-digit part be a constant rather than a branch.
*/
private void drawDigitRow(Graphics2D g2d, double rowCentreX, double dy, double dw, double dh,
double pitch, double nudge, boolean decimalPoints) {
int digitCount = displayType.getDigitCount();
double firstCentreX = rowCentreX - (digitCount - 1) * pitch / 2.0;
for (int d = 0; d < digitCount; d++) {
double dx = firstCentreX + d * pitch - dw / 2.0 - nudge;
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor, decimalPoints);
}
}
@Override
protected void updateControlPoints() {
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()) {
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;
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);
@@ -238,8 +324,10 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
Color outlineFill = displayType.isModule() ? boardColor : bodyColor;
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : outlineFill);
g2d.fill(boardShape);
drawingObserver.stopTracking();
@@ -248,89 +336,78 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
if (displayType == DisplayType.SingleDigit_10Pin) {
// Dark display face
g2d.setColor(FACE_BLACK);
if (!displayType.isModule()) {
// the recessed window is a shade of the moulding, so it follows the body colour
g2d.setColor(bodyColor.darker());
g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
g2d.setColor(FACE_BORDER);
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();
double dx = boardX + (boardW - dw) / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels();
// The pitch is unscaled while the digits are drawn slightly under size, so they keep their
// true centres and only the gaps widen. The row is centred on the body rather than measured
// from its left edge, so it stays put across packages of different widths. A single-digit
// package is the same construction with a count of one.
double dw = px(displayType.getDigitWidthMm()) * DIGIT_DRAW_SCALE;
double dh = px(displayType.getDigitHeightMm()) * DIGIT_DRAW_SCALE;
double pitch = px(displayType.getDigitPitchMm());
double dy = boardY + (boardH - dh) / 2.0;
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor);
double rowCentreX = boardX + boardW / 2.0;
} else if (displayType == DisplayType.FourDigit_12Pin) {
// Dark display face
g2d.setColor(FACE_BLACK);
g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
g2d.setColor(FACE_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3));
Punctuation punctuation = getPunctuation();
drawDigitRow(g2d, rowCentreX, dy, dw, dh, pitch, 0.5 * SEGMENT_THICKNESS.convertToPixels(),
punctuation.hasDecimalPoints());
// 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();
double dy = boardY + (boardH - dh) / 2.0;
for (int d = 0; d < 4; d++) {
double centerX = firstCenterX + d * pitch;
double dx = centerX - dw / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels();
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor);
if (punctuation.hasColon() && hasMiddlePair()) {
// Colon sized off the glyph grid like the decimal point, so it tracks the digit rather
// than staying a fixed few pixels across. The row is centred, so the gap between the two
// middle digits is the body's centre line.
double dotD = dw / GLYPH_WIDTH * 2.0;
double colonX = rowCentreX - dotD / 2.0;
g2d.setColor(ledColor);
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.35 - dotD / 2.0, dotD, dotD));
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.65 - dotD / 2.0, dotD, dotD));
}
// Center colon dots between digits 1 & 2
double colonX = boardX + boardW / 2.0;
double colonY1 = dy + dh * 0.35;
double colonY2 = dy + dh * 0.65;
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));
} else {
// TM1637 4-Digit Display Module
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);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 14, boardY + boardH - 14, 12);
Module module = displayType.getModule();
double holeInset = px(module.getHoleInsetMm());
double holeSize = px(module.getHoleSizeMm());
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + holeInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + holeInset, boardY + boardH - holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset, boardY + holeInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeInset,
boardY + boardH - holeInset, holeSize);
// 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 bezelW = px(module.getBezelWidthMm());
double bezelH = px(module.getBezelLengthMm());
double bezelX = boardX + (boardW - bezelW) / 2.0;
double bezelY = boardY + (boardH - bezelH) / 2.0;
g2d.setColor(FACE_BLACK);
g2d.setColor(bodyColor);
g2d.fill(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 4, 4));
g2d.setColor(FACE_BORDER);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 4, 4));
// 4 Digits inside 0.36" bezel
double dw = new Size(5.5d, SizeUnit.mm).convertToPixels();
double dh = new Size(9.2d, SizeUnit.mm).convertToPixels();
double pitch = new Size(7.62d, SizeUnit.mm).convertToPixels();
double firstCenterX = bezelX + (bezelW - 3 * pitch) / 2.0;
// at full size here: the module's digits sit on a bezel rather than between pin rows
double dw = px(displayType.getDigitWidthMm());
double dh = px(displayType.getDigitHeightMm());
double pitch = px(displayType.getDigitPitchMm());
double dy = bezelY + (bezelH - dh) / 2.0;
for (int d = 0; d < 4; d++) {
double centerX = firstCenterX + d * pitch;
double dx = centerX - dw / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels() * 0.65;
drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor);
Punctuation punctuation = getPunctuation();
drawDigitRow(g2d, bezelX + bezelW / 2.0, dy, dw, dh, pitch,
0.5 * SEGMENT_THICKNESS.convertToPixels() * 0.65, punctuation.hasDecimalPoints());
if (punctuation.hasColon() && hasMiddlePair()) {
double dotD = dw / GLYPH_WIDTH * 2.0;
double colonX = bezelX + bezelW / 2.0 - dotD / 2.0;
g2d.setColor(ledColor);
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.35 - dotD / 2.0, dotD, dotD));
g2d.fill(new Ellipse2D.Double(colonX, dy + dh * 0.65 - dotD / 2.0, dotD, dotD));
}
// Center colon dots
double colonX = bezelX + bezelW / 2.0;
double colonY1 = bezelY + bezelH / 2.0 - 8.0;
double colonY2 = bezelY + bezelH / 2.0 + 6.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));
}
}
@@ -375,126 +452,88 @@ 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, so
* the separation between them comes from outlining each segment in the background colour. Index
* order is A through G, matching the SEG_* bits.
*/
private void drawSevenSegmentDigit(Graphics2D g2d, double x, double y, double w, double h, String charToDisplay, Color onColor) {
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} from {@link #SEGMENT_POINTS}.
* Unlit segments are drawn first as a dim silhouette, then the lit ones, then every segment is
* outlined in the face colour so that neighbours sharing a vertex still read as separate.
*/
private void drawSevenSegmentDigit(Graphics2D g2d, double x, double y, double w, double h,
String charToDisplay, Color onColor, boolean showDecimalPoint) {
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)
g2d.setColor(LED_OFF);
for (int i = 0; i < 7; i++) {
if ((mask & (1 << i)) == 0) {
g2d.fill(segments[i]);
}
}
if ((mask & SEG_DP) == 0 && !charToDisplay.contains(".")) {
if (showDecimalPoint && (mask & SEG_DP) == 0 && !charToDisplay.contains(".")) {
g2d.fill(dpShape);
}
// 2. Draw lit active segments
g2d.setColor(onColor);
for (int i = 0; i < 7; i++) {
if ((mask & (1 << i)) != 0) {
g2d.fill(segments[i]);
}
}
if ((mask & SEG_DP) != 0 || charToDisplay.contains(".")) {
if (showDecimalPoint && ((mask & SEG_DP) != 0 || charToDisplay.contains("."))) {
g2d.fill(dpShape);
}
// Outlining every segment in the background colour is what stops neighbours that share a
// vertex from fusing into one shape.
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);
}
@@ -505,7 +544,157 @@ public class SevenSegmentDisplay extends AbstractMakerBoard {
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(6, 3, width - 12, height - 6, 3, 3));
// Draw single stylized '8'
drawSevenSegmentDigit(g2d, 9, 5, width - 18, height - 10, "8.", LED_RED);
// the icon stands for the component type, so it keeps its decimal point whatever
// Punctuation is set to
drawSevenSegmentDigit(g2d, 9, 5, width - 18, height - 10, "8.", LED_RED, true);
}
/**
* The packages this component can draw, each carrying its own dimensions in millimetres.
*
* <p>Digit heights are definitional -- 0.36 and 0.56 inches are what the parts are named for.
* The 0.56 inch body, digit width and pitch have never been checked against a datasheet, and
* the 0.36 inch equivalents are placeholders scaled from the digit height rather than
* measurements.
*/
public enum DisplayType {
// 5161AS, 5621AS, 3641AS and 5641AS. The two four-digit parts differ in size but share a
// pinout, which is why one array serves both.
SingleDigit_10Pin("1-Digit 0.56\"", 12.7d, 19.0d, 8.1d, 14.2d, 0d, 15.24d, true, 1,
PIN_NAMES_1DIGIT, null),
TwoDigit_0_56_10Pin("2-Digit 0.56\"", 25.0d, 19.0d, 8.1d, 14.2d, 12.7d, 15.24d, true, 2,
PIN_NAMES_2DIGIT, null),
FourDigit_0_36_12Pin("4-Digit 0.36\"", 30.0d, 14.0d, 5.2d, 9.14d, 7.5d, 10.16d, true, 4,
PIN_NAMES_4DIGIT, null),
FourDigit_0_56_12Pin("4-Digit 0.56\"", 50.3d, 19.0d, 8.1d, 14.2d, 12.7d, 15.24d, true, 4,
PIN_NAMES_4DIGIT, null),
TM1637_Module_4Pin("4-Digit TM1637 Module", 42.0d, 24.0d, 5.5d, 9.2d, 7.62d, 0d, false, 4,
PIN_NAMES_TM1637, new Module(30.0d, 14.0d, 1.778d, 1.524d));
private final String 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;
private final int digitCount;
private final String[] pinNames;
private final Module module;
DisplayType(String label, double bodyWidthMm, double bodyLengthMm, double digitWidthMm,
double digitHeightMm, double digitPitchMm, double rowSpacingMm, boolean dualRow,
int digitCount, String[] pinNames, Module module) {
this.label = label;
this.bodyWidthMm = bodyWidthMm;
this.bodyLengthMm = bodyLengthMm;
this.digitWidthMm = digitWidthMm;
this.digitHeightMm = digitHeightMm;
this.digitPitchMm = digitPitchMm;
this.rowSpacingMm = rowSpacingMm;
this.dualRow = dualRow;
this.digitCount = digitCount;
this.pinNames = pinNames;
this.module = module;
}
/**
* Distance between the two pin rows. It cannot be shared: a package only works if its rows sit
* within its body, and the 0.36 inch part is shorter than the 0.6 inch the larger ones 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; }
/** Carried rather than inferred, so a two-, six- or eight-digit part is a constant rather than
* another branch through {@code draw}. */
public int getDigitCount() { return digitCount; }
/** The part's pins in DIP order. */
public String[] getPinNames() { return pinNames; }
/**
* The board a driven module is built on, or {@code null} for a bare package. A module carries
* its own driver, so it brings out no common pins and its polarity is not something you order.
*/
public Module getModule() { return module; }
public boolean isModule() { return module != null; }
}
/**
* Which electrode the digits share. The two are identical in pinout, package and dimensions, so
* this changes nothing that is drawn and reaches only the BOM.
*/
public enum Common {
Cathode("Common Cathode"),
Anode("Common Anode");
private final String label;
Common(String label) { this.label = label; }
@Override public String toString() { return label; }
}
/**
* What the package carries besides the digits. On a twelve-pin part these are not independent:
* A-G plus DP and four digit commons already account for every pin, so a colon has to share the
* decimal point's line or replace it. The property describes the face of the part, not a pin.
*/
public enum Punctuation {
None("No Punctuation"),
DecimalPoints("Decimal Points"),
Colon("Colon"),
Both("Colon + Decimal Points");
private final String label;
Punctuation(String label) { this.label = label; }
@Override public String toString() { return label; }
public boolean hasDecimalPoints() { return this == DecimalPoints || this == Both; }
public boolean hasColon() { return this == Colon || this == Both; }
}
/**
* The circuit board a driven module is built on, with the display mounted on it as a bezel and
* mounting holes at the corners. A bare package has none of this.
*
* <p>The hole figures are the pixel offsets this class drew before they were figures at all,
* converted at the canvas scale rather than measured. The exact conversions are kept rather
* than two-decimal roundings because the roundings moved the holes by a fiftieth of a pixel,
* which antialiasing turned into a visible difference.
*
* @author Branislav Stojkovic
*/
public static class Module {
private final double bezelWidthMm;
private final double bezelLengthMm;
private final double holeInsetMm;
private final double holeSizeMm;
public Module(double bezelWidthMm, double bezelLengthMm, double holeInsetMm,
double holeSizeMm) {
this.bezelWidthMm = bezelWidthMm;
this.bezelLengthMm = bezelLengthMm;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
}
public double getBezelWidthMm() { return bezelWidthMm; }
public double getBezelLengthMm() { return bezelLengthMm; }
/** Centre of a corner hole, from each of the two edges nearest it. */
public double getHoleInsetMm() { return holeInsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
}
}
@@ -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;
@@ -27,11 +27,14 @@ 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.Display;
import org.diylc.common.HorizontalAlignment;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
@@ -44,30 +47,60 @@ import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// @ComponentDescriptor(name = "TFT Touch Screen (ILI9341)", category = "Displays & Outputs",
// author = "Branislav Stojkovic", description = "2.8\" 240x320 Color TFT LCD Display with SPI Interface, Touch, and SD Slot",
// instanceNamePrefix = "DISP", zOrder = IDIYComponent.BOARD,
// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
// enableCache = true)
@ComponentDescriptor(name = "TFT Display", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Color TFT LCD display module with an SPI interface",
instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class TFTDisplay extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
/**
* Silkscreen names for the 2.8". The default label would strip the touch prefix and print five
* pins all reading "T", and the names it keeps do not fit the 0.1" pitch. The nodes keep the
* full forms for the tooltips and the netlist.
*/
public static final String[] SILK_NAMES_ILI9341 = new String[] {"VCC", "GND", "CS", "RST", "DC",
"SDI", "SCK", "LED", "SDO", "TCK", "TCS", "TDI", "TDO", "IRQ"};
/** The 2.4" and the 2.8" are the same product with a different panel in it. */
public static final String[] PIN_NAMES_ILI9341 = new String[] {"VCC", "GND", "CS", "RESET", "DC",
"MOSI (SDI)", "SCK", "LED", "MISO (SDO)", "T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ"};
/** The micro-SD socket, brought out on its own row on the opposite edge from everything else. */
public static final String[] PIN_NAMES_SD =
new String[] {"SD_CS", "SD_MOSI", "SD_MISO", "SD_SCK"};
/**
* The SD row drops the prefix its node names carry, which the default label would reduce to four
* pins all reading "SD". Only the row's position marks these as the card socket's.
*/
public static final String[] SILK_NAMES_SD = new String[] {"CS", "MOSI", "MISO", "SCK"};
/**
* Four-wire SPI plus reset, data/command and the backlight, which is what a board of this size
* brings out whichever controller is behind it.
*/
public static final String[] PIN_NAMES_SPI_8PIN =
new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS", "BLK"};
public static Color TFT_RED = Color.decode("#C0392B");
public static Color SCREEN_BG = Color.decode("#111111");
public static Color GLASS_COLOR = Color.decode("#1A1A1A");
public static Color SCREEN_INK = Color.decode("#E0E0E0");
public static Size BOARD_WIDTH = new Size(86.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static final String[] PIN_NAMES = new String[] {
"VCC", "GND", "CS", "RESET", "DC", "MOSI (SDI)", "SCK", "LED", "MISO (SDO)",
"T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ"
};
private Controller controller = Controller.ILI9341_2_8;
private Display screen = Display.VALUE;
public TFTDisplay() {
super();
@@ -75,43 +108,149 @@ public class TFTDisplay extends AbstractMakerBoard {
updateControlPoints();
}
@EditableProperty(name = "Controller")
public Controller getController() {
return controller == null ? Controller.ILI9341_2_8 : controller;
}
public void setController(Controller controller) {
this.controller = controller;
updateControlPoints();
invalidateCache();
}
@EditableProperty
public Display getScreen() {
return screen == null ? Display.VALUE : screen;
}
public void setScreen(Display screen) {
this.screen = screen;
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getController().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
Controller controller = getController();
String[] pinNames = controller.getPinNames();
if (index >= 0 && index < pinNames.length) {
return pinNames[index];
}
String[] secondary = controller.getSecondaryPinNames();
if (secondary != null && index >= pinNames.length
&& index < pinNames.length + secondary.length) {
return secondary[index - pinNames.length];
}
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
protected String getSilkPinLabel(int index) {
Controller controller = getController();
if (controller.getPinNames() == PIN_NAMES_ILI9341 && index >= 0
&& index < SILK_NAMES_ILI9341.length) {
return SILK_NAMES_ILI9341[index];
}
int secondaryIndex = index - controller.getPinNames().length;
if (controller.getSecondaryPinNames() == PIN_NAMES_SD && secondaryIndex >= 0
&& secondaryIndex < SILK_NAMES_SD.length) {
return SILK_NAMES_SD[secondaryIndex];
}
return super.getSilkPinLabel(index);
}
double[][] relativeOffsets = new double[14][2];
for (int i = 0; i < 14; i++) {
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
/** Left edge of the board, derived so that the primary pin row sits centred across it. */
private double getBoardX(double x) {
Controller controller = getController();
double spacing = PIN_SPACING.convertToPixels();
double span = (controller.getPinNames().length - 1) * spacing;
return x - (px(controller.getBoardWidthMm()) - span) / 2.0;
}
/**
* A board headed at the bottom stands above control point zero; one headed at the top hangs
* below it.
*/
private double getBoardY(double y) {
Controller controller = getController();
if (controller.isHeaderAtTop()) {
return y - px(controller.getHeaderOffsetMm());
}
return y - px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm());
}
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
Controller controller = getController();
int primaryCount = controller.getPinNames().length;
String[] secondary = controller.getSecondaryPinNames();
int secondaryCount = secondary == null ? 0 : secondary.length;
double[][] relativeOffsets = new double[primaryCount + secondaryCount][2];
for (int i = 0; i < primaryCount; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
}
rotatePoints(firstPoint, relativeOffsets);
// The second row faces the first, so its distance from control point zero is what the two
// offsets leave of the length. Both rows are centred on the width.
if (secondaryCount > 0) {
double indent = (primaryCount - secondaryCount) * spacing / 2.0;
double across = px(controller.getBoardLengthMm() - controller.getHeaderOffsetMm()
- controller.getSecondaryHeaderOffsetMm());
if (!controller.isHeaderAtTop()) {
across = -across;
}
for (int i = 0; i < secondaryCount; i++) {
relativeOffsets[primaryCount + i][0] = indent + i * spacing;
relativeOffsets[primaryCount + i][1] = across;
}
}
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@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);
Controller controller = getController();
double boardX = getBoardX(p0.getX());
double boardY = getBoardY(p0.getY());
double boardW = px(controller.getBoardWidthMm());
double boardH = px(controller.getBoardLengthMm());
if (controller.isRound()) {
// The tab has to reach the disc's centre line before the two are unioned: stopped at the
// bottom of the disc it would meet it at a tangent and leave a notch either side.
double discR = boardW / 2.0;
double discCy = boardY + discR;
double tabW = px(controller.getTabWidthMm());
Area body = new Area(new Ellipse2D.Double(boardX, boardY, boardW, boardW));
body.add(new Area(new Rectangle2D.Double(boardX + (boardW - tabW) / 2.0, discCy, tabW,
boardY + boardH - discCy)));
return body;
}
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, radius, radius);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project,
IDrawingObserver drawingObserver) {
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
@@ -125,10 +264,11 @@ public class TFTDisplay extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - 70;
double boardY = y - 20;
Controller controller = getController();
double boardW = px(controller.getBoardWidthMm());
double boardH = px(controller.getBoardLengthMm());
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
@@ -144,38 +284,103 @@ 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);
// Centring the panel between the hole rows rather than on an edge is what keeps it clear of
// both pairs. A board without holes falls back to the space below the pin row.
double bandTop = boardY + px(controller.getHeaderOffsetMm());
double bandBottom = boardY + boardH;
// Color Screen Area
double screenMarginX = 35;
double screenW = boardW - 2 * screenMarginX - 25;
double screenH = boardH - 65;
double screenX = boardX + screenMarginX;
double screenY = boardY + 45;
if (controller.hasMountingHoles()) {
double holeInset = px(controller.getHoleInsetMm());
double sideInset = px(controller.getHoleSideInsetMm());
double headerSideInset = px(controller.getHeaderSideHoleInsetMm());
double holeSize = px(controller.getHoleSizeMm());
double topInset = controller.isHeaderAtTop() ? headerSideInset : holeInset;
double bottomInset = controller.isHeaderAtTop() ? holeInset : headerSideInset;
MakerBoardPainter.drawMountingHole(g2d, boardX + sideInset, boardY + topInset, holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - sideInset, boardY + topInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + sideInset, boardY + boardH - bottomInset,
holeSize);
MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - sideInset,
boardY + boardH - bottomInset, holeSize);
bandTop = boardY + topInset;
bandBottom = boardY + boardH - bottomInset;
}
double screenW = px(controller.getScreenWidthMm());
double screenH = px(controller.getScreenLengthMm());
Shape panel;
if (controller.isRound()) {
// the lit circle is centred on the disc, which sits above the tab rather than on the
// centre line of the bounding box
double discR = boardW / 2.0;
double discCx = boardX + discR;
double discCy = boardY + discR;
panel = new Ellipse2D.Double(discCx - screenW / 2.0, discCy - screenH / 2.0, screenW,
screenH);
} else {
double bandCentre = (bandTop + bandBottom) / 2.0;
double screenX;
double screenY;
if (controller.hasGlass()) {
Glass glass = controller.getGlass();
double glassH = px(glass.getLengthMm());
double glassW = glass.spansTheBoard() ? boardW : px(glass.getWidthMm());
double glassX = glass.isPlacedFromTheRightEdge()
? boardX + boardW - px(glass.getRightInsetMm()) - glassW
: boardX + (boardW - glassW) / 2.0;
double glassY = bandCentre - glassH / 2.0;
g2d.setColor(GLASS_COLOR);
g2d.fill(new Rectangle2D.Double(glassX, glassY, glassW, glassH));
// Centred in the glass, and vertically too unless the board states its own figure: the
// driver's bonding region takes the bottom of the panel, so centring would drop it.
screenX = glassX + (glassW - screenW) / 2.0;
screenY = controller.hasMeasuredScreenTop() ? boardY + px(controller.getScreenTopMm())
: glassY + (glassH - screenH) / 2.0;
} else {
screenX = boardX + (boardW - screenW) / 2.0;
screenY = controller.hasMeasuredScreenTop() ? boardY + px(controller.getScreenTopMm())
: bandCentre - screenH / 2.0;
}
panel = new Rectangle2D.Double(screenX, screenY, screenW, screenH);
}
g2d.setColor(SCREEN_BG);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
g2d.fill(panel);
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(panel);
// 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);
// Only the inscribed square is safely inside a round panel.
Rectangle2D textArea = panel.getBounds2D();
if (controller.isRound()) {
double side = textArea.getWidth() / Math.sqrt(2.0);
textArea = new Rectangle2D.Double(textArea.getCenterX() - side / 2.0,
textArea.getCenterY() - side / 2.0, side, side);
}
MakerBoardPainter.drawScreenText(g2d, textArea, SCREEN_INK, getScreen(), getName(),
getValueForDisplay());
// SD Card holder on right edge
MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - 28, boardY + 100, 36, 120, "SD");
// Names lie flat: the strip is 3.5 mm deep on the two smallest boards and a label stood on
// end needs 4.7 mm. Each row prints into the board rather than off its own edge.
double[][] offsets = getRelativeOffsets();
drawFlatRowPinLabels(g2d, x, y, offsets, 0, controller.getPinNames().length,
controller.isHeaderAtTop(), SILK_COLOR);
if (controller.hasSecondaryHeader()) {
drawFlatRowPinLabels(g2d, x, y, offsets, controller.getPinNames().length,
controller.getSecondaryPinNames().length, !controller.isHeaderAtTop(), SILK_COLOR);
}
}
g2d.setTransform(oldTx);
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPinHeader(g2d, 0, controller.getPinNames().length, outlineMode, drawingObserver);
if (controller.hasSecondaryHeader()) {
drawPinHeader(g2d, controller.getPinNames().length,
controller.getSecondaryPinNames().length, outlineMode, drawingObserver);
}
g2d.setComposite(oldComposite);
}
@@ -183,18 +388,220 @@ 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
// The glass reaches both edges of the board and is centred down it.
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(8, 6, width - 16, 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, HorizontalAlignment.CENTER,
VerticalAlignment.CENTER);
}
public enum Controller {
// Ordered by diagonal; a new variant goes in at its place in that run rather than at the end.
// A glass length of zero means the module has no dark panel wider than its own lit area, so
// only the screen is drawn, and a hole size of zero means no mounting holes at all. The round
// board carries the same two outline figures as the rest: width is the disc diameter and
// length runs from the tab's outer edge to the far side of the disc, so the tab's projection
// is the difference between them.
//
// A board headed at the top hangs below its row and one headed at the bottom grows upwards
// from it. Both occur here, so every constant states its own edge rather than taking a
// default.
// The one board whose bezel does not span it, being pushed into the right-hand side with the
// driver's circuitry filling the bare L that leaves.
ST7735_0_96("0.96\" ST7735 80x160", 30.0d, 24.0d, 21.7d, 10.8d,
new Glass(12.8d, 23.7d, 2.0d), 1.44d, 2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 0d, null, 0d,
2.5d, true),
GC9A01_1_28("1.28\" GC9A01 240x240 Round", 38.0d, 45.5d, 32.5d, 32.5d, null, 1.76d, 0d, 0d,
22.9d,
new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"}, false),
// Lit area 1.5 mm above the middle of the bezel; rederive it if the hole rows move.
ST7789_1_54("1.54\" ST7789 240x240", 32.0d, 43.72d, 27.66d, 27.66d, new Glass(33.7d), 1.5d,
2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 6.53d, null, 0d, 2.5d, true),
// Bezel narrower than the PCB but centred across it, unlike the 0.96"'s. Lit area 2 mm above
// the middle of the bezel; rederive it if the bezel moves.
ST7735_1_8("1.8\" ST7735 128x160", 35.0d, 56.0d, 28.0d, 35.0d, new Glass(45.83d, 33.0d), 1.41d,
2.5d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 8.5d, null, 0d, 2.5d, true),
// The plain 1.8"'s panel on a longer board with a micro-SD row, which turns the module round.
// The one board whose holes are not the same distance in from every edge, and whose 8-pin row
// runs alongside the holes on its edge rather than between them.
ST7735_1_8_SD("1.8\" ST7735 128x160 (SD)", 34.0d, 58.0d, 28.0d, 35.0d,
new Glass(45.83d, 33.0d), 2.5d, 3.0d, 2.0d, 0d, PIN_NAMES_SPI_8PIN, 9.5d, PIN_NAMES_SD,
2.5d, 3.0d, 2.75d, false),
// The pair of holes beside the 14-pin row is set deeper than the rest: at the shared 3 mm the
// row's 14 pins, spanning 33 mm of a 42.72 mm board, would sit on them.
ILI9341_2_4("2.4\" ILI9341 240x320 (Touch + SD)", 42.72d, 77.18d, 36.72d, 48.96d,
new Glass(60.26d), 2.0d, 3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 9.26d, PIN_NAMES_SD, 2.0d,
6.92d, false),
// As the 2.4", with the pair beside the pin row set deeper again. That carries the panel with
// it, the glass being centred between the hole rows, so the lit area's 9.70 mm has to be
// rederived if the holes move.
ILI9341_2_8("2.8\" ILI9341 240x320 (Touch + SD)", 50.0d, 86.0d, 43.2d, 57.6d, new Glass(69.2d),
2.0d, 3.0d, 3.0d, 0d, PIN_NAMES_ILI9341, 9.70d, PIN_NAMES_SD, 2.5d, 6.50d, false);
private final String label;
private final double boardWidthMm;
private final double boardLengthMm;
private final double screenWidthMm;
private final double screenLengthMm;
private final Glass glass;
private final double headerOffsetMm;
private final double holeInsetMm;
private final double holeSizeMm;
private final double tabWidthMm;
private final String[] pinNames;
private final double screenTopMm;
private final String[] secondaryPinNames;
private final double secondaryHeaderOffsetMm;
private final double headerSideHoleInsetMm;
private final double holeSideInsetMm;
private final boolean headerAtTop;
/** For a board whose lit area is centred and whose pins all leave on one row. */
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, boolean headerAtTop) {
this(label, boardWidthMm, boardLengthMm, screenWidthMm, screenLengthMm, glass, headerOffsetMm,
holeInsetMm, holeSizeMm, tabWidthMm, pinNames, 0d, null, 0d, holeInsetMm, headerAtTop);
}
/** For a board whose holes are the same distance in from every edge they are near. */
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, double screenTopMm,
String[] secondaryPinNames, double secondaryHeaderOffsetMm, double headerSideHoleInsetMm,
boolean headerAtTop) {
this(label, boardWidthMm, boardLengthMm, screenWidthMm, screenLengthMm, glass, headerOffsetMm,
holeInsetMm, holeSizeMm, tabWidthMm, pinNames, screenTopMm, secondaryPinNames,
secondaryHeaderOffsetMm, headerSideHoleInsetMm, holeInsetMm, headerAtTop);
}
Controller(String label, double boardWidthMm, double boardLengthMm, double screenWidthMm,
double screenLengthMm, Glass glass, double headerOffsetMm, double holeInsetMm,
double holeSizeMm, double tabWidthMm, String[] pinNames, double screenTopMm,
String[] secondaryPinNames, double secondaryHeaderOffsetMm, double headerSideHoleInsetMm,
double holeSideInsetMm, boolean headerAtTop) {
this.label = label;
this.boardWidthMm = boardWidthMm;
this.boardLengthMm = boardLengthMm;
this.screenWidthMm = screenWidthMm;
this.screenLengthMm = screenLengthMm;
this.glass = glass;
this.headerOffsetMm = headerOffsetMm;
this.holeInsetMm = holeInsetMm;
this.holeSizeMm = holeSizeMm;
this.tabWidthMm = tabWidthMm;
this.pinNames = pinNames;
this.screenTopMm = screenTopMm;
this.secondaryPinNames = secondaryPinNames;
this.secondaryHeaderOffsetMm = secondaryHeaderOffsetMm;
this.headerSideHoleInsetMm = headerSideHoleInsetMm;
this.holeSideInsetMm = holeSideInsetMm;
this.headerAtTop = headerAtTop;
}
@Override public String toString() { return label; }
public double getBoardWidthMm() { return boardWidthMm; }
public double getBoardLengthMm() { return boardLengthMm; }
public double getScreenWidthMm() { return screenWidthMm; }
public double getScreenLengthMm() { return screenLengthMm; }
/** The dark panel, or {@code null} on a board whose lit area is all there is to draw. */
public Glass getGlass() { return glass; }
public double getHeaderOffsetMm() { return headerOffsetMm; }
public double getHoleSizeMm() { return holeSizeMm; }
public String[] getPinNames() { return pinNames; }
/**
* Inset from each of the two edges nearest a hole, except where
* {@link #getHeaderSideHoleInsetMm()} or {@link #getHoleSideInsetMm()} overrides it.
*/
public double getHoleInsetMm() { return holeInsetMm; }
/** Overrides the shared inset across the board; only the 1.8" with the SD socket needs it. */
public double getHoleSideInsetMm() { return holeSideInsetMm; }
/**
* Inset of the two holes on the header's edge, set deeper on boards whose row would reach them.
*/
public double getHeaderSideHoleInsetMm() { return headerSideHoleInsetMm; }
public boolean hasMountingHoles() { return holeSizeMm > 0; }
public boolean hasGlass() { return glass != null; }
/** True where the main row leaves by the top edge, so the board hangs below it. */
public boolean isHeaderAtTop() { return headerAtTop; }
/** Width of the tab the header sits on; meaningful only for a round board. */
public double getTabWidthMm() { return tabWidthMm; }
/** How far the tab projects past the disc: whatever the outline is longer than it is wide. */
public double getTabProjectionMm() { return boardLengthMm - boardWidthMm; }
public boolean isRound() { return tabWidthMm > 0; }
/** Top of the lit area from the board's top edge; zero where it is centred in the glass. */
public double getScreenTopMm() { return screenTopMm; }
public boolean hasMeasuredScreenTop() { return screenTopMm > 0; }
/** The second pin row, or {@code null} on a board that brings everything out on one. */
public String[] getSecondaryPinNames() { return secondaryPinNames; }
public boolean hasSecondaryHeader() { return secondaryPinNames != null; }
/** Clearance from the second row to the top edge, the one the main header does not use. */
public double getSecondaryHeaderOffsetMm() { return secondaryHeaderOffsetMm; }
}
/**
* The dark panel the lit area sits in, whose frame is the display's visible border. Centred
* between the board's mounting holes, and across the board unless it states an inset from the
* right edge.
*
* @author Branislav Stojkovic
*/
public static class Glass {
private final double lengthMm;
private final double widthMm;
private final double rightInsetMm;
/** A panel that spans the board. */
public Glass(double lengthMm) {
this(lengthMm, 0d, 0d);
}
/** A panel narrower than its board, centred across it. */
public Glass(double lengthMm, double widthMm) {
this(lengthMm, widthMm, 0d);
}
/** A panel narrower than its board and pushed towards the right edge. */
public Glass(double lengthMm, double widthMm, double rightInsetMm) {
this.lengthMm = lengthMm;
this.widthMm = widthMm;
this.rightInsetMm = rightInsetMm;
}
public double getLengthMm() { return lengthMm; }
/** Zero where the panel is as wide as the board. */
public double getWidthMm() { return widthMm; }
/** How far the panel stops short of the right edge; meaningful only where it is narrower. */
public double getRightInsetMm() { return rightInsetMm; }
public boolean spansTheBoard() { return widthMm <= 0; }
/** True for a narrower panel placed from the right edge rather than centred on the board. */
public boolean isPlacedFromTheRightEdge() { return rightInsetMm > 0; }
}
}
@@ -0,0 +1,232 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "NeoPixel Breakout", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Single-LED Addressable RGB WS2812B NeoPixel Breakout",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BBreakout extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
// A three-pad column up each short edge, with the two mounting holes on the centre line
// between them.
public static Size BOARD_WIDTH = new Size(0.5d, SizeUnit.in);
public static Size BOARD_HEIGHT = new Size(0.4d, SizeUnit.in);
// The real board's three-way connectors are drawn as plain pads: at this size a connector body
// would hide the LED, and what a layout needs is where the three wires land. The pads are flush
// with their own edge, so how far in their centres sit is half their reach.
public static Size PAD_WIDTH = new Size(1.5d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(0.3d, SizeUnit.in);
// The two grounds are the same net on the board, as are the two supplies, and are numbered only
// to keep the node names distinct.
private static final String[] PIN_NAMES = {
"GND_1", "VIN_1", "IN",
"GND_2", "VIN_2", "OUT"
};
public WS2812BBreakout() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// the columns are flush against their own edges, so what separates them is the board less one
// whole pad
double columnSpacing = BOARD_WIDTH.convertToPixels() - PAD_WIDTH.convertToPixels();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
relativeOffsets[3 + i][0] = columnSpacing;
relativeOffsets[3 + i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
/** The first pad column is flush with this edge, so its centres sit half a pad inboard. */
private double getBoardX(double x) {
return x - PAD_WIDTH.convertToPixels() / 2.0;
}
/** Derived so that the three-pad column is centred across the height. */
private double getBoardY(double y) {
double padSpan = 2 * PIN_SPACING.convertToPixels();
return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0;
}
Point2D getLedCentre() {
Point2D p0 = controlPoints[0];
return new Point2D.Double(getBoardX(p0.getX()) + BOARD_WIDTH.convertToPixels() / 2.0,
getBoardY(p0.getY()) + BOARD_HEIGHT.convertToPixels() / 2.0);
}
/**
* The holes sit on the centre line astride the LED and are what the board is tightest against:
* 0.27 mm of board outside each rim and 0.31 mm between the rim and the package.
*/
Point2D getHoleCentre(boolean bottom) {
Point2D centre = getLedCentre();
double offset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
return new Point2D.Double(centre.getX(), centre.getY() + (bottom ? offset : -offset));
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()),
BOARD_WIDTH.convertToPixels(), BOARD_HEIGHT.convertToPixels(), 4, 4);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (boolean bottom : new boolean[] {false, true}) {
Point2D hole = getHoleCentre(bottom);
MakerBoardPainter.drawMountingHole(g2d, hole.getX(), hole.getY(), holeSize);
}
// The 2.35 mm between a pad and the package is not room beside a pad, so there is nowhere a
// name would go and this face carries no silkscreen. Drawn lit off the family's wheel, so a
// breakout reads as the same part as one pixel of a stick.
Point2D led = getLedCentre();
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(),
RGB_LED_SIZE.convertToPixels(), getLedColors(1)[0], 0, getLedType());
}
g2d.setTransform(oldTx);
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
// landscape in the board's own 5:4, so the icon does not read as one of the portrait modules
double rectW = width - 4;
double rectH = rectW * 0.8;
double rectX = 2;
double rectY = (height - rectH) / 2.0;
g2d.setColor(NEO_BLACK);
g2d.fill(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 3, 3));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 3, 3));
double padW = 3;
double padH = 2;
double padStep = 5;
g2d.setColor(PAD_COLOR);
for (int i = 0; i < 3; i++) {
double padY = height / 2.0 - padStep + i * padStep - padH / 2.0;
g2d.fill(new Rectangle2D.Double(rectX, padY, padW, padH));
g2d.fill(new Rectangle2D.Double(rectX + rectW - padW, padY, padW, padH));
}
double ledSize = Math.min(rectW - 6, 12);
MakerBoardPainter.drawAddressableLed(g2d, width / 2.0, height / 2.0, ledSize,
Color.decode("#FF3333"));
}
}
@@ -0,0 +1,256 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// A sibling of WS2812BRing rather than another RingSize constant: the Jewel is a solid disc
// with no inner rim, six LEDs on a circle with a seventh in the middle, pads inside that circle
// rather than out by the rim, and two mounting holes no ring has.
@ComponentDescriptor(name = "NeoPixel Jewel", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B NeoPixel Jewel (7 LEDs)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BJewel extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
public static Size OUTER_DIAMETER = new Size(23.0d, SizeUnit.mm);
// Measured: with no inner rim there is no midpoint to place the LEDs on, as the rings do.
public static Size LED_CIRCLE_DIAMETER = new Size(16.0d, SizeUnit.mm);
// Inside the LED circle, where the rings put theirs just in from the outer rim.
public static Size PAD_CIRCLE_DIAMETER = new Size(9.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(19.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
/** The LEDs on the circle. The seventh sits in the middle and is not one of them. */
public static final int RING_LED_COUNT = 6;
private static final String[] PAD_NAMES = new String[] {"OUT", "G_1", "G_2", "PWR", "IN"};
// Which gap between two consecutive ring LEDs each pad sits in. OUT is anticlockwise of the
// top LED and the rest follow clockwise, so the sixth gap carries no pad: it is the one due
// left, where the left mounting hole sits.
private static final int[] PAD_GAP_INDEX = new int[] {0, 1, 2, 3, 4};
public WS2812BJewel() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PAD_NAMES.length) {
return PAD_NAMES[index];
}
return Integer.toString(index + 1);
}
private double getOuterRadius() {
return OUTER_DIAMETER.convertToPixels() / 2.0;
}
/**
* The one place the pad radius is decided: {@link #updateControlPoints()} offsets every pad from
* the first by it and {@link #getCenter()} works backwards from the first pad, so the two
* disagreeing would draw the body away from its own control points.
*/
private double getPadRadius() {
return PAD_CIRCLE_DIAMETER.convertToPixels() / 2.0;
}
/**
* Each pad sits in a gap between two consecutive ring LEDs, the gaps {@link #PAD_GAP_INDEX}
* records. Where the sequence starts is arbitrary, since the component rotates.
*/
private double getPadAngle(int index) {
double gap = PAD_GAP_INDEX[index] + RING_LED_COUNT - 0.5;
return 2 * Math.PI * gap / RING_LED_COUNT - Math.PI / 2.0;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double padR = getPadRadius();
double theta0 = getPadAngle(0);
double[][] relativeOffsets = new double[PAD_NAMES.length][2];
for (int i = 0; i < PAD_NAMES.length; i++) {
double theta = getPadAngle(i);
relativeOffsets[i][0] = padR * (Math.cos(theta) - Math.cos(theta0));
relativeOffsets[i][1] = padR * (Math.sin(theta) - Math.sin(theta0));
}
rotatePoints(firstPoint, relativeOffsets);
}
private Point2D getCenter() {
Point2D p0 = controlPoints[0];
double padR = getPadRadius();
double theta0 = getPadAngle(0);
// unrotated, the centre sits one pad radius back along the first pad's angle
return new Point2D.Double(p0.getX() - padR * Math.cos(theta0),
p0.getY() - padR * Math.sin(theta0));
}
@Override
public Shape getBodyShape() {
Point2D center = getCenter();
double outerR = getOuterRadius();
return new Ellipse2D.Double(center.getX() - outerR, center.getY() - outerR, outerR * 2,
outerR * 2);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
Point2D center = getCenter();
double cx = center.getX();
double cy = center.getY();
double outerR = getOuterRadius();
Shape disc = new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2);
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(disc);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(disc);
if (!outlineMode) {
double holeOffset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, cx - holeOffset, cy, holeSize);
MakerBoardPainter.drawMountingHole(g2d, cx + holeOffset, cy, holeSize);
double ledR = LED_CIRCLE_DIAMETER.convertToPixels() / 2.0;
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors(RING_LED_COUNT + 1);
LedType ledType = getLedType();
for (int i = 0; i < RING_LED_COUNT; i++) {
double angle = 2 * Math.PI * i / RING_LED_COUNT - Math.PI / 2.0;
MakerBoardPainter.drawAddressableLed(g2d, cx + ledR * Math.cos(angle),
cy + ledR * Math.sin(angle), ledSize, ledColors[i % ledColors.length],
angle + Math.PI / 2.0, ledType);
}
// the centre package has no radius to face along, so it stays square to the board
MakerBoardPainter.drawAddressableLed(g2d, cx, cy, ledSize,
ledColors[RING_LED_COUNT % ledColors.length], 0, ledType);
// A pad sits in the gap between two LEDs, nowhere near enough room for its name.
}
g2d.setTransform(oldTx);
drawPcbSolderPads(g2d, 0, PAD_NAMES.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
int cx = width / 2;
int cy = height / 2;
int outerR = width / 2 - 2;
Ellipse2D disc = new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2);
g2d.setColor(NEO_BLACK);
g2d.fill(disc);
g2d.setColor(Color.DARK_GRAY);
g2d.draw(disc);
Color[] rainbow = new Color[] {
Color.decode("#FF3333"),
Color.decode("#FFD700"),
Color.decode("#00E676"),
Color.decode("#00E5FF"),
Color.decode("#E040FB"),
Color.decode("#FF6B35")
};
double ledR = outerR * 0.6;
int dotR = Math.max(2, (int) (outerR * 0.22));
for (int i = 0; i < RING_LED_COUNT; i++) {
double angle = 2 * Math.PI * i / RING_LED_COUNT - Math.PI / 2.0;
double lx = cx + ledR * Math.cos(angle);
double ly = cy + ledR * Math.sin(angle);
g2d.setColor(rainbow[i % rainbow.length]);
g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2));
}
// the filled centre is what separates this icon from the ring's
g2d.setColor(Color.WHITE);
g2d.fill(new Ellipse2D.Double(cx - dotR, cy - dotR, dotR * 2, dotR * 2));
}
}
@@ -0,0 +1,326 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.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.Ellipse2D;
import java.awt.geom.Point2D;
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.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.utils.Constants;
// Figures are read off Adafruit's published EagleCAD board file rather than a product listing,
// which gives only the outline.
//
// Three relationships hold on this board and are recorded as the measurements they are rather
// than derived, because a panel from another maker need not share them: the board is exactly
// eight pitches across, the pixel grid is centred leaving half a pitch outside the outer
// pixels, and the port row lands in the gap between the first and second rows of pixels.
@ComponentDescriptor(name = "NeoPixel Panel", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B NeoPixel Matrix Panel (8x8, 64 LEDs)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BPanel extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color NEO_BLACK = Color.decode("#111111");
public static Size BOARD_SIZE = new Size(71.12d, SizeUnit.mm);
public static Size PIXEL_PITCH = new Size(8.89d, SizeUnit.mm);
/** How far the outer pad of a port sits in from the side edge its port stands against. */
public static Size PAD_EDGE_INSET = new Size(1.905d, SizeUnit.mm);
/** How far a port row sits in from the end of the board it runs beside. */
public static Size PAD_ROW_INSET = new Size(8.89d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.8d, SizeUnit.mm);
public static Size CORNER_RADIUS = new Size(1.0d, SizeUnit.mm);
/** Pixels along one edge of the square matrix. */
public static final int MATRIX_ORDER = 8;
/**
* Hole centres from the middle of the board. The pattern is symmetric about both axes, so which
* way the vertical axis points does not change the drawing.
*/
private static final double[][] MOUNTING_HOLES = new double[][] {
{-33.02d, 17.78d}, {-33.02d, 0d}, {-33.02d, -17.78d},
{33.02d, 17.78d}, {33.02d, 0d}, {33.02d, -17.78d},
{-8.89d, 26.67d}, {8.89d, 26.67d},
{-8.89d, 0d}, {8.89d, 0d},
{-8.89d, -26.67d}, {8.89d, -26.67d}};
/**
* Input first, then output. Both ports carry the same two rails, numbered to keep the node names
* distinct; the board prints them +5V and GND, on its back rather than this face.
*/
public static final String[] PAD_NAMES =
new String[] {"DIN", "+5V_1", "GND_1", "DOUT", "+5V_2", "GND_2"};
/**
* Eagle specifies text by cap height and a Java font by em size, about a third larger again for
* this family. Converting is what makes the drawn strings reach as far across the board as the
* real artwork; set at the cap height they come out well short.
*/
private static final double CAP_HEIGHT_RATIO = 0.72d;
/**
* What the face prints, from layer 21 of the board file. Every line lies in a gap between two
* rows of pixels, which is the only bare board this part has. The port names are on layer 22,
* its back.
*/
static final Silk[] SILKSCREEN = new Silk[] {
new Silk(-19.685d, -17.653d, "Adafruit NeoPixel 8X8", silkFont(2.286d)),
new Silk(-29.972d, 0.0635d, "64 RGB LEDs", silkFont(1.778d)),
new Silk(-6.858d, -0.1397d, "24 bit Color", silkFont(1.4224d)),
new Silk(12.446d, -0.1905d, "Only ONE Pin", silkFont(1.778d)),
new Silk(-25.4d, 17.8435d, "bLiNkY bLiNkY bLiNkY bLiNkY bLiNkY", silkFont(1.778d))};
public WS2812BPanel() {
super();
this.bodyColor = NEO_BLACK;
updateControlPoints();
}
/** The panel comes in one size, so the package rather than the grid keeps the BOM rows apart. */
@Override
protected String getVariantLabel() {
return "8x8, 64 LEDs, " + getLedType();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PAD_NAMES.length) {
return PAD_NAMES[index];
}
return Integer.toString(index + 1);
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
private static Font silkFont(double capHeightMm) {
return new Font(SILK_FONT_FAMILY, Font.PLAIN,
(int) Math.round(px(capHeightMm) / CAP_HEIGHT_RATIO));
}
/** Left edge of the board; the input port's first pad sits one inset in from it. */
private double getBoardX(double x) {
return x - PAD_EDGE_INSET.convertToPixels();
}
/** Top edge of the board; the input port's row sits its own inset down from it. */
private double getBoardY(double y) {
return y - PAD_ROW_INSET.convertToPixels();
}
/**
* The two ports are each other turned about the centre of the board, which is what lets panels
* be chained: the input runs inward from the left edge near the top, the output inward from the
* right edge near the bottom.
*/
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double board = BOARD_SIZE.convertToPixels();
double acrossBoard = board - 2 * PAD_EDGE_INSET.convertToPixels();
double downBoard = board - 2 * PAD_ROW_INSET.convertToPixels();
double[][] relativeOffsets = new double[PAD_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = i * spacing;
relativeOffsets[i][1] = 0;
relativeOffsets[3 + i][0] = acrossBoard - i * spacing;
relativeOffsets[3 + i][1] = downBoard;
}
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double board = BOARD_SIZE.convertToPixels();
double radius = CORNER_RADIUS.convertToPixels();
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), board, board,
radius, radius);
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double board = BOARD_SIZE.convertToPixels();
double boardX = getBoardX(x);
double boardY = getBoardY(y);
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
double centreX = boardX + board / 2.0;
double centreY = boardY + board / 2.0;
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (double[] hole : MOUNTING_HOLES) {
MakerBoardPainter.drawMountingHole(g2d, centreX + px(hole[0]), centreY + px(hole[1]),
holeSize);
}
// printed before the packages, so a glyph straying under one is covered as it is on the board
g2d.setColor(SILK_COLOR);
for (Silk silk : SILKSCREEN) {
g2d.setFont(silk.getFont());
StringUtils.drawCenteredText(g2d, silk.getText(), centreX + px(silk.getXMm()),
centreY + px(silk.getYMm()), HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
}
double pitch = PIXEL_PITCH.convertToPixels();
double margin = (board - (MATRIX_ORDER - 1) * pitch) / 2.0;
double ledSize = RGB_LED_SIZE.convertToPixels();
// The sweep follows the data chain, so it shows the addressing order: a progressive raster,
// left to right along each row and back, not the serpentine most cheap panels use.
int ledCount = MATRIX_ORDER * MATRIX_ORDER;
Color[] ledColors = getLedColors(ledCount);
LedType ledType = getLedType();
for (int i = 0; i < ledCount; i++) {
MakerBoardPainter.drawAddressableLed(g2d, boardX + margin + (i % MATRIX_ORDER) * pitch,
boardY + margin + (i / MATRIX_ORDER) * pitch, ledSize, ledColors[i], 0, ledType);
}
// The port names are printed on the back of the real board, so this face carries none.
}
g2d.setTransform(oldTx);
drawPcbSolderPads(g2d, 0, PAD_NAMES.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
double boardSize = Math.min(width, height) - 4;
double boardX = (width - boardSize) / 2.0;
double boardY = (height - boardSize) / 2.0;
g2d.setColor(NEO_BLACK);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardSize, boardSize, 2, 2));
g2d.setColor(Color.DARK_GRAY);
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardSize, boardSize, 2, 2));
// four by four rather than eight by eight: at toolbox size eight rows fill in to a block
Color[] rainbow = new Color[] {
Color.decode("#FF3333"),
Color.decode("#FFD700"),
Color.decode("#00E676"),
Color.decode("#00E5FF"),
Color.decode("#E040FB"),
Color.decode("#FF6B35")
};
int order = 4;
double pitch = boardSize / order;
double dotSize = Math.max(2.0, pitch * 0.55);
for (int i = 0; i < order * order; i++) {
double dotX = boardX + (i % order + 0.5) * pitch - dotSize / 2.0;
double dotY = boardY + (i / order + 0.5) * pitch - dotSize / 2.0;
g2d.setColor(rainbow[i % rainbow.length]);
g2d.fill(new Ellipse2D.Double(dotX, dotY, dotSize, dotSize));
}
}
/**
* One line of silkscreen, positioned by its left edge and vertical centre from the middle of the
* board, with y running down the drawing rather than up as Eagle records it.
*/
static class Silk {
private final double xMm;
private final double yMm;
private final String text;
private final Font font;
Silk(double xMm, double yMm, String text, Font font) {
this.xMm = xMm;
this.yMm = yMm;
this.text = text;
this.font = font;
}
public double getXMm() { return xMm; }
public double getYMm() { return yMm; }
public String getText() { return text; }
public Font getFont() { return font; }
}
}
@@ -23,22 +23,20 @@ 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;
import org.diylc.common.ObjectCache;
import org.diylc.common.Orientation;
import org.diylc.common.VerticalAlignment;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -51,49 +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)
public class WS2812BRing extends AbstractMakerBoard {
@ComponentDescriptor(name = "NeoPixel Ring", 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 AbstractAddressableLedBoard {
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);
private final String label;
private final int ledCount;
private final double outerDiameterMm;
private final double innerDiameterMm;
RingSize(String label, int ledCount, double outerDiameterMm, double innerDiameterMm) {
this.label = label;
this.ledCount = ledCount;
this.outerDiameterMm = outerDiameterMm;
this.innerDiameterMm = innerDiameterMm;
}
@Override public String toString() { return label; }
public int getLedCount() { return ledCount; }
public double getOuterDiameterMm() { return outerDiameterMm; }
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"};
// The inherited solder pad footprint is sized for a breakout board rather than a ring.
public static Size PAD_EDGE_INSET = new Size(1.4d, SizeUnit.mm);
// Measured from the pad's edge, so the gap stays the same if the pad footprint changes.
public static Size PAD_LABEL_GAP = new Size(0.4d, SizeUnit.mm);
private RingSize ringSize = RingSize._16_LED;
@@ -114,39 +85,63 @@ public class WS2812BRing extends AbstractMakerBoard {
invalidateCache();
}
// Every ring is sold in each package, so neither half identifies what to buy on its own.
@Override
protected String getVariantLabel() {
RingSize ringSize = getRingSize();
return ringSize == null ? null : ringSize + ", " + getLedType();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
String[] padNames = ringSize.getPadNames();
if (index >= 0 && index < padNames.length) {
return padNames[index];
}
return Integer.toString(index + 1);
}
/** The circle the LED centres sit on: the midpoint between the inner and outer rims. */
private double getMidRadius() {
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
double innerR = new Size(ringSize.getInnerDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
return (outerR + innerR) / 2.0;
}
/**
* Pads sit out towards the rim rather than on the LED circle. This has to be the one place the
* radius is decided: {@link #updateControlPoints()} offsets every pad from the first by it and
* {@link #getCenter()} works backwards from the first pad, so the two disagreeing would draw the
* body away from its own control points.
*/
private double getPadRadius() {
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
return outerR - PAD_EDGE_INSET.convertToPixels();
}
/**
* Each pad sits in a gap between two consecutive LEDs, the gaps {@link RingSize} records. The
* sequence is anchored at the gap just past the bottom of the ring and runs clockwise; where it
* starts is arbitrary, since the component rotates.
*/
private double getPadAngle(int index) {
double spacing = PIN_SPACING.convertToPixels();
double midR = getMidRadius();
double deltaTheta = spacing / midR;
// 4 pads placed symmetrically at the bottom of the circular ring (near angle PI / 2)
return Math.PI / 2.0 + (index - 1.5) * deltaTheta;
int ledCount = ringSize.getLedCount();
double gap = ringSize.getPadGapIndex(index) + ledCount / 2.0 + 0.5;
return 2 * Math.PI * gap / ledCount - Math.PI / 2.0;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double midR = getMidRadius();
double padR = getPadRadius();
double theta0 = getPadAngle(0);
double[][] relativeOffsets = new double[4][2];
for (int i = 0; i < 4; i++) {
int padCount = ringSize.getPadNames().length;
double[][] relativeOffsets = new double[padCount][2];
for (int i = 0; i < padCount; i++) {
double theta = getPadAngle(i);
relativeOffsets[i][0] = midR * (Math.cos(theta) - Math.cos(theta0));
relativeOffsets[i][1] = midR * (Math.sin(theta) - Math.sin(theta0));
relativeOffsets[i][0] = padR * (Math.cos(theta) - Math.cos(theta0));
relativeOffsets[i][1] = padR * (Math.sin(theta) - Math.sin(theta0));
}
rotatePoints(firstPoint, relativeOffsets);
@@ -154,10 +149,10 @@ public class WS2812BRing extends AbstractMakerBoard {
private Point2D getCenter() {
Point2D p0 = controlPoints[0];
double midR = getMidRadius();
double padR = getPadRadius();
double theta0 = getPadAngle(0);
// When unrotated, center is at p0 - (midR*cos(theta0), midR*sin(theta0))
return new Point2D.Double(p0.getX() - midR * Math.cos(theta0), p0.getY() - midR * Math.sin(theta0));
// unrotated, the centre sits one pad radius back along the first pad's angle
return new Point2D.Double(p0.getX() - padR * Math.cos(theta0), p0.getY() - padR * Math.sin(theta0));
}
@Override
@@ -189,9 +184,7 @@ public class WS2812BRing extends AbstractMakerBoard {
double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
double innerR = new Size(ringSize.getInnerDiameterMm() / 2.0, SizeUnit.mm).convertToPixels();
double midR = (outerR + innerR) / 2.0;
// Annular ring shape: outer circle minus inner circle
Area outerArea = new Area(new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2));
Area innerArea = new Area(new Ellipse2D.Double(cx - innerR, cy - innerR, innerR * 2, innerR * 2));
outerArea.subtract(innerArea);
@@ -209,58 +202,49 @@ 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;
double ledR = getMidRadius();
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors(ledCount);
LedType ledType = getLedType();
// Draw all LEDs around the circular ring
// each package is turned to face out along its own radius, the way it is mounted
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 + ledR * Math.cos(angle),
cy + ledR * Math.sin(angle), ledSize, ledColors[i % ledColors.length],
angle + Math.PI / 2.0, ledType);
}
// Center silkscreen text
g2d.setColor(Color.decode("#666666"));
g2d.setFont(SILK_FONT_SMALL);
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);
// Names run out along their own radius, turned with the pad. A pad sits in the gap between
// two consecutive LEDs, and that is the only room a name has: square to the board it would
// lie across the LEDs either side.
g2d.setColor(SILK_COLOR);
g2d.setFont(PIN_FONT);
double labelR =
getPadRadius() - PAD_SIZE.convertToPixels() / 2.0 - PAD_LABEL_GAP.convertToPixels();
String[] padNames = ringSize.getPadNames();
for (int i = 0; i < padNames.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty()) {
continue;
}
double angle = getPadAngle(i);
double textX = cx + labelR * Math.cos(angle);
double textY = cy + labelR * Math.sin(angle);
// 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 turn maps the text advance direction onto the outward radius, so RIGHT alignment ends
// the name at the pad
AffineTransform oldLabelTx = g2d.getTransform();
g2d.rotate(angle, textX, textY);
StringUtils.drawCenteredText(g2d, label, textX, textY, HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
g2d.setTransform(oldLabelTx);
}
}
g2d.setTransform(oldTx);
// Draw the 4 solder pads directly on the circular ring PCB
drawSolderPads(g2d, 0, 4, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, ringSize.getPadNames().length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -301,4 +285,58 @@ public class WS2812BRing extends AbstractMakerBoard {
g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2));
}
}
public enum RingSize {
// Dimensioned in inches, so both diameters are exact conversions rather than roundings. LEDs
// sit on the midpoint between the rims, which misses the measured LED circle by 0.64 mm on the
// 12-LED ring and 0.51 mm on the 24 rather than carrying a third diameter per ring.
//
// Each ring lists its pads in order around the circle with the number of LEDs following each
// one. The final figure wraps back to the first pad, so the gaps must sum to the LED count;
// the constructor checks that.
_12_LED("12 LEDs", 12, 36.83, 23.368,
new String[] {"OUT", "IN", "GND", "PWR"},
new int[] {2, 4, 2, 4}),
_16_LED("16 LEDs", 16, 44.45, 31.75,
new String[] {"IN", "OUT", "G_1", "G_2", "V+_1", "V+_2"},
new int[] {2, 5, 1, 4, 1, 3}),
_24_LED("24 LEDs", 24, 65.532, 52.324,
new String[] {"OUT", "IN", "G_1", "G_2", "PWR_1", "PWR_2"},
new int[] {2, 8, 2, 2, 2, 8});
private final String label;
private final int ledCount;
private final double outerDiameterMm;
private final double innerDiameterMm;
private final String[] padNames;
private final int[] padGapIndex;
RingSize(String label, int ledCount, double outerDiameterMm, double innerDiameterMm,
String[] padNames, int[] ledsAfterPad) {
this.label = label;
this.ledCount = ledCount;
this.outerDiameterMm = outerDiameterMm;
this.innerDiameterMm = innerDiameterMm;
this.padNames = padNames;
this.padGapIndex = new int[padNames.length];
int gap = 0;
for (int i = 0; i < padNames.length; i++) {
padGapIndex[i] = gap;
gap += ledsAfterPad[i];
}
if (gap != ledCount) {
throw new IllegalArgumentException(
label + ": pad gaps sum to " + gap + " but the ring carries " + ledCount + " LEDs");
}
}
@Override public String toString() { return label; }
public int getLedCount() { return ledCount; }
public double getOuterDiameterMm() { return outerDiameterMm; }
public double getInnerDiameterMm() { return innerDiameterMm; }
public String[] getPadNames() { return padNames; }
/** Which gap between LEDs the pad at {@code index} sits in, counted from the first pad. */
public int getPadGapIndex(int index) { return padGapIndex[index]; }
}
}
@@ -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.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
@@ -49,30 +46,40 @@ 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)
public class WS2812BStick extends AbstractMakerBoard {
@ComponentDescriptor(name = "NeoPixel Stick", 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 AbstractAddressableLedBoard {
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(51.1d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(10.22d, SizeUnit.mm);
public static Size BOARD_WIDTH = new Size(53.0d, SizeUnit.mm);
public static Size BOARD_HEIGHT = new Size(10.2d, SizeUnit.mm);
// Surface pads flush with the cut end, so how far in their centres sit is half their width.
// The width is the reach inward from the edge; the length has to fit the 0.1 inch pad pitch.
public static Size PAD_WIDTH = new Size(3.0d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(1.5d, SizeUnit.mm);
// Air between a pad and the nearest package. Spreading the row to the pads' inner edge instead
// leaves the two touching, which draws as copper against plastic with no board between.
public static Size PAD_CLEARANCE = new Size(0.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(2.0d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SPACING = new Size(1.0d, SizeUnit.in);
public static Size MOUNTING_HOLE_TOP_OFFSET = new Size(2.0d, SizeUnit.mm);
public static final int LED_COUNT = 8;
// Both grounds on an end are the same net, 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() {
@@ -81,6 +88,11 @@ public class WS2812BStick extends AbstractMakerBoard {
updateControlPoints();
}
@Override
protected String getVariantLabel() {
return getLedType().toString();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
@@ -93,33 +105,66 @@ public class WS2812BStick extends AbstractMakerBoard {
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double w = BOARD_WIDTH.convertToPixels();
// the columns are flush against their own ends, so what separates them is the board less one
// whole pad
double columnSpacing = BOARD_WIDTH.convertToPixels() - PAD_WIDTH.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);
}
/** The first pad column is flush with this edge, so its centres sit half a pad inboard. */
private double getBoardX(double x) {
return x - PAD_WIDTH.convertToPixels() / 2.0;
}
/** Derived so that the four-pad column is centred across the width. */
private double getBoardY(double y) {
double padSpan = 3 * PIN_SPACING.convertToPixels();
return y - (BOARD_HEIGHT.convertToPixels() - padSpan) / 2.0;
}
/**
* The row is spread evenly between the inner edges of the two pad columns rather than packed
* edge to edge, and sits low on the board: the mounting holes take the top, so the packages are
* centred in what is left below them.
*/
Point2D getLedCentre(int index) {
Point2D p0 = controlPoints[0];
double boardX = getBoardX(p0.getX());
double boardY = getBoardY(p0.getY());
double ledSize = RGB_LED_SIZE.convertToPixels();
double inset = PAD_WIDTH.convertToPixels() + PAD_CLEARANCE.convertToPixels();
double fieldStart = boardX + inset;
double fieldEnd = boardX + BOARD_WIDTH.convertToPixels() - inset;
double pitch = (fieldEnd - fieldStart - ledSize) / (LED_COUNT - 1);
return new Point2D.Double(fieldStart + ledSize / 2.0 + index * pitch,
(getHoleCentre(false).getY() + MOUNTING_HOLE_SIZE.convertToPixels() / 2.0 + boardY
+ BOARD_HEIGHT.convertToPixels()) / 2.0);
}
/** The two holes sit a fixed span apart astride the board's centre. */
Point2D getHoleCentre(boolean right) {
Point2D p0 = controlPoints[0];
double offset = MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
return new Point2D.Double(
getBoardX(p0.getX()) + BOARD_WIDTH.convertToPixels() / 2.0 + (right ? offset : -offset),
getBoardY(p0.getY()) + MOUNTING_HOLE_TOP_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();
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
@@ -138,11 +183,6 @@ public class WS2812BStick extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardW = BOARD_WIDTH.convertToPixels();
double boardH = BOARD_HEIGHT.convertToPixels();
double boardX = x - 10;
double boardY = y - 10;
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
@@ -157,56 +197,30 @@ 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;
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));
double holeSize = MOUNTING_HOLE_SIZE.convertToPixels();
for (boolean right : new boolean[] {false, true}) {
Point2D hole = getHoleCentre(right);
MakerBoardPainter.drawMountingHole(g2d, hole.getX(), hole.getY(), holeSize);
}
// 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);
// The packages go down before the pads, so the row starts at the pads' inner edge rather than
// under them. The pad names are printed on the back of the real board.
double ledSize = RGB_LED_SIZE.convertToPixels();
Color[] ledColors = getLedColors(LED_COUNT);
LedType ledType = getLedType();
for (int i = 0; i < LED_COUNT; i++) {
Point2D led = getLedCentre(i);
MakerBoardPainter.drawAddressableLed(g2d, led.getX(), led.getY(), ledSize, ledColors[i], 0,
ledType);
}
}
g2d.setTransform(oldTx);
// Draw input and output header pins
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
// The stick ships bare and is wired by soldering, so it carries surface pads at the cut ends
// rather than the plated holes the ring and the jewel have.
drawSurfacePads(g2d, 0, controlPoints.length, PAD_WIDTH, PAD_LENGTH, outlineMode,
drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -239,11 +253,9 @@ public class WS2812BStick extends AbstractMakerBoard {
double dotX = rectX + margin + i * step;
double dotY = height / 2.0 - dotSize / 2.0;
// 5050 package mini white backing
g2d.setColor(Color.WHITE);
g2d.fill(new Rectangle2D.Double(dotX - 0.5, dotY - 0.5, dotSize + 1, dotSize + 1));
// RGB LED dot
g2d.setColor(rainbow[i]);
g2d.fill(new Ellipse2D.Double(dotX, dotY, dotSize, dotSize));
}
@@ -0,0 +1,373 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
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.AbstractAddressableLedBoard;
import org.diylc.components.MakerBoardPainter;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.annotations.BomPolicy;
import org.diylc.core.annotations.ComponentDescriptor;
import org.diylc.core.annotations.EditableProperty;
import org.diylc.core.annotations.KeywordPolicy;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.diylc.presenter.CalcUtils;
import org.diylc.utils.Constants;
@ComponentDescriptor(name = "NeoPixel Strip", category = "Displays & Outputs",
author = "Branislav Stojkovic",
description = "Addressable RGB WS2812B LED Strip (30, 60 or 144 LEDs per metre)",
instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class WS2812BStrip extends AbstractAddressableLedBoard {
private static final long serialVersionUID = 1L;
public static Color TAPE_WHITE = Color.decode("#F2F2F2");
// White tape is the one light-coloured board in this family, so the ink follows the board
// colour rather than being fixed.
public static Color SILK_DARK_COLOR = Color.decode("#333333");
// The uncut pad is a pill lying along the tape with the cut through its middle, so a cut end
// keeps half of one. Along the tape it is this nominal where there is room and otherwise as
// much as the density leaves, which at 144/m brings it down to its own width.
public static Size PAD_WIDTH = new Size(1.5d, SizeUnit.mm);
public static Size PAD_LENGTH = new Size(3.0d, SizeUnit.mm);
// Air left between a pad and the nearest package, and between a pad and its printed name.
public static Size PAD_CLEARANCE = new Size(0.2d, SizeUnit.mm);
public static Size LABEL_GAP = new Size(0.5d, SizeUnit.mm);
/**
* A cut length of tape rather than a board, so the count is a free number. The ceiling is about
* the shape rather than the control-point array, which stays at six: 144 LEDs of 30/m tape is
* already most of five metres.
*/
public static final int MIN_LED_COUNT = 1;
public static final int MAX_LED_COUNT = 144;
public static final int DEFAULT_LED_COUNT = 8;
// Both cuts carry all three lines, which is what makes tape chainable: the rails run the whole
// length as continuous copper and only the data pad changes sense. The rails are numbered to
// keep the node names distinct; the tape prints them +5V and GND at both ends.
public static final String[] PIN_NAMES =
new String[] {"+5V_1", "DIN", "GND_1", "+5V_2", "DOUT", "GND_2"};
// The bare designations the tape prints: the rails are not numbered on the part, and the data
// pad is printed two letters whichever end it is.
public static final String[] SILK_NAMES =
new String[] {"+5V", "DI", "GND", "+5V", "DO", "GND"};
private Density density = Density._60;
private int ledCount = DEFAULT_LED_COUNT;
public WS2812BStrip() {
super();
this.bodyColor = TAPE_WHITE;
updateControlPoints();
}
@EditableProperty(name = "Density")
public Density getDensity() {
if (density == null) {
density = Density._60;
}
return density;
}
public void setDensity(Density density) {
this.density = density;
updateControlPoints();
invalidateCache();
}
@EditableProperty(name = "LEDs")
public int getLedCount() {
if (ledCount < MIN_LED_COUNT) {
ledCount = DEFAULT_LED_COUNT;
}
return ledCount;
}
public void setLedCount(int ledCount) {
this.ledCount = Math.max(MIN_LED_COUNT, Math.min(MAX_LED_COUNT, ledCount));
updateControlPoints();
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getDensity() + ", " + getLedCount() + " LEDs, " + getLedType();
}
@Override
public String getControlPointNodeName(int index) {
if (index >= 0 && index < PIN_NAMES.length) {
return PIN_NAMES[index];
}
return Integer.toString(index + 1);
}
@Override
protected String getSilkPinLabel(int index) {
if (index >= 0 && index < SILK_NAMES.length) {
return SILK_NAMES[index];
}
return super.getSilkPinLabel(index);
}
/** The lettering that contrasts with the tape colour the user has chosen. */
private Color getSilkInk() {
return CalcUtils.calculateLuminance(bodyColor) < 128d ? SILK_COLOR : SILK_DARK_COLOR;
}
private double getPitch() {
return new Size(getDensity().getPitchMm(), SizeUnit.mm).convertToPixels();
}
private double getTapeWidth() {
return new Size(getDensity().getWidthMm(), SizeUnit.mm).convertToPixels();
}
/** Linear in the count rather than proportional to it, since the two cut ends do not scale. */
private double getTapeLength() {
return (getLedCount() - 1) * getPitch() + 2 * getLeadIn();
}
/** The span a column of three pads occupies across the tape. */
private static double getPadRunLength() {
return 2 * PIN_SPACING.convertToPixels();
}
/**
* Tape is a repeating cell one pitch long with its package in the middle and a cut falling on a
* cell boundary, so this is half a pitch. That is what makes a length exactly as many pitches
* long as it has LEDs, and keeps the pitch across the joint when two are butted together.
*/
double getLeadIn() {
return getPitch() / 2.0;
}
/**
* Half of the pill it was cut from: the nominal length where the density leaves room, otherwise
* as much as fits between the cut and the first package, never less than its own width.
*/
double getPadDepth() {
return getPadLength() / 2.0;
}
double getPadLength() {
double room = 2 * (getLeadIn() - RGB_LED_SIZE.convertToPixels() / 2.0
- PAD_CLEARANCE.convertToPixels());
return Math.max(PAD_WIDTH.convertToPixels(),
Math.min(PAD_LENGTH.convertToPixels(), room));
}
/** Control point zero sits here, on the cut, which runs through the pad. */
private double getBoardX(double x) {
return x;
}
/** Each column of pads is centred across the width, so the first sits above centre. */
private double getBoardY(double y) {
return y - (getTapeWidth() - getPadRunLength()) / 2.0;
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
// Spacing the three rows by the pad pitch rather than by a fraction of the width keeps them
// apart: fractions of a 10 mm tape would leave them almost edge to edge.
//
// The cut runs through the middle of a pad, so a control point sits on the cut itself and the
// two columns are a whole tape length apart. That is also what makes two lengths chain.
double columnSpacing = getTapeLength();
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
for (int i = 0; i < 3; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
relativeOffsets[3 + i][0] = columnSpacing;
relativeOffsets[3 + i][1] = i * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
return new RoundRectangle2D.Double(getBoardX(p0.getX()), getBoardY(p0.getY()), getTapeLength(),
getTapeWidth(), 4, 4);
}
/**
* Names go in the bare strip between the pad and the first package, which the density decides:
* 12.7 mm at 30/m, 4.3 mm at 60/m and 0.2 mm at 144/m. They are left off where they do not fit
* rather than shrunk to suit the worst case.
*/
private void drawCutEndLabels(Graphics2D g2d, double boardX, double boardY) {
double room = getLeadIn() - RGB_LED_SIZE.convertToPixels() / 2.0 - getPadDepth();
double gap = LABEL_GAP.convertToPixels();
double spacing = PIN_SPACING.convertToPixels();
double firstRow = boardY + (getTapeWidth() - getPadRunLength()) / 2.0;
g2d.setFont(PIN_ROW_FLAT_FONT);
g2d.setColor(getSilkInk());
FontMetrics metrics = g2d.getFontMetrics();
for (int i = 0; i < PIN_NAMES.length; i++) {
String label = getSilkPinLabel(i);
if (label == null || label.isEmpty() || metrics.stringWidth(label) + gap > room) {
continue;
}
boolean input = i < 3;
double x = input ? boardX + getPadDepth() + gap
: boardX + getTapeLength() - getPadDepth() - gap;
StringUtils.drawCenteredText(g2d, label, x, firstRow + (i % 3) * spacing,
input ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
}
}
@Override
public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode,
Project project, IDrawingObserver drawingObserver) {
if (checkPointsClipped(g2d.getClip())) {
return;
}
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
AffineTransform oldTx = g2d.getTransform();
if (orientation != Orientation.DEFAULT) {
g2d.rotate(orientation.toRadians(), x, y);
}
double boardX = getBoardX(x);
double boardY = getBoardY(y);
double tapeH = getTapeWidth();
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
drawingObserver.stopTracking();
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
drawCutEndLabels(g2d, boardX, boardY);
double pitch = getPitch();
double ledSize = RGB_LED_SIZE.convertToPixels();
double firstCenter = boardX + getLeadIn();
int count = getLedCount();
Color[] colors = getLedColors(count);
LedType ledType = getLedType();
for (int i = 0; i < count; i++) {
MakerBoardPainter.drawAddressableLed(g2d, firstCenter + i * pitch, boardY + tapeH / 2.0,
ledSize, colors[i], 0, ledType);
}
}
g2d.setTransform(oldTx);
double padWidth = PAD_WIDTH.convertToPixels();
drawBisectedPads(g2d, 0, 3, padWidth, getPadLength(), true, outlineMode, drawingObserver);
drawBisectedPads(g2d, 3, 3, padWidth, getPadLength(), false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(TAPE_WHITE);
g2d.fill(new RoundRectangle2D.Double(1, height / 2.0 - 5, width - 2, 10, 2, 2));
g2d.setColor(Color.GRAY);
g2d.draw(new RoundRectangle2D.Double(1, height / 2.0 - 5, width - 2, 10, 2, 2));
Color[] colors = new Color[] {Color.decode("#FFEE00"), Color.decode("#FF1122"),
Color.decode("#2255FF"), Color.decode("#22DD44")};
for (int i = 0; i < 4; i++) {
g2d.setColor(colors[i]);
g2d.fillRect(3 + i * (width - 6) / 4, (int) (height / 2.0 - 3), 5, 6);
}
}
/**
* Tape is sold by LEDs per metre, so the pitch is derived rather than stored. Width does not
* follow and is a stated norm rather than a measurement: 10 mm at 30 and 60, 12 mm at 144,
* where the package leaves no room for the conductor beside it.
*/
public enum Density {
_30("30 LED/m", 30, 10.0d),
_60("60 LED/m", 60, 10.0d),
_144("144 LED/m", 144, 12.0d);
private final String label;
private final int ledsPerMetre;
private final double widthMm;
Density(String label, int ledsPerMetre, double widthMm) {
this.label = label;
this.ledsPerMetre = ledsPerMetre;
this.widthMm = widthMm;
}
@Override public String toString() { return label; }
public int getLedsPerMetre() { return ledsPerMetre; }
public double getWidthMm() { return widthMm; }
/** Centre-to-centre LED spacing: one metre shared out between that many LEDs. */
public double getPitchMm() { return 1000.0d / ledsPerMetre; }
}
}
@@ -64,7 +64,7 @@ public class ArduinoMega extends AbstractMakerBoard {
// Pin names in sequence (98 pins total)
public static final String[] PIN_NAMES = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Low A0..A7 (8..15)
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7",
// Analog High A8..A15 (16..23)
@@ -72,7 +72,7 @@ public class ArduinoMega extends AbstractMakerBoard {
// Digital Low D0..D7 (24..31)
"D0 (RX0)", "D1 (TX0)", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)",
// Digital High D8..D13, GND, AREF, SDA, SCL (32..41)
"D8 (~)", "D9 (~)", "D10 (~)", "D11 (~)", "D12 (~)", "D13 (~)", "GND3", "AREF", "SDA", "SCL",
"D8 (~)", "D9 (~)", "D10 (~)", "D11 (~)", "D12 (~)", "D13 (~)", "GND_3", "AREF", "SDA", "SCL",
// Communication Header D14..D21 (42..49)
"D14 (TX3)", "D15 (RX3)", "D16 (TX2)", "D17 (RX2)", "D18 (TX1)", "D19 (RX1)", "D20 (SDA)", "D21 (SCL)",
// Double Digital 2x18 Header (50..85: D22..D53, GNDx2, 5Vx2)
@@ -294,7 +294,6 @@ public class ArduinoMega extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
// Draw PCB body
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
@@ -305,7 +304,7 @@ public class ArduinoMega extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// Mounting holes (6 mounting holes from technical design)
// six, from the technical design
double holeDiameter = new Size(0.12d, SizeUnit.in).convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.55d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter); // (550, 100 mils)
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter); // (600, 2000 mils)
@@ -343,11 +342,9 @@ public class ArduinoMega extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "RST", btnX + btnW / 2.0,
btnY + btnH + new Size(1.0d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Arduino Infinity Logo
MakerBoardLogos.drawArduinoLogo(g2d, boardX + new Size(1.2d, SizeUnit.in).convertToPixels(),
boardY + new Size(0.4d, SizeUnit.in).convertToPixels());
// Silkscreen text & branding
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT_LARGE);
StringUtils.drawCenteredText(g2d, "ARDUINO", boardX + new Size(1.44d, SizeUnit.in).convertToPixels(),
@@ -403,7 +400,6 @@ public class ArduinoMega extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw header pins with continuity tracking
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
@@ -416,38 +412,32 @@ public class ArduinoMega extends AbstractMakerBoard {
double boardW = width - boardX - 2;
double boardH = height - 6;
// USB Type-B Jack (metallic silver, top left protruding)
double usbW = 7;
double usbH = 8;
double usbX = 1;
double usbY = boardY + 2;
// DC Power Jack (dark body, bottom left protruding)
double dcW = 7;
double dcH = 6;
double dcX = 1;
double dcY = boardY + boardH - dcH - 3;
// Board PCB
g2d.setColor(ARDUINO_TEAL);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
g2d.setColor(ARDUINO_TEAL.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
// Draw USB Jack
g2d.setColor(USB_METAL_COLOR);
g2d.fill(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
g2d.setColor(METAL_SHIELD_BORDER);
g2d.draw(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
// Draw DC Jack
g2d.setColor(IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
g2d.setColor(Color.BLACK);
g2d.draw(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
// Arduino Infinity logo
double scale = 14.0 / 95.56;
double logoW = 95.56 * scale;
double logoH = 45.33 * scale;
@@ -455,7 +445,6 @@ public class ArduinoMega extends AbstractMakerBoard {
double logoY = boardY + 3.5;
MakerBoardLogos.drawArduinoLogo(g2d, logoX, logoY, scale);
// MEGA text below logo
g2d.setColor(SILK_COLOR);
int fontSize = Math.max(6, (int) Math.round(boardH * 0.26));
g2d.setFont(ICON_FONT.deriveFont((float) fontSize));
@@ -76,63 +76,11 @@ public class ArduinoNano extends AbstractMakerBoard {
public static final int PINS_PER_ROW = 15;
// Ordered by label so the Version drop-down reads alphabetically. XStream serializes enum
// constants by name, so the order here is free to change without affecting existing .diy files.
public enum NanoVersion {
CLASSIC("Nano (ATmega328)", "NANO", "m328P", null),
NANO_33_BLE("Nano 33 BLE", "33 BLE", null, "NINA-B306"),
NANO_33_BLE_SENSE("Nano 33 BLE Sense", "33 BLE SENSE", null, "NINA-B306"),
NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"),
NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106"),
EVERY("Nano Every", "EVERY", "m4809", "SAMD11"),
NANO_R4("Nano R4", "NANO R4", "RA4M1", "QWIIC"),
// Silkscreened "CONNECT" rather than the full name, which does not fit between the two rows of
// pad names; the RP2040 package right below it carries the part number
NANO_RP2040_CONNECT("Nano RP2040 Connect", "CONNECT", "RP2040", "NINA-W102");
private final String label;
private final String silkLabel;
private final String mcuLabel;
private final String moduleLabel;
NanoVersion(String label, String silkLabel, String mcuLabel, String moduleLabel) {
this.label = label;
this.silkLabel = silkLabel;
this.mcuLabel = mcuLabel;
this.moduleLabel = moduleLabel;
}
public String getSilkLabel() {
return silkLabel;
}
/**
* Marking of the separately visible MCU package, or null when the MCU is a die inside the
* module rather than its own part on the board.
*/
public String getMcuLabel() {
return mcuLabel;
}
/**
* Marking of the part occupying the end of the board opposite the USB jack, where the classic
* Nano has its ICSP header. Null on the classic Nano, which has the header instead.
*/
public String getModuleLabel() {
return moduleLabel;
}
@Override
public String toString() {
return label;
}
}
public static final String[] PIN_NAMES = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13",
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13",
// ICSP (30..35)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP"
};
@@ -145,26 +93,26 @@ public class ArduinoNano extends AbstractMakerBoard {
// Nano Every (ABX00028): PWM on D3, D5, D6, D9, D10 only, and D13 is SCK.
public static final String[] PIN_NAMES_EVERY = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
};
// Nano 33 IoT (ABX00027): the SAMD21 adds PWM on D2 relative to the Every.
public static final String[] PIN_NAMES_33_IOT = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
};
// Nano 33 BLE and BLE Sense (ABX00030): the nRF52840 routes PWM to every digital pin, D13
// included.
public static final String[] PIN_NAMES_33_BLE = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (~, SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano RP2040 Connect (ABX00053): every RP2040 GPIO has a PWM slice. Note REC (BOOTSEL) where
@@ -173,17 +121,17 @@ public class ArduinoNano extends AbstractMakerBoard {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
"VIN", "GND_2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano R4 (ABX00142): the Renesas RA4M1 board. Same PWM set as the classic, but D4 / D5 double as
// the CAN pins, A1-A3 reach the on-chip OPAMP, A0 is the DAC, and BOOT replaces the second RESET.
public static final String[] PIN_NAMES_R4 = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D7", "D8", "D9 (~)", "D10 (~, CS)", "D11 (~, MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"VIN", "GND_2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"A2 (OPAMP -)", "A1 (OPAMP +)", "A0 (DAC)", "AREF", "3.3V", "D13 (SCK)",
// Qwiic socket (30), a single point at the centre of the connector so a wire can land on it
"QWIIC"
@@ -194,11 +142,11 @@ public class ArduinoNano extends AbstractMakerBoard {
// number, which is what the ESP-IDF documentation refers to.
public static final String[] PIN_NAMES_ESP32 = new String[] {
// Left row (0..14, top to bottom)
"D1 (~, TX/GPIO43)", "D0 (~, RX/GPIO44)", "RST1", "GND1", "D2 (~, GPIO5)", "D3 (~, GPIO6)",
"D1 (~, TX/GPIO43)", "D0 (~, RX/GPIO44)", "RST1", "GND_1", "D2 (~, GPIO5)", "D3 (~, GPIO6)",
"D4 (~, GPIO7)", "D5 (~, GPIO8)", "D6 (~, GPIO9)", "D7 (~, GPIO10)", "D8 (~, GPIO17)",
"D9 (~, GPIO18)", "D10 (~, GPIO21)", "D11 (~, MOSI/GPIO38)", "D12 (~, MISO/GPIO47)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "B1", "VUSB", "A7 (~, GPIO14)", "A6 (~, GPIO13)", "A5 (~, SCL/GPIO12)",
"VIN", "GND_2", "B1", "VUSB", "A7 (~, GPIO14)", "A6 (~, GPIO13)", "A5 (~, SCL/GPIO12)",
"A4 (~, SDA/GPIO11)", "A3 (~, GPIO4)", "A2 (~, GPIO3)", "A1 (~, GPIO2)", "A0 (~, GPIO1)",
"B0", "3.3V", "D13 (~, SCK/GPIO48)"
};
@@ -262,6 +210,11 @@ public class ArduinoNano extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
private String[] getPinNames() {
switch (getVersion()) {
case EVERY:
@@ -391,9 +344,8 @@ public class ArduinoNano extends AbstractMakerBoard {
return relativeOffsets;
}
// ICSP header (2x3 pins, 30..35) flush with top edge:
// Outer row is 0.05" (10px) from top edge (-20px / -0.10" relative to Pin 0)
// Inner row is 0.15" (30px) from top edge (0px / 0.00" relative to Pin 0, aligned with Pin 0 & Pin 15)
// ICSP block flush with the top edge: the outer row 0.05" from it and the inner row
// 0.15", which puts the inner row level with pins 0 and 15.
double icspOuterY = -spacing; // -20px (-0.10")
double icspInnerY = 0; // 0px (0.00")
@@ -401,17 +353,11 @@ public class ArduinoNano extends AbstractMakerBoard {
double col1X = rowSpacing / 2.0; // 60px (0.30")
double col2X = rowSpacing / 2.0 + spacing; // 80px (0.40")
// Pin 1 (MISO at col2X, outer row)
relativeOffsets[30] = new double[] {col2X, icspOuterY};
// Pin 2 (5V_ICSP at col2X, inner row)
relativeOffsets[31] = new double[] {col2X, icspInnerY};
// Pin 3 (SCK at col1X, outer row)
relativeOffsets[32] = new double[] {col1X, icspOuterY};
// Pin 4 (MOSI at col1X, inner row)
relativeOffsets[33] = new double[] {col1X, icspInnerY};
// Pin 5 (RST_ICSP at col0X, outer row)
relativeOffsets[34] = new double[] {col0X, icspOuterY};
// Pin 6 (GND_ICSP at col0X, inner row)
relativeOffsets[35] = new double[] {col0X, icspInnerY};
return relativeOffsets;
@@ -480,7 +426,6 @@ public class ArduinoNano extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
// Draw PCB body
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
@@ -529,7 +474,6 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(-chipSize / 2.0, -chipSize / 2.0, chipSize, chipSize, 4, 4));
// Pin 1 dot
g2d.setColor(PIN_MARKER_COLOR);
g2d.fill(new Ellipse2D.Double(-chipSize / 2.0 + 3, -chipSize / 2.0 + 3, 3, 3));
@@ -583,7 +527,6 @@ public class ArduinoNano extends AbstractMakerBoard {
}
}
// Reset Button
double btnW = BUTTON_WIDTH.convertToPixels();
double btnH = BUTTON_LENGTH.convertToPixels();
double btnX = boardX + (boardW - btnW) / 2.0;
@@ -615,7 +558,6 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw pins or solder pads
if (headers) {
drawPinHeader(g2d, 0, getHeaderPinCount(), outlineMode, drawingObserver);
} else if (getVersion() == NanoVersion.CLASSIC) {
@@ -637,15 +579,12 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.setColor(ARDUINO_TEAL.darker());
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
// ICSP header at top
g2d.setColor(HEADER_BODY_COLOR);
g2d.fill(new Rectangle2D.Double(width / 2.0 - 4, boardY, 8, 4));
// Mini USB at bottom
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect((int) (width / 2.0 - 4), (int) (boardY + boardH - 4), 8, 4);
// 45-degree rotated diamond IC chip in center
AffineTransform oldTx = g2d.getTransform();
g2d.translate(width / 2.0, boardY + boardH / 2.0 + 1);
g2d.rotate(Math.PI / 4.0);
@@ -655,11 +594,62 @@ public class ArduinoNano extends AbstractMakerBoard {
g2d.draw(new RoundRectangle2D.Double(-4, -4, 8, 8, 1, 1));
g2d.setTransform(oldTx);
// Pin strips on sides
g2d.setColor(PIN_COLOR);
for (int y = 5; y < height - 5; y += 3) {
g2d.fillRect(7, y, 2, 2);
g2d.fillRect(width - 9, y, 2, 2);
}
}
// Ordered by label so the Version drop-down reads alphabetically. XStream serializes enum
// constants by name, so the order here is free to change without affecting existing .diy files.
public enum NanoVersion {
CLASSIC("Nano (ATmega328)", "NANO", "m328P", null),
NANO_33_BLE("Nano 33 BLE", "33 BLE", null, "NINA-B306"),
NANO_33_BLE_SENSE("Nano 33 BLE Sense", "33 BLE SENSE", null, "NINA-B306"),
NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"),
NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106"),
EVERY("Nano Every", "EVERY", "m4809", "SAMD11"),
NANO_R4("Nano R4", "NANO R4", "RA4M1", "QWIIC"),
// Silkscreened "CONNECT" rather than the full name, which does not fit between the two rows of
// pad names; the RP2040 package right below it carries the part number
NANO_RP2040_CONNECT("Nano RP2040 Connect", "CONNECT", "RP2040", "NINA-W102");
private final String label;
private final String silkLabel;
private final String mcuLabel;
private final String moduleLabel;
NanoVersion(String label, String silkLabel, String mcuLabel, String moduleLabel) {
this.label = label;
this.silkLabel = silkLabel;
this.mcuLabel = mcuLabel;
this.moduleLabel = moduleLabel;
}
public String getSilkLabel() {
return silkLabel;
}
/**
* Marking of the separately visible MCU package, or null when the MCU is a die inside the
* module rather than its own part on the board.
*/
public String getMcuLabel() {
return mcuLabel;
}
/**
* Marking of the part occupying the end of the board opposite the USB jack, where the classic
* Nano has its ICSP header. Null on the classic Nano, which has the header instead.
*/
public String getModuleLabel() {
return moduleLabel;
}
@Override
public String toString() {
return label;
}
}
}
@@ -61,24 +61,6 @@ public class ArduinoUno extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum ArduinoUnoVersion {
LEONARDO("Leonardo"),
REV3("UNO R3"),
R4_MINIMA("UNO R4 Minima"),
R4_WIFI("UNO R4 WiFi");
private String label;
private ArduinoUnoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
private ArduinoUnoVersion version = ArduinoUnoVersion.REV3;
public static Size BOARD_WIDTH = new Size(68.6d, SizeUnit.mm);
@@ -93,13 +75,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// Pin names in sequence (44 pins total)
public static final String[] PIN_NAMES = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, ATmega328P)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP",
// Top-Left ICSP Header (38..43, ATmega16U2)
@@ -130,13 +112,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// ICSP header, and I2C is shared with D2 / D3 rather than living solely on A4 / A5.
public static final String[] PIN_NAMES_LEONARDO = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2 (SDA)", "D3 (~, SCL)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, ATmega32U4)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP"
};
@@ -159,13 +141,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// carries a 10-pin SWD debug connector in the top right corner, modelled as a single node.
public static final String[] PIN_NAMES_R4_MINIMA = new String[] {
// Power Header (0..7)
"BOOT", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"BOOT", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, RA4M1)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP",
// SWD Connector (38)
@@ -222,15 +204,20 @@ public class ArduinoUno extends AbstractMakerBoard {
public void setVersion(ArduinoUnoVersion version) {
this.version = version;
if (isR4()) {
this.bodyColor = ARDUINO_BLUE;
} else {
this.bodyColor = ARDUINO_TEAL;
// The two generations are different colours, but the board follows the version only while its
// colour is still one of ours; a colour the user chose survives a version switch.
if (ARDUINO_TEAL.equals(bodyColor) || ARDUINO_BLUE.equals(bodyColor)) {
this.bodyColor = isR4() ? ARDUINO_BLUE : ARDUINO_TEAL;
}
updateControlPoints();
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.
@@ -442,7 +429,6 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.rotate(orientation.toRadians(), x, y);
}
// Board bounding origin
double boardX = x - new Size(1.1d, SizeUnit.in).convertToPixels();
double boardY = y - new Size(2.0d, SizeUnit.in).convertToPixels();
@@ -450,7 +436,6 @@ public class ArduinoUno extends AbstractMakerBoard {
Composite oldComposite = applyAlpha(g2d, componentState);
// Draw PCB body
drawingObserver.startTracking();
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
@@ -461,7 +446,6 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.draw(boardShape);
if (!outlineMode) {
// Mounting holes
double holeDiameter = new Size(0.12d, SizeUnit.in).convertToPixels();
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter);
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.55d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter);
@@ -544,7 +528,6 @@ public class ArduinoUno extends AbstractMakerBoard {
new Size(0.22d, SizeUnit.in).convertToPixels(), "ATmega328P");
}
// Reset Button near USB
double btnW = BUTTON_WIDTH.convertToPixels();
double btnH = BUTTON_LENGTH.convertToPixels();
double btnX = boardX + new Size(0.235d, SizeUnit.in).convertToPixels() - btnW / 2.0;
@@ -560,11 +543,9 @@ public class ArduinoUno extends AbstractMakerBoard {
double brandShiftY = getVersion() == ArduinoUnoVersion.R4_MINIMA
? new Size(24d, SizeUnit.mm).convertToPixels() : 0;
// Arduino Infinity Logo
MakerBoardLogos.drawArduinoLogo(g2d, boardX + new Size(1.26d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels(),
boardY + new Size(0.4411d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels() + brandShiftY);
// Silkscreen text & branding
g2d.setColor(SILK_COLOR);
double arduinoX = boardX + new Size(1.5d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels();
@@ -577,7 +558,6 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.setFont(SILK_FONT_LARGE);
StringUtils.drawCenteredText(g2d, subText, arduinoX, subTextY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Header silkscreen labels
g2d.setFont(SILK_FONT_SMALL);
// the header captions sit beyond the rotated pin names, which take up the space right next to
// the headers themselves
@@ -606,8 +586,7 @@ public class ArduinoUno extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw header pins with continuity tracking; the Minima's SWD point sits inside the connector
// body drawn above and gets no header pin of its own
// the Minima's SWD point sits inside the connector body drawn above and gets no header pin
int headerPinCount = getVersion() == ArduinoUnoVersion.R4_MINIMA
? controlPoints.length - 1 : controlPoints.length;
drawPinHeader(g2d, 0, headerPinCount, outlineMode, drawingObserver);
@@ -622,38 +601,32 @@ public class ArduinoUno extends AbstractMakerBoard {
double boardW = width - boardX - 2;
double boardH = height - 6;
// USB Type-B Jack (metallic silver, top left protruding)
double usbW = 7;
double usbH = 8;
double usbX = 1;
double usbY = boardY + 2;
// DC Power Jack (dark body, bottom left protruding)
double dcW = 7;
double dcH = 6;
double dcX = 1;
double dcY = boardY + boardH - dcH - 3;
// Board PCB
g2d.setColor(this.bodyColor);
g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
g2d.setColor(this.bodyColor.darker());
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4));
// Draw USB Jack
g2d.setColor(USB_METAL_COLOR);
g2d.fill(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
g2d.setColor(METAL_SHIELD_BORDER);
g2d.draw(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2));
// Draw DC Jack
g2d.setColor(IC_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
g2d.setColor(Color.BLACK);
g2d.draw(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2));
// Arduino Infinity logo
double scale = 15.0 / 95.56;
double logoW = 95.56 * scale;
double logoH = 45.33 * scale;
@@ -661,7 +634,6 @@ public class ArduinoUno extends AbstractMakerBoard {
double logoY = boardY + 3.5;
MakerBoardLogos.drawArduinoLogo(g2d, logoX, logoY, scale);
// UNO text below logo
g2d.setColor(SILK_COLOR);
String iconText = getVersion().toString().toUpperCase(Locale.ROOT).replace("UNO R4 ", "R4 ");
int fontSize = Math.max(5, (int) Math.round(boardH * 0.20));
@@ -670,4 +642,22 @@ public class ArduinoUno extends AbstractMakerBoard {
double textX = boardX + (boardW / 2.0) + 0.5;
StringUtils.drawCenteredText(g2d, iconText, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum ArduinoUnoVersion {
LEONARDO("Leonardo"),
REV3("UNO R3"),
R4_MINIMA("UNO R4 Minima"),
R4_WIFI("UNO R4 WiFi");
private String label;
private ArduinoUnoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -62,25 +62,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum DevKitVersion {
DevKit_V1_30Pin("ESP32 DevKit V1 (30-Pin)"),
DevKitC_V4_38Pin("ESP32 DevKitC V4 (38-Pin)"),
ESP32_S3_DevKitC_44Pin("ESP32-S3 DevKitC-1 (44-Pin)"),
ESP32_C3_DevKitM_1("ESP32-C3 DevKitM-1 (30-Pin)"),
ESP32_C6_DevKitC_1("ESP32-C6 DevKitC-1 (32-Pin)");
private final String label;
DevKitVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
public static Size BOARD_WIDTH_30 = new Size(28.33d, SizeUnit.mm);
public static Size BOARD_LENGTH_30 = new Size(51.45d, SizeUnit.mm);
public static Size TOP_MARGIN_30 = new Size(6.7d, SizeUnit.mm);
@@ -354,6 +335,11 @@ public class ESP32DevKit extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
@EditableProperty(name = "Headers")
public boolean getHeaders() {
return headers;
@@ -373,9 +359,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
/**
* Calculates unrotated relative offsets of all control points for the current version.
*/
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
@@ -522,7 +505,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
// Determine the center X for the current version
double centerX;
if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin || isRiscV()) {
centerX = x + ROW_SPACING_S3.convertToPixels() / 2.0;
@@ -554,7 +536,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setColor(bodyColor);
g2d.fill(new Rectangle2D.Double(mainX, mainY, boardW, boardH));
// Antenna tab in #1e1e1e
g2d.setColor(ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH));
} else if (isRiscV()) {
@@ -572,7 +553,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setColor(bodyColor);
g2d.fill(new Rectangle2D.Double(mainX, mainY, boardW, boardH));
// Antenna tab in #1e1e1e
g2d.setColor(ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH));
} else {
@@ -590,7 +570,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setColor(bodyColor);
g2d.fill(new RoundRectangle2D.Double(mainX, mainY, mainW, mainH, 8, 8));
// Antenna tab in #1e1e1e
g2d.setColor(ANTENNA_BG_COLOR);
g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH));
}
@@ -657,7 +636,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
double btnRightX = rightHoleX - 14 - 0.5 * shift1mm - btnW;
drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "EN", "BOOT");
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "ESP32 DevKit V1", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
@@ -704,7 +682,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
MakerBoardPainter.drawMicroUsb(g2d, usbLeftX, usbY, usbW, usbH, "UART");
MakerBoardPainter.drawMicroUsb(g2d, usbRightX, usbY, usbW, usbH, "USB");
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "ESP32-S3 DevKitC-1", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
@@ -759,7 +736,6 @@ public class ESP32DevKit extends AbstractMakerBoard {
MakerBoardPainter.drawUsbC(g2d, mainX + boardW - usbEdgeDist - usbW / 2.0, usbY, usbW, usbH, "USB");
}
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, isC3 ? "ESP32-C3 DevKitM-1" : "ESP32-C6 DevKitC-1", centerX,
@@ -804,13 +780,11 @@ public class ESP32DevKit extends AbstractMakerBoard {
double btnRightX = mainX + mainW - 38 - btnW;
drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "EN", "BOOT");
// Silkscreen
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
StringUtils.drawCenteredText(g2d, "ESP32 DevKitC V4", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
// Draw pin labels next to control points
drawPinLabels(g2d, x, y, getRelativeOffsets(), SILK_COLOR);
}
@@ -854,4 +828,23 @@ public class ESP32DevKit extends AbstractMakerBoard {
g2d.setFont(ICON_FONT);
StringUtils.drawCenteredText(g2d, "ESP32", width / 2, height / 2 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum DevKitVersion {
DevKit_V1_30Pin("ESP32 DevKit V1 (30-Pin)"),
DevKitC_V4_38Pin("ESP32 DevKitC V4 (38-Pin)"),
ESP32_S3_DevKitC_44Pin("ESP32-S3 DevKitC-1 (44-Pin)"),
ESP32_C3_DevKitM_1("ESP32-C3 DevKitM-1 (30-Pin)"),
ESP32_C6_DevKitC_1("ESP32-C6 DevKitC-1 (32-Pin)");
private final String label;
DevKitVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -83,23 +83,6 @@ public class RaspberryPi extends AbstractMakerBoard {
"PCIe", "MIPI 1", "MIPI 0", "UART"
};
public enum RaspberryPiVersion {
PI_3_B("Pi 3 (Model B+)"),
PI_4_B("Pi 4 (Model B)"),
PI_5("Pi 5");
private String label;
private RaspberryPiVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
private RaspberryPiVersion version = RaspberryPiVersion.PI_5;
@org.diylc.core.annotations.EditableProperty(name = "Version")
@@ -113,6 +96,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;
@@ -258,7 +247,6 @@ public class RaspberryPi extends AbstractMakerBoard {
MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(61.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(52.5d, SizeUnit.mm).convertToPixels(), holeDiameter);
// USB & Ethernet Ports on the right edge
double topDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 45.75d : 47.0d;
double midDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 27.0d : 29.0d;
double botDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 9.0d : 10.25d;
@@ -270,25 +258,21 @@ public class RaspberryPi extends AbstractMakerBoard {
double ethernetCenterY = (version == RaspberryPiVersion.PI_4_B) ? topCenterY : bottomCenterY;
double topUsbCenterY = (version == RaspberryPiVersion.PI_4_B) ? bottomCenterY : topCenterY;
// Top USB slot (USB 3.0)
MakerBoardPainter.drawUsbA(g2d, boardX + boardW - new Size(14.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(topUsbCenterY - 7.0d, SizeUnit.mm).convertToPixels(),
USB_A_DUAL_LENGTH.convertToPixels(),
new Size(14.0d, SizeUnit.mm).convertToPixels(), "USB 3.0");
// USB 2.0 (middle)
MakerBoardPainter.drawUsbA(g2d, boardX + boardW - new Size(14.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(middleCenterY - 7.0d, SizeUnit.mm).convertToPixels(),
USB_A_DUAL_LENGTH.convertToPixels(),
new Size(14.0d, SizeUnit.mm).convertToPixels(), "USB 2.0");
// Ethernet
MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - new Size(18.0d, SizeUnit.mm).convertToPixels(),
boardY + new Size(ethernetCenterY - 8.0d, SizeUnit.mm).convertToPixels(),
new Size(21.0d, SizeUnit.mm).convertToPixels(),
new Size(16.0d, SizeUnit.mm).convertToPixels(), "ETHERNET");
// Broadcom SoC
double socW;
double socH;
double socX;
@@ -492,7 +476,6 @@ public class RaspberryPi extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "GPIO", boardX + new Size(32.5d, SizeUnit.mm).convertToPixels(),
boardY + new Size(7.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Raspberry Pi Silkscreen text below 40-pin header
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT_TITLE);
String versionLabel = version.toString();
@@ -511,7 +494,6 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw 40 GPIO header pins and 4 PoE header pins
drawPinHeader(g2d, 0, 44, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
@@ -524,7 +506,6 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.setColor(RPI_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 4, 4));
// USB & Ethernet (Top USB 3.0, Middle USB 2.0, Bottom Ethernet)
g2d.setColor(USB_METAL_COLOR);
if (version == RaspberryPiVersion.PI_4_B) {
g2d.fillRect(width - 9, 5, 7, 6);
@@ -536,7 +517,6 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.fillRect(width - 9, 20, 7, 6);
}
// SoC
g2d.setColor(IC_BODY_COLOR);
if (version == RaspberryPiVersion.PI_5) {
g2d.fillRect(13, 14, 7, 7);
@@ -546,11 +526,9 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.fillRect(10, 11, 5, 5);
}
// GPIO Header
g2d.setColor(HEADER_BODY_COLOR);
g2d.fillRect(4, 5, 18, 3);
// PCIe & MIPI connectors & PoE
g2d.setColor(IC_BODY_COLOR);
if (version == RaspberryPiVersion.PI_5) {
g2d.setColor(IC_BODY_COLOR);
@@ -575,4 +553,21 @@ public class RaspberryPi extends AbstractMakerBoard {
g2d.setFont(ICON_FONT_LARGE);
StringUtils.drawCenteredText(g2d, "RPi", 14, 25, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum RaspberryPiVersion {
PI_3_B("Pi 3 (Model B+)"),
PI_4_B("Pi 4 (Model B)"),
PI_5("Pi 5");
private String label;
private RaspberryPiVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -61,24 +61,6 @@ public class RaspberryPiPico extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum PicoVersion {
PICO("Pi Pico"),
PICO_W("Pi Pico W"),
PICO_2("Pi Pico 2"),
PICO_2_W("Pi Pico 2 W");
private final String label;
PicoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
public static Size BOARD_WIDTH = new Size(21.0d, SizeUnit.mm);
public static Size BOARD_LENGTH = new Size(51.0d, SizeUnit.mm);
@@ -107,11 +89,11 @@ public class RaspberryPiPico extends AbstractMakerBoard {
public static final String[] PIN_NAMES = new String[] {
// Left row (pins 0..19)
"GP0", "GP1", "GND1", "GP2", "GP3", "GP4", "GP5", "GND2", "GP6", "GP7",
"GP8", "GP9", "GND3", "GP10", "GP11", "GP12", "GP13", "GND4", "GP14", "GP15",
"GP0", "GP1", "GND_1", "GP2", "GP3", "GP4", "GP5", "GND_2", "GP6", "GP7",
"GP8", "GP9", "GND_3", "GP10", "GP11", "GP12", "GP13", "GND_4", "GP14", "GP15",
// Right row (pins 20..39)
"VBUS", "VSYS", "GND5", "3V3_EN", "3V3 (OUT)", "ADC_VREF", "GP28", "GND6", "GP27", "GP26",
"RUN", "GP22", "GND7", "GP21", "GP20", "GP19", "GP18", "GND8", "GP17", "GP16",
"VBUS", "VSYS", "GND_5", "3V3_EN", "3V3 (OUT)", "ADC_VREF", "GP28", "GND_6", "GP27", "GP26",
"RUN", "GP22", "GND_7", "GP21", "GP20", "GP19", "GP18", "GND_8", "GP17", "GP16",
// SWD debug pins (pins 40..42)
"SWCLK", "GND_SWD", "SWDIO"
};
@@ -139,6 +121,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.
@@ -290,7 +277,6 @@ public class RaspberryPiPico extends AbstractMakerBoard {
MakerBoardPainter.drawMountingHole(g2d, holeCenterX - holeDistX, bottomHoleY, holeDiameter);
MakerBoardPainter.drawMountingHole(g2d, holeCenterX + holeDistX, bottomHoleY, holeDiameter);
// Micro USB Connector
double usbW = USB_MICRO_WIDTH.convertToPixels();
double usbH = USB_MICRO_LENGTH.convertToPixels();
double usbOverhang = new Size(1.3d, SizeUnit.mm).convertToPixels();
@@ -405,10 +391,6 @@ public class RaspberryPiPico extends AbstractMakerBoard {
return NOTCH_SIZE;
}
/**
* Helper to draw Raspberry Pi Pico's castellated solder pads along the left and right edges
* and the SWD round pads.
*/
protected void drawCastellatedPads(Graphics2D g2d, double boardX, double boardY, double boardW, double boardH,
double pin1OffsetX, double pin1OffsetY, boolean outlineMode, IDrawingObserver drawingObserver) {
if (outlineMode) return;
@@ -485,14 +467,12 @@ public class RaspberryPiPico extends AbstractMakerBoard {
g2d.setColor(RPI_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(6, 2, width - 12, height - 4, 3, 3));
// Castellated edge pads on left and right in icon
g2d.setColor(PAD_COLOR);
for (int y = 5; y <= height - 6; y += 3) {
g2d.fillRect(6, y, 3, 2);
g2d.fillRect(width - 9, y, 3, 2);
}
// USB Connector
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(11, 2, 10, 3);
@@ -504,4 +484,22 @@ public class RaspberryPiPico extends AbstractMakerBoard {
g2d.setFont(ICON_FONT);
StringUtils.drawCenteredText(g2d, "PICO", width / 2, height / 2 + 10, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum PicoVersion {
PICO("Pi Pico"),
PICO_W("Pi Pico W"),
PICO_2("Pi Pico 2"),
PICO_2_W("Pi Pico 2 W");
private final String label;
PicoVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -75,23 +75,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
PIN_NAMES[44] = "TV 2";
}
public enum ZeroVersion {
PI_ZERO("Pi Zero"),
PI_ZERO_W("Pi Zero W"),
PI_ZERO_2_W("Pi Zero 2 W");
private final String label;
ZeroVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
protected ZeroVersion version = ZeroVersion.PI_ZERO;
protected boolean headers = false;
@@ -115,6 +98,11 @@ public class RaspberryPiZero extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
@EditableProperty(name = "Headers")
public boolean getHeaders() {
return headers;
@@ -288,7 +276,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
double socY = boardY + new Size(11.5d, SizeUnit.mm).convertToPixels();
MakerBoardPainter.drawChip(g2d, socX, socY, socW, socH, "");
// Raspberry Pi logo on SoC chip
double logoSize = new Size(7.0d, SizeUnit.mm).convertToPixels();
double logoW = logoSize * (72.51 / 92.604);
double logoX = socX + (socW - logoW) / 2.0;
@@ -324,7 +311,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "GPIO", boardX + new Size(42.5d, SizeUnit.mm).convertToPixels(),
y + PIN_SPACING.convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen for RUN and TV pads
double spacing = PIN_SPACING.convertToPixels();
double runBoxX = x + 18 * spacing - spacing / 2.0;
double runBoxY = y + 1 * spacing - spacing / 2.0;
@@ -357,9 +343,6 @@ public class RaspberryPiZero extends AbstractMakerBoard {
g2d.setComposite(oldComposite);
}
/**
* Helper to draw GPIO solder pads (copper pads with drill holes, square for Pin 1).
*/
protected void drawSolderPads(Graphics2D g2d, boolean outlineMode, IDrawingObserver drawingObserver) {
drawPcbSolderPads(g2d, 0, 40, true, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 41, 2, true, outlineMode, drawingObserver);
@@ -374,17 +357,14 @@ public class RaspberryPiZero extends AbstractMakerBoard {
g2d.setColor(RPI_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3));
// GPIO Solder Pads in icon
g2d.setColor(PAD_COLOR);
for (int x = 6; x <= width - 8; x += 3) {
g2d.fillRect(x, 7, 2, 2);
}
// SoC
g2d.setColor(IC_BODY_COLOR);
g2d.fillRect(10, 12, 6, 6);
// Connectors on bottom
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(6, height - 7, 4, 3);
g2d.fillRect(18, height - 7, 3, 3);
@@ -394,4 +374,21 @@ public class RaspberryPiZero extends AbstractMakerBoard {
g2d.setFont(ICON_FONT);
StringUtils.drawCenteredText(g2d, "ZERO", width / 2 + 4, 21, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum ZeroVersion {
PI_ZERO("Pi Zero"),
PI_ZERO_W("Pi Zero W"),
PI_ZERO_2_W("Pi Zero 2 W");
private final String label;
ZeroVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -58,23 +58,6 @@ public class Teensy extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum TeensyVersion {
Teensy_3_2("Teensy 3.2"),
Teensy_4_0("Teensy 4.0"),
Teensy_4_1("Teensy 4.1");
private final String label;
TeensyVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
// gap between the Ethernet header pads and the silkscreen box printed around them
public static Size ETH_BOX_MARGIN = new Size(1.2d, SizeUnit.mm);
@@ -106,15 +89,15 @@ public class Teensy extends AbstractMakerBoard {
// VUSB (33, near VIN): VUSB
public static final String[] PIN_NAMES_40 = new String[] {
// Left row (0..13)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"GND_1", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)",
// Right row (14..27)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"VIN (3.6-5.5V)", "GND_2", "3.3V_1 (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/CRX1/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "On/Off",
"VBAT", "3.3V_2 (End)", "GND_3 (End)", "Program", "On/Off",
// VUSB (33)
"VUSB"
};
@@ -142,19 +125,19 @@ public class Teensy extends AbstractMakerBoard {
// VUSB (64, near VIN): VUSB
public static final String[] PIN_NAMES_41 = new String[] {
// Left row (0..23)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"GND_1", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V_1", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"26 (A12/MOSI1)", "27 (A13/SCK1)", "28 (RX7)", "29 (TX7)", "30 (CRX3)", "31 (CTX3)",
"32 (OUT1B)",
// Right row (24..47)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"VIN (3.6-5.5V)", "GND_2", "3.3V_2 (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL)", "18 (A4/SDA)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND", "41 (A17)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND_3", "41 (A17)",
"40 (A16)", "39 (A15/MISO1)", "38 (A14/CS1)", "37 (CS)", "36 (CS)", "35 (TX8)",
"34 (RX8)", "33 (MCLK2)",
// Middle cluster (48..52, left to right near push button)
"VBAT", "3.3V (Mid)", "GND (Mid)", "Program", "On/Off",
"VBAT", "3.3V_3 (Mid)", "GND_4 (Mid)", "Program", "On/Off",
// Ethernet header (53..58, 3x2)
"ETH_TX-", "ETH_LED", "ETH_RX+", "ETH_TX+", "ETH_GND", "ETH_RX-",
// USB Host header (59..63, 1x5)
@@ -177,14 +160,14 @@ public class Teensy extends AbstractMakerBoard {
// severable VUSB pad next to Vin. Labels follow the PJRC Teensy 3.2 pinout card.
public static final String[] PIN_NAMES_32 = new String[] {
// Left row (0..13)
"GND", "0 (RX1)", "1 (TX1)", "2", "3 (PWM)", "4 (PWM)", "5 (PWM)", "6 (PWM)", "7 (RX3)",
"GND_1", "0 (RX1)", "1 (TX1)", "2", "3 (PWM)", "4 (PWM)", "5 (PWM)", "6 (PWM)", "7 (RX3)",
"8 (TX3)", "9 (RX2/PWM)", "10 (TX2/CS/PWM)", "11 (MOSI)", "12 (MISO)",
// Right row (14..27)
"Vin (3.6-6.0V)", "AGND", "3.3V (100mA)", "23 (A9/PWM)", "22 (A8/PWM)", "21 (A7/PWM)",
"Vin (3.6-6.0V)", "AGND", "3.3V_1 (100mA)", "23 (A9/PWM)", "22 (A8/PWM)", "21 (A7/PWM)",
"20 (A6/PWM)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3)", "16 (A2)", "15 (A1/CS)",
"14 (A0)", "13 (SCK/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "A14/DAC",
"VBAT", "3.3V_2 (End)", "GND_2 (End)", "Program", "A14/DAC",
// VUSB (33)
"VUSB"
};
@@ -238,6 +221,11 @@ public class Teensy extends AbstractMakerBoard {
invalidateCache();
}
@Override
protected String getVariantLabel() {
return getVersion().toString();
}
@EditableProperty(name = "Headers")
public boolean getHeaders() {
return headers;
@@ -415,11 +403,9 @@ public class Teensy extends AbstractMakerBoard {
drawingObserver.startTracking();
// Fill board body (green PCB)
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
// Board outline
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
@@ -453,7 +439,6 @@ public class Teensy extends AbstractMakerBoard {
MakerBoardPainter.drawChip(g2d, chipX, chipY, chipW, chipH,
getVersion() == TeensyVersion.Teensy_3_2 ? "MK20DX256" : "iMXRT1062");
// Pushbutton (Program button)
double btnW = BUTTON_WIDTH.convertToPixels();
double btnH = BUTTON_LENGTH.convertToPixels();
double btnX = centerX - btnW / 2.0;
@@ -462,9 +447,9 @@ public class Teensy extends AbstractMakerBoard {
: boardY + new Size(35.5d, SizeUnit.mm).convertToPixels() + new Size(0.25d, SizeUnit.in).convertToPixels();
MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH);
// Teensy 4.1 extras: Ethernet PHY + SD card slot
// the 4.1's Ethernet PHY and SD card slot
if (getVersion() == TeensyVersion.Teensy_4_1) {
// MicroSD card slot at bottom edge (12mm long, flush with bottom edge so it does not stick out)
// flush with the bottom edge so it does not stick out
double sdW = new Size(12.0d, SizeUnit.mm).convertToPixels();
double sdH = new Size(12.0d, SizeUnit.mm).convertToPixels();
double sdX = centerX - sdW / 2.0;
@@ -494,7 +479,6 @@ public class Teensy extends AbstractMakerBoard {
g2d.draw(new Rectangle2D.Double(usbBoxX, usbBoxY, usbBoxW, usbBoxW));
}
// Silkscreen "TEENSY" label
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
String silkText = getVersion().toString();
@@ -516,7 +500,6 @@ public class Teensy extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Render pin headers or gold solder pads with drill holes
if (headers) {
drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
} else {
@@ -526,9 +509,6 @@ public class Teensy extends AbstractMakerBoard {
g2d.setComposite(oldComposite);
}
/**
* Helper to draw GPIO solder pads (copper/gold pads with drill holes, square for Pin 1).
*/
protected void drawSolderPads(Graphics2D g2d, boolean outlineMode, IDrawingObserver drawingObserver) {
drawPcbSolderPads(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver);
}
@@ -540,18 +520,15 @@ public class Teensy extends AbstractMakerBoard {
g2d.setColor(TEENSY_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3));
// GPIO Solder Pads in icon
g2d.setColor(PAD_COLOR);
for (int y = 5; y <= height - 6; y += 3) {
g2d.fillRect(6, y, 2, 2);
g2d.fillRect(width - 8, y, 2, 2);
}
// MCU chip
g2d.setColor(IC_BODY_COLOR);
g2d.fillRect(10, 8, 12, 8);
// USB connector at top
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(12, 2, 8, 3);
@@ -560,4 +537,21 @@ public class Teensy extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "TEENSY", width / 2, height / 2 + 8,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
public enum TeensyVersion {
Teensy_3_2("Teensy 3.2"),
Teensy_4_0("Teensy 4.0"),
Teensy_4_1("Teensy 4.1");
private final String label;
TeensyVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
}
@@ -59,6 +59,8 @@ public class WemosD1Mini extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
private static final int PINS_PER_ROW = 8;
public static Size BOARD_WIDTH = new Size(1.0d, SizeUnit.in);
public static Size BOARD_LENGTH = new Size(1.34d, SizeUnit.in);
public static Size ROW_SPACING = new Size(0.9d, SizeUnit.in);
@@ -92,14 +94,6 @@ public class WemosD1Mini extends AbstractMakerBoard {
"5V", "G (GND)", "D4 (GPIO2)", "D3 (GPIO0)", "D2 (GPIO4)", "D1 (GPIO5)", "RX (GPIO3)", "TX (GPIO1)"
};
public static final String[] SILK_PIN_NAMES_LEFT = new String[] {
"RST", "A0", "D0", "D5", "D6", "D7", "D8", "3V3"
};
public static final String[] SILK_PIN_NAMES_RIGHT = new String[] {
"TX", "RX", "D1", "D2", "D3", "D4", "G", "5V"
};
protected boolean headers = false;
public WemosD1Mini() {
@@ -126,23 +120,28 @@ public class WemosD1Mini extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double[][] relativeOffsets = new double[16][2];
for (int i = 0; i < 8; i++) {
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
// Left row (pins 0..7, top to bottom)
for (int i = 0; i < PINS_PER_ROW; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
}
for (int i = 0; i < 8; i++) {
relativeOffsets[8 + i][0] = rowSpacing;
relativeOffsets[8 + i][1] = (7 - i) * spacing;
// Right row (pins 8..15, bottom to top, which is the order the board numbers them in)
for (int i = 0; i < PINS_PER_ROW; i++) {
relativeOffsets[PINS_PER_ROW + i][0] = rowSpacing;
relativeOffsets[PINS_PER_ROW + i][1] = (PINS_PER_ROW - 1 - i) * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
@@ -287,18 +286,19 @@ public class WemosD1Mini extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "D1 mini", x + rowSpacing / 2.0, labelY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen: Pin labels
// Silkscreen: pin names, lying flat beside their row as the board prints them rather than
// standing on end like drawPinLabels' -- the 0.9" row spacing leaves room for them. Both the
// text and the position come from the control point, so the right row's bottom-to-top order
// is followed rather than mirrored by a second array kept in step by hand.
g2d.setFont(SILK_FONT_TINY);
double spacing = PIN_SPACING.convertToPixels();
double labelOffset = new Size(1.4d, SizeUnit.mm).convertToPixels();
for (int i = 0; i < 8; i++) {
double pinY = y + i * spacing;
// Left pin labels
StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_LEFT[i], x + labelOffset, pinY,
HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
// Right pin labels
StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_RIGHT[i], x + rowSpacing - labelOffset, pinY,
HorizontalAlignment.RIGHT, VerticalAlignment.CENTER);
double[][] offsets = getRelativeOffsets();
for (int i = 0; i < offsets.length; i++) {
boolean leftRow = i < PINS_PER_ROW;
StringUtils.drawCenteredText(g2d, getSilkPinLabel(i),
x + offsets[i][0] + (leftRow ? labelOffset : -labelOffset), y + offsets[i][1],
leftRow ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
}
}
@@ -216,7 +216,7 @@ public class MT3608BoostConverter extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw square/rounded rectangular tinned solder pads using LIGHT_METAL_COLOR
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -188,7 +188,7 @@ public class TP4056Charger extends AbstractMakerBoard {
g2d.setTransform(oldTx);
// Draw tinned solder pads
drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver);
drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
@@ -88,7 +88,10 @@ public class DHTSensor extends AbstractMakerBoard {
public void setModel(DHTModel model) {
this.model = model;
this.bodyColor = (model == DHTModel.DHT11) ? DHT11_BLUE : DHT22_WHITE;
// Follows the model only while the colour is still a default of ours; see ArduinoUno.
if (DHT11_BLUE.equals(bodyColor) || DHT22_WHITE.equals(bodyColor)) {
this.bodyColor = model == DHTModel.DHT11 ? DHT11_BLUE : DHT22_WHITE;
}
invalidateCache();
}
@@ -0,0 +1,129 @@
/*
*
* 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.transform;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.displays.LEDBarGraph;
import org.diylc.core.IDIYComponent;
/**
* Rotation and mirroring for {@link LEDBarGraph}, modelled on {@link SIL_ICTransformer}. A sibling
* rather than a reuse of {@link DIL_ICTransformer}: that one guards on
* {@code getClass().equals(DIL_IC.class)} and casts to {@code DIL_IC}, which is the convention
* across this package, so it would refuse a bar graph rather than rotate it.
*
* @author Branislav Stojkovic
*/
public class LEDBarGraphTransformer implements IComponentTransformer {
@Override
public boolean canRotate(IDIYComponent<?> component) {
return component.getClass().equals(LEDBarGraph.class);
}
@Override
public boolean canMirror(IDIYComponent<?> component) {
return component.getClass().equals(LEDBarGraph.class);
}
/**
* Mirroring reverses which end carries the first segment, so it reorders the pins without
* changing what any of them is connected to. That is unlike a DIP IC, where the function of a
* given pin number moves, which is why this returns false where {@code DIL_ICTransformer} does
* not.
*/
@Override
public boolean mirroringChangesCircuit() {
return false;
}
@Override
public void rotate(IDIYComponent<?> component, Point2D center, int direction) {
AffineTransform rotate =
AffineTransform.getRotateInstance(Math.PI / 2 * direction, center.getX(), center.getY());
for (int index = 0; index < component.getControlPointCount(); index++) {
Point2D p = new Point2D.Double();
rotate.transform(component.getControlPoint(index), p);
component.setControlPoint(p, index);
}
LEDBarGraph bar = (LEDBarGraph) component;
Orientation o = bar.getOrientation();
int oValue = o.ordinal();
oValue += direction;
if (oValue < 0) {
oValue = Orientation.values().length - 1;
}
if (oValue >= Orientation.values().length) {
oValue = 0;
}
bar.setOrientation(Orientation.values()[oValue]);
}
@SuppressWarnings("incomplete-switch")
@Override
public void mirror(IDIYComponent<?> component, Point2D center, int direction) {
LEDBarGraph bar = (LEDBarGraph) component;
double dx = center.getX() - bar.getControlPoint(0).getX();
double dy = center.getY() - bar.getControlPoint(0).getY();
// The orientation has to turn with the points: the body is drawn from it, so reflecting the
// pins alone would leave the package facing the way it did before.
if (direction == IComponentTransformer.HORIZONTAL) {
Orientation o = bar.getOrientation();
switch (o) {
case _90:
o = Orientation._270;
break;
case _270:
o = Orientation._90;
}
for (int i = 0; i < bar.getControlPointCount(); i++) {
Point2D p = bar.getControlPoint(i);
bar.setControlPoint(new Point2D.Double(p.getX() + 2 * dx, p.getY()), i);
}
bar.setOrientation(o);
} else {
Orientation o = bar.getOrientation();
switch (o) {
case DEFAULT:
o = Orientation._180;
break;
case _180:
o = Orientation.DEFAULT;
}
for (int i = 0; i < bar.getControlPointCount(); i++) {
Point2D p = bar.getControlPoint(i);
bar.setControlPoint(new Point2D.Double(p.getX(), p.getY() + 2 * dy), i);
}
bar.setOrientation(o);
}
}
}
@@ -74,7 +74,7 @@ public class Jumbo4PinTubeSocket extends AbstractAngledComponent<String> {
public static Size PIN_DIAMETER = new Size(1d, SizeUnit.mm);
public static Size HOLE_SIZE = new Size(5d, SizeUnit.mm);
public static Size OCTAL_TICK_SIZE = new Size(2d, SizeUnit.mm);
private static String[] electrodeLabels = new String[] {"G", "F", "P", "F"};
private static String[] electrodeLabels = new String[] {"G", "F1", "P", "F2"};
private String type = "";
@@ -0,0 +1,138 @@
/*
*
* 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;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.PropertyWrapper;
import org.diylc.core.IDIYComponent;
import org.diylc.core.Template;
import org.diylc.presenter.ComponentProcessor;
import org.diylc.serialization.ProjectFileManager;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Checks every factory variant shipped in {@code import-defaults/variants.xml} against the class it
* configures. Nothing else does, and the failure is silent: a template names its properties as
* plain strings and its enum constants through an XStream package alias, so a renamed property, a
* renamed constant or a mistyped alias gives a variant that loads and then sets nothing. The user
* picks it from the palette and gets a default component.
*
* <p>This goes through {@link ComponentProcessor#extractProperties} and {@link PropertyWrapper},
* which is exactly what {@code InstantiationManager} does when it applies a template, so a variant
* that passes here is one the application can really apply. That matters more than it sounds:
* matching properties by hand gets autoboxing and inherited accessors subtly wrong, and would
* report drift that is not there.
*/
public class FactoryVariantsTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
@SuppressWarnings("unchecked")
private static Map<String, List<Template>> loadVariants() {
InputStream in = FactoryVariantsTest.class.getClassLoader()
.getResourceAsStream("import-defaults/variants.xml");
Assert.assertNotNull("variants.xml is not on the classpath", in);
return (Map<String, List<Template>>) ProjectFileManager.xStreamSerializer.fromXML(in);
}
/**
* Problems are collected rather than asserted one at a time, so a run reports every stale
* template at once instead of stopping at the first.
*/
@Test
public void everyVariantNamesLivePropertiesAndApplies() throws Exception {
List<String> problems = new ArrayList<String>();
int checked = 0;
for (Map.Entry<String, List<Template>> entry : loadVariants().entrySet()) {
Class<?> type;
try {
type = Class.forName(entry.getKey());
} catch (ClassNotFoundException e) {
problems.add(entry.getKey() + ": no such class");
continue;
}
Map<String, PropertyWrapper> properties = new HashMap<String, PropertyWrapper>();
for (PropertyWrapper property : ComponentProcessor.getInstance().extractProperties(type)) {
properties.put(property.getName(), property);
}
for (Template variant : entry.getValue()) {
String where = type.getSimpleName() + " / " + variant.getName();
if (variant.getValues() == null || variant.getValues().isEmpty()) {
problems.add(where + ": sets nothing");
continue;
}
checked++;
for (Map.Entry<String, Object> value : variant.getValues().entrySet()) {
PropertyWrapper property = properties.get(value.getKey());
if (property == null) {
problems.add(where + ": no property named \"" + value.getKey() + "\"");
continue;
}
if (value.getValue() == null) {
continue;
}
IDIYComponent<?> component =
(IDIYComponent<?>) type.getDeclaredConstructor().newInstance();
PropertyWrapper applied = (PropertyWrapper) property.clone();
applied.setValue(value.getValue());
try {
applied.writeTo(component);
} catch (Exception e) {
problems.add(where + ": \"" + value.getKey() + "\" will not apply -- "
+ e.getClass().getSimpleName() + " writing a "
+ value.getValue().getClass().getSimpleName() + " to a "
+ property.getType().getSimpleName());
continue;
}
applied.readFrom(component);
if (!value.getValue().equals(applied.getValue())) {
problems.add(where + ": \"" + value.getKey() + "\" set to " + value.getValue()
+ " but reads back " + applied.getValue());
}
}
}
}
Assert.assertTrue("no variants were found at all", checked > 0);
Assert.assertEquals(problems.size() + " stale factory variant value(s):\n "
+ String.join("\n ", problems) + "\n", 0, problems.size());
}
}
@@ -0,0 +1,106 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.Display;
import org.diylc.components.displays.CharacterLCD.LCDInterface;
import org.diylc.components.displays.CharacterLCD.LCDSize;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* What the character LCD prints on its screen. The size and the interface are the whole of what a
* buyer chooses between and neither is legible from the drawing, so the lit area carries them.
*/
public class CharacterLCDTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static CharacterLCD of(LCDSize lcdSize, LCDInterface lcdInterface) {
CharacterLCD display = new CharacterLCD();
display.setLcdSize(lcdSize);
display.setLcdInterface(lcdInterface);
return display;
}
@Test
public void everyScreenPrintsItsSizeAndInterface() {
for (LCDSize lcdSize : LCDSize.values()) {
for (LCDInterface lcdInterface : LCDInterface.values()) {
CharacterLCD printed = of(lcdSize, lcdInterface);
CharacterLCD blank = of(lcdSize, lcdInterface);
blank.setScreen(Display.NONE);
Assert.assertEquals(Display.VALUE, printed.getScreen());
Assert.assertEquals(lcdSize + ", " + lcdInterface, printed.getValueForDisplay());
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
}
}
}
/**
* Both inks exist on real modules, so the text has to survive a screen colour the user picks. Two
* backlights at opposite ends of the range must not render the same, or one is printing its text
* in the backlight's own colour.
*/
@Test
public void theInkFollowsTheBacklight() {
CharacterLCD dark = new CharacterLCD();
dark.setScreenColor(Color.decode("#1E88E5"));
CharacterLCD pale = new CharacterLCD();
pale.setScreenColor(Color.decode("#CDDC39"));
CharacterLCD darkBlank = new CharacterLCD();
darkBlank.setScreenColor(Color.decode("#1E88E5"));
darkBlank.setScreen(Display.NONE);
CharacterLCD paleBlank = new CharacterLCD();
paleBlank.setScreenColor(Color.decode("#CDDC39"));
paleBlank.setScreen(Display.NONE);
MakerBoardTestSupport.assertScreenTextIsDrawn(dark, darkBlank);
MakerBoardTestSupport.assertScreenTextIsDrawn(pale, paleBlank);
}
@Test
public void drawsCleanlyInEveryState() {
for (LCDSize lcdSize : LCDSize.values()) {
for (LCDInterface lcdInterface : LCDInterface.values()) {
for (Display screen : Display.values()) {
CharacterLCD display = of(lcdSize, lcdInterface);
display.setScreen(screen);
MakerBoardTestSupport.assertDrawsCleanly(display);
}
}
}
}
}
@@ -0,0 +1,412 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.IComponentTransformer;
import org.diylc.common.Orientation;
import org.diylc.components.displays.LEDBarGraph.BarColor;
import org.diylc.components.displays.LEDBarGraph.Segments;
import org.diylc.components.transform.LEDBarGraphTransformer;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry and transform tests for the bar graph. Deliberately not in
* {@code MakerComponentsTest}: that suite covers maker boards, and this is a DIP-outline part on
* a different base class.
*/
public class LEDBarGraphTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
/** Each segment has its own anode and cathode, so the package grows with the count. */
@Test
public void pinCountIsTwicePerSegment() {
Assert.assertEquals(16, bar(Segments._8).getControlPointCount());
Assert.assertEquals(20, bar(Segments._10).getControlPointCount());
Assert.assertEquals(24, bar(Segments._12).getControlPointCount());
}
/** One pin pitch of package per segment, so the body lengthens as segments are added. */
@Test
public void packageLengthensWithSegmentCount() {
double pitch = LEDBarGraph.PIN_SPACING.convertToPixels();
for (Segments segments : Segments.values()) {
Rectangle2D body = bar(segments).getBody().getBounds2D();
Assert.assertEquals("length for " + segments, segments.getCount() * pitch, body.getHeight(),
0.5d);
}
}
/** The width is the package's own and does not vary with the count. */
@Test
public void packageWidthIsTheSameForEveryCount() {
double expected = LEDBarGraph.PACKAGE_WIDTH.convertToPixels();
for (Segments segments : Segments.values()) {
Assert.assertEquals("width for " + segments, expected,
bar(segments).getBody().getBounds2D().getWidth(), 0.5d);
}
}
@Test
public void pinsFormTwoRowsAtTheRightPitch() {
LEDBarGraph graph = bar(Segments._10);
double pitch = LEDBarGraph.PIN_SPACING.convertToPixels();
double rowSpacing = LEDBarGraph.ROW_SPACING.convertToPixels();
Point2D first = graph.getControlPoint(0);
Assert.assertEquals("pitch along the near row", pitch,
graph.getControlPoint(1).getY() - first.getY(), 0.5d);
Assert.assertEquals("near row stays on one line", first.getX(),
graph.getControlPoint(9).getX(), 0.5d);
Assert.assertEquals("row spacing", rowSpacing,
graph.getControlPoint(10).getX() - first.getX(), 0.5d);
}
/**
* The defining difference from a DIP IC: its leads leave the sides of the plastic, so its body
* sits between the rows. A bar graph's pins leave the underside, so the package is wider than the
* row spacing and every pin falls beneath it.
*/
@Test
public void bodyCoversEveryPin() {
double pinSize = (int) LEDBarGraph.PIN_SIZE.convertToPixels() / 2 * 2;
for (Orientation orientation : Orientation.values()) {
for (Segments segments : Segments.values()) {
LEDBarGraph graph = new LEDBarGraph();
graph.setOrientation(orientation);
graph.setSegments(segments);
Rectangle2D body = graph.getBody().getBounds2D();
for (int i = 0; i < graph.getControlPointCount(); i++) {
Point2D pin = graph.getControlPoint(i);
Rectangle2D pinRect = new Rectangle2D.Double(pin.getX() - pinSize / 2,
pin.getY() - pinSize / 2, pinSize, pinSize);
Assert.assertTrue("pin " + i + " is not covered at " + orientation + " " + segments,
body.contains(pinRect));
}
}
}
}
/** DIP numbering: down one side and back up the other, whatever the pin count. */
@Test
public void pinsAreNumberedRoundThePackage() {
for (Segments segments : Segments.values()) {
LEDBarGraph graph = bar(segments);
int pins = segments.getPinCount();
int half = pins / 2;
Assert.assertEquals("first pin, " + segments, "1", graph.getControlPointNodeName(0));
Assert.assertEquals("last of near row, " + segments, Integer.toString(half),
graph.getControlPointNodeName(half - 1));
Assert.assertEquals("first of far row, " + segments, Integer.toString(pins),
graph.getControlPointNodeName(half));
Assert.assertEquals("last pin, " + segments, Integer.toString(half + 1),
graph.getControlPointNodeName(pins - 1));
}
}
/**
* The segment field is derived from the body, so an orientation the derivation does not handle
* shows up here rather than in a drawing nobody checks. {@code getSegmentRects} is private, so
* this reaches it reflectively rather than widening the class's surface for a test.
*/
@Test
public void segmentsStayInsideTheBody() throws Exception {
java.lang.reflect.Method segmentRects = LEDBarGraph.class.getDeclaredMethod("getSegmentRects");
segmentRects.setAccessible(true);
for (Orientation orientation : Orientation.values()) {
for (Segments segments : Segments.values()) {
LEDBarGraph graph = new LEDBarGraph();
graph.setOrientation(orientation);
graph.setSegments(segments);
Rectangle2D body = graph.getBody().getBounds2D();
Rectangle2D[] rects = (Rectangle2D[]) segmentRects.invoke(graph);
Assert.assertEquals("segment count at " + orientation + " " + segments,
segments.getCount(), rects.length);
for (int i = 0; i < rects.length; i++) {
Assert.assertTrue("segment " + i + " escapes the body at " + orientation + " " + segments,
body.contains(rects[i]));
Assert.assertTrue("segment " + i + " has no area at " + orientation + " " + segments,
rects[i].getWidth() > 0 && rects[i].getHeight() > 0);
}
}
}
}
/**
* Not implied by {@link #segmentsStayInsideTheBody()}: dividing the body into equal bars leaves
* every segment inside the package and still about 5 px off its own pins on a ten segment part.
*/
@Test
public void segmentsAreCentredOnTheirPins() throws Exception {
java.lang.reflect.Method segmentRects = LEDBarGraph.class.getDeclaredMethod("getSegmentRects");
segmentRects.setAccessible(true);
for (Orientation orientation : Orientation.values()) {
for (Segments segments : Segments.values()) {
LEDBarGraph graph = new LEDBarGraph();
graph.setOrientation(orientation);
graph.setSegments(segments);
Rectangle2D[] rects = (Rectangle2D[]) segmentRects.invoke(graph);
int count = segments.getCount();
for (int i = 0; i < count; i++) {
Point2D near = graph.getControlPoint(i);
Point2D far = graph.getControlPoint(i + count);
double pinCentreX = (near.getX() + far.getX()) / 2.0;
double pinCentreY = (near.getY() + far.getY()) / 2.0;
String where = "segment " + i + " at " + orientation + " " + segments;
Assert.assertEquals(where + " is off its pins horizontally", pinCentreX,
rects[i].getCenterX(), 0.01d);
Assert.assertEquals(where + " is off its pins vertically", pinCentreY,
rects[i].getCenterY(), 0.01d);
}
}
}
}
/** Neighbouring segments must not touch, or the bar reads as one block rather than a scale. */
@Test
public void segmentsDoNotTouchEachOther() throws Exception {
java.lang.reflect.Method segmentRects = LEDBarGraph.class.getDeclaredMethod("getSegmentRects");
segmentRects.setAccessible(true);
for (Segments segments : Segments.values()) {
LEDBarGraph graph = new LEDBarGraph();
graph.setSegments(segments);
Rectangle2D[] rects = (Rectangle2D[]) segmentRects.invoke(graph);
for (int i = 1; i < rects.length; i++) {
Assert.assertTrue("segments " + (i - 1) + " and " + i + " overlap at " + segments,
rects[i].getMinY() >= rects[i - 1].getMaxY() - 0.01d);
}
}
}
/**
* One red at the top, two yellow below it and green for the rest, so the green band grows with
* the segment count rather than the bands being fixed fractions.
*/
@Test
public void segmentColoursRunUpTheScale() throws Exception {
java.lang.reflect.Method segmentColor =
LEDBarGraph.class.getDeclaredMethod("getSegmentColor", int.class);
segmentColor.setAccessible(true);
for (Segments segments : Segments.values()) {
LEDBarGraph graph = bar(segments);
int count = segments.getCount();
int red = 0;
int yellow = 0;
int green = 0;
for (int i = 0; i < count; i++) {
Object colour = segmentColor.invoke(graph, i);
if (LEDBarGraph.SEGMENT_RED.equals(colour)) {
red++;
} else if (LEDBarGraph.SEGMENT_YELLOW.equals(colour)) {
yellow++;
} else if (LEDBarGraph.SEGMENT_GREEN.equals(colour)) {
green++;
} else {
Assert.fail("segment " + i + " of " + segments + " is none of the three scale colours");
}
}
Assert.assertEquals("red segments on " + segments, 1, red);
Assert.assertEquals("yellow segments on " + segments, 2, yellow);
Assert.assertEquals("green segments on " + segments, count - 3, green);
}
}
/** Red belongs at the top of the scale, which is the first segment as the part is drawn. */
@Test
public void redIsAtTheTopOfTheScale() throws Exception {
java.lang.reflect.Method segmentColor =
LEDBarGraph.class.getDeclaredMethod("getSegmentColor", int.class);
segmentColor.setAccessible(true);
LEDBarGraph graph = bar(Segments._10);
Assert.assertEquals(LEDBarGraph.SEGMENT_RED, segmentColor.invoke(graph, 0));
Assert.assertEquals(LEDBarGraph.SEGMENT_YELLOW, segmentColor.invoke(graph, 1));
Assert.assertEquals(LEDBarGraph.SEGMENT_YELLOW, segmentColor.invoke(graph, 2));
Assert.assertEquals(LEDBarGraph.SEGMENT_GREEN, segmentColor.invoke(graph, 3));
Assert.assertEquals(LEDBarGraph.SEGMENT_GREEN, segmentColor.invoke(graph, 9));
}
/**
* {@code BomMaker} keys rows on the type name and the value together, so a blank value would
* merge every variant into one line.
*/
@Test
public void everyVariantGetsItsOwnBomRow() {
java.util.Set<String> values = new java.util.HashSet<String>();
for (Segments segments : Segments.values()) {
String value = bar(segments).getValueForDisplay();
Assert.assertNotNull("value for " + segments, value);
Assert.assertFalse("value for " + segments + " is blank", value.trim().isEmpty());
values.add(value);
}
Assert.assertEquals("each variant needs a distinct BOM value", Segments.values().length,
values.size());
}
/** A part number the user has typed is the more specific answer, so it is kept and appended. */
@Test
public void aTypedPartNumberIsKeptBesideTheVariant() {
LEDBarGraph graph = bar(Segments._10);
Assert.assertEquals("10 Segment", graph.getValueForDisplay());
graph.setValue("LTA-1000G");
Assert.assertTrue("variant should survive a part number being typed",
graph.getValueForDisplay().contains("10 Segment"));
Assert.assertTrue("part number should reach the BOM",
graph.getValueForDisplay().contains("LTA-1000G"));
graph.setValue(" ");
Assert.assertEquals("whitespace is not a part number", "10 Segment",
graph.getValueForDisplay());
}
@Test
public void transformerAcceptsThisComponent() {
LEDBarGraphTransformer transformer = new LEDBarGraphTransformer();
LEDBarGraph graph = new LEDBarGraph();
Assert.assertTrue("must be rotatable", transformer.canRotate(graph));
Assert.assertTrue("must be mirrorable", transformer.canMirror(graph));
Assert.assertFalse("mirroring reorders pins but changes no connection",
transformer.mirroringChangesCircuit());
}
@Test
public void fourRotationsReturnToStart() {
LEDBarGraphTransformer transformer = new LEDBarGraphTransformer();
LEDBarGraph graph = new LEDBarGraph();
Point2D start =
new Point2D.Double(graph.getControlPoint(0).getX(), graph.getControlPoint(0).getY());
Point2D centre = new Point2D.Double(100, 100);
for (int i = 0; i < 4; i++) {
transformer.rotate(graph, centre, 1);
}
Assert.assertEquals("orientation returns", Orientation.DEFAULT, graph.getOrientation());
Assert.assertTrue("anchor returns", graph.getControlPoint(0).distance(start) < 0.5d);
}
/** Mirroring has to turn the orientation too, or the body faces the way it did before. */
@Test
public void mirroringFlipsTheOrientation() {
LEDBarGraphTransformer transformer = new LEDBarGraphTransformer();
Point2D centre = new Point2D.Double(100, 100);
LEDBarGraph horizontal = new LEDBarGraph();
horizontal.setOrientation(Orientation._90);
transformer.mirror(horizontal, centre, IComponentTransformer.HORIZONTAL);
Assert.assertEquals(Orientation._270, horizontal.getOrientation());
LEDBarGraph vertical = new LEDBarGraph();
vertical.setOrientation(Orientation.DEFAULT);
transformer.mirror(vertical, centre, IComponentTransformer.VERTICAL);
Assert.assertEquals(Orientation._180, vertical.getOrientation());
}
@Test
public void singleColorsRenderUniformSegments() throws Exception {
java.lang.reflect.Method segmentColor =
LEDBarGraph.class.getDeclaredMethod("getSegmentColor", int.class);
segmentColor.setAccessible(true);
BarColor[] singleColors = new BarColor[] {
BarColor.RED, BarColor.GREEN, BarColor.YELLOW, BarColor.BLUE, BarColor.ORANGE,
BarColor.WHITE
};
for (BarColor color : singleColors) {
LEDBarGraph graph = bar(Segments._10);
graph.setBarColor(color);
for (int i = 0; i < 10; i++) {
Assert.assertEquals("segment " + i + " for " + color,
color.getSegmentColor(i, 10), segmentColor.invoke(graph, i));
}
}
}
@Test
public void blueGreenYellowRedDistribution() throws Exception {
java.lang.reflect.Method segmentColor =
LEDBarGraph.class.getDeclaredMethod("getSegmentColor", int.class);
segmentColor.setAccessible(true);
LEDBarGraph graph10 = bar(Segments._10);
graph10.setBarColor(BarColor.BLUE_GREEN_YELLOW_RED);
Assert.assertEquals(LEDBarGraph.SEGMENT_RED, segmentColor.invoke(graph10, 0));
Assert.assertEquals(LEDBarGraph.SEGMENT_RED, segmentColor.invoke(graph10, 1));
Assert.assertEquals(LEDBarGraph.SEGMENT_YELLOW, segmentColor.invoke(graph10, 2));
Assert.assertEquals(LEDBarGraph.SEGMENT_YELLOW, segmentColor.invoke(graph10, 3));
Assert.assertEquals(LEDBarGraph.SEGMENT_YELLOW, segmentColor.invoke(graph10, 4));
Assert.assertEquals(LEDBarGraph.SEGMENT_GREEN, segmentColor.invoke(graph10, 5));
Assert.assertEquals(LEDBarGraph.SEGMENT_GREEN, segmentColor.invoke(graph10, 8));
Assert.assertEquals(LEDBarGraph.SEGMENT_BLUE, segmentColor.invoke(graph10, 9));
LEDBarGraph graph4 = bar(Segments._4);
graph4.setBarColor(BarColor.BLUE_GREEN_YELLOW_RED);
Assert.assertEquals(LEDBarGraph.SEGMENT_RED, segmentColor.invoke(graph4, 0));
Assert.assertEquals(LEDBarGraph.SEGMENT_YELLOW, segmentColor.invoke(graph4, 1));
Assert.assertEquals(LEDBarGraph.SEGMENT_GREEN, segmentColor.invoke(graph4, 2));
Assert.assertEquals(LEDBarGraph.SEGMENT_BLUE, segmentColor.invoke(graph4, 3));
}
@Test
public void colorVariantDistinguishesBomRow() {
LEDBarGraph defaultGraph = bar(Segments._10);
LEDBarGraph redGraph = bar(Segments._10);
redGraph.setBarColor(BarColor.RED);
LEDBarGraph bgyrGraph = bar(Segments._10);
bgyrGraph.setBarColor(BarColor.BLUE_GREEN_YELLOW_RED);
Assert.assertEquals("10 Segment", defaultGraph.getValueForDisplay());
Assert.assertEquals("10 Segment, Red", redGraph.getValueForDisplay());
Assert.assertEquals("10 Segment, Blue-Green-Yellow-Red", bgyrGraph.getValueForDisplay());
}
@Test
public void barColorDefaultsWhenNull() {
LEDBarGraph graph = new LEDBarGraph();
graph.setBarColor(null);
Assert.assertEquals(BarColor.GREEN_YELLOW_RED, graph.getBarColor());
}
private static LEDBarGraph bar(Segments segments) {
LEDBarGraph graph = new LEDBarGraph();
graph.setSegments(segments);
return graph;
}
}
@@ -0,0 +1,151 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.displays.LEDMatrix.Modules;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Basic checks for the MAX7219 matrix. The three variants differ in outline and in which edge
* their headers leave by, so each one places the same ten pins differently.
*/
public class LEDMatrixTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static LEDMatrix of(Modules modules) {
LEDMatrix matrix = new LEDMatrix();
matrix.setModules(modules);
return matrix;
}
@Test
public void boardSizeFollowsTheModules() {
for (Modules modules : Modules.values()) {
Rectangle2D bounds = of(modules).getBodyShape().getBounds2D();
Assert.assertEquals(modules + " width", modules.getBoardWidthMm(), mm(bounds.getWidth()),
0.01d);
Assert.assertEquals(modules + " length", modules.getBoardHeightMm(), mm(bounds.getHeight()),
0.01d);
}
}
/**
* Every board is exactly as wide as its modules. Asserted as an equality rather than a fit,
* because that is where the cascaded boards' widths come from: a transcribed 128 or 256 that did
* not match the module count would otherwise pass.
*/
@Test
public void theBoardIsExactlyAsWideAsItsModules() {
for (Modules modules : Modules.values()) {
double matrixSize = mm(LEDMatrix.MATRIX_SIZE.convertToPixels());
Assert.assertEquals(modules + " width", modules.getCount() * matrixSize,
modules.getBoardWidthMm(), 0.01d);
}
}
@Test
public void controlPointsAreTheTwoHeadersInOrder() {
String[] expected = new String[] {"VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN", "VCC_OUT",
"GND_OUT", "DOUT", "CS_OUT", "CLK_OUT"};
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
Assert.assertEquals(modules.toString(), expected.length, matrix.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals(modules + " pin " + i, expected[i], matrix.getControlPointNodeName(i));
Assert.assertTrue(modules + " duplicate node name " + expected[i], names.add(expected[i]));
}
}
}
/**
* The tall single runs its rows across the width; the tileable variants stand them on end at the
* short edges.
*/
@Test
public void eachHeaderIsAFivePinRow() {
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
MakerBoardTestSupport.assertRow(matrix, 0, 4);
MakerBoardTestSupport.assertRow(matrix, 5, 9);
}
}
@Test
public void everyPinSitsOnTheBoard() {
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
Rectangle2D board = matrix.getBodyShape().getBounds2D();
for (int i = 0; i < matrix.getControlPointCount(); i++) {
Assert.assertTrue(modules + " pin " + i + " is off the board",
board.contains(matrix.getControlPoint(i)));
}
}
}
@Test
public void modulesRoundTripThroughTheSetter() {
Assert.assertEquals(Modules.Single_8x8, new LEDMatrix().getModules());
for (Modules modules : Modules.values()) {
LEDMatrix matrix = of(modules);
Assert.assertEquals(modules, matrix.getModules());
Assert.assertEquals(modules.toString(), matrix.getValueForDisplay());
}
}
/** A project saved before the variant property existed deserialises with no modules set. */
@Test
public void aMissingModulesValueFallsBackToTheSingle() {
LEDMatrix matrix = of(null);
Assert.assertEquals(Modules.Single_8x8, matrix.getModules());
}
@Test
public void drawsCleanlyInEveryState() {
for (Modules modules : Modules.values()) {
MakerBoardTestSupport.assertDrawsCleanly(of(modules));
}
}
}
@@ -0,0 +1,195 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.Display;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the Nokia 5110 module. Its pin row is on the bottom edge, so the board grows
* upwards from control point zero and every feature is measured down from an edge the control
* points do not touch.
*/
public class Nokia5110LCDTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static Nokia5110LCD of(Color screenColor) {
Nokia5110LCD lcd = new Nokia5110LCD();
lcd.setScreenColor(screenColor);
return lcd;
}
@Test
public void boardIsTheModuleOutline() {
Rectangle2D bounds = new Nokia5110LCD().getBodyShape().getBounds2D();
Assert.assertEquals("width", 43.8d, mm(bounds.getWidth()), 0.01d);
Assert.assertEquals("length", 45.8d, mm(bounds.getHeight()), 0.01d);
}
@Test
public void controlPointsAreTheEightPinsInOrder() {
Nokia5110LCD display = new Nokia5110LCD();
String[] expected = new String[] {"RST", "CE", "DC", "DIN", "CLK", "VCC", "BL", "GND"};
Assert.assertEquals(expected.length, display.getControlPointCount());
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
@Test
public void pinsSitAtHeaderPitchAlongOneRow() {
Nokia5110LCD display = new Nokia5110LCD();
for (int i = 1; i < display.getControlPointCount(); i++) {
Point2D previous = display.getControlPoint(i - 1);
Point2D current = display.getControlPoint(i);
Assert.assertEquals("pitch at pin " + i, 2.54d, mm(current.getX() - previous.getX()), 0.01d);
Assert.assertEquals("pin " + i + " left the row", previous.getY(), current.getY(), 0.01d);
}
}
/** Clear of the bottom edge rather than the top, which is the easy thing to get backwards. */
@Test
public void headerIsCentredOnTheBottomEdge() {
Nokia5110LCD display = new Nokia5110LCD();
Rectangle2D board = display.getBodyShape().getBounds2D();
Point2D first = display.getControlPoint(0);
Point2D last = display.getControlPoint(display.getControlPointCount() - 1);
Assert.assertEquals("left margin", board.getMaxX() - last.getX(), first.getX() - board.getX(),
0.01d);
Assert.assertEquals("clearance from the bottom edge", 3.5d, mm(board.getMaxY() - first.getY()),
0.01d);
Assert.assertTrue("the row sits below the glass", first.getY() > board.getCenterY());
}
/**
* The glass is placed from the board's top edge rather than centred in the frame, so nothing
* keeps the two in step on its own.
*/
@Test
public void glassSitsInsideTheBezel() {
double bezelTop = Nokia5110LCD.BEZEL_TOP_OFFSET.convertToPixels();
double bezelBottom = bezelTop + Nokia5110LCD.BEZEL_LENGTH.convertToPixels();
double glassTop = Nokia5110LCD.DISPLAY_TOP_OFFSET.convertToPixels();
double glassBottom = glassTop + Nokia5110LCD.DISPLAY_LENGTH.convertToPixels();
Assert.assertTrue("glass reaches above the bezel", glassTop >= bezelTop);
Assert.assertTrue("glass reaches below the bezel", glassBottom <= bezelBottom);
Assert.assertTrue("glass is wider than the bezel", Nokia5110LCD.DISPLAY_WIDTH
.convertToPixels() <= Nokia5110LCD.BEZEL_WIDTH.convertToPixels());
Assert.assertTrue("bezel runs off the bottom of the board",
bezelBottom <= Nokia5110LCD.BOARD_LENGTH.convertToPixels());
}
/**
* Catches the outline being entered the wrong way round: with the two figures swapped the
* vertical pair sits 1.4 mm from the edge and a 3 mm hole breaches it.
*/
@Test
public void mountingHolesClearTheBoardEdges() {
double insetX = (Nokia5110LCD.BOARD_WIDTH.convertToPixels()
- Nokia5110LCD.MOUNTING_HOLE_SPACING_X.convertToPixels()) / 2.0;
double insetY = (Nokia5110LCD.BOARD_LENGTH.convertToPixels()
- Nokia5110LCD.MOUNTING_HOLE_SPACING_Y.convertToPixels()) / 2.0;
double radius = Nokia5110LCD.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
Assert.assertTrue("holes breach the side edges", insetX >= radius);
Assert.assertTrue("holes breach the top and bottom edges", insetY >= radius);
}
/**
* The strip between the pin row and the bottom edge is the one place on this board where a label
* can run out of room, being only as deep as the header's clearance from the edge.
*/
@Test
public void silkLabelsStayInTheStripBelowThePins() {
Assert.assertTrue("the labels sit past the bottom edge",
AbstractMakerBoard.PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels() < Nokia5110LCD.HEADER_OFFSET
.convertToPixels());
Assert.assertTrue("the labels sit on top of the pins",
AbstractMakerBoard.PIN_ROW_FLAT_LABEL_OFFSET.convertToPixels() > AbstractMakerBoard.PIN_SIZE
.convertToPixels());
}
/** No variant property, but a resolution and a controller worth printing. */
@Test
public void theGlassPrintsTheResolutionAndController() {
Nokia5110LCD printed = new Nokia5110LCD();
Nokia5110LCD blank = new Nokia5110LCD();
blank.setScreen(Display.NONE);
Assert.assertEquals("84x48 PCD8544", printed.getValueForDisplay());
Assert.assertEquals(Display.VALUE, printed.getScreen());
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
}
/**
* The dark ink the glass normally carries disappears into the darker backlights, so two at
* opposite ends of the range must each still show their text.
*/
@Test
public void theInkFollowsTheBacklight() {
Nokia5110LCD dark = of(Color.decode("#1B3C73"));
Nokia5110LCD pale = of(Color.decode("#6B7668"));
Nokia5110LCD darkBlank = of(Color.decode("#1B3C73"));
darkBlank.setScreen(Display.NONE);
Nokia5110LCD paleBlank = of(Color.decode("#6B7668"));
paleBlank.setScreen(Display.NONE);
MakerBoardTestSupport.assertScreenTextIsDrawn(dark, darkBlank);
MakerBoardTestSupport.assertScreenTextIsDrawn(pale, paleBlank);
}
/** A project saved before the backlight colour existed deserialises with none set. */
@Test
public void aMissingBacklightColourFallsBackToTheGlass() {
Assert.assertEquals(Nokia5110LCD.LCD_COLOR, of(null).getScreenColor());
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new Nokia5110LCD());
}
}
@@ -0,0 +1,621 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.Display;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.displays.OLEDDisplay.Layout;
import org.diylc.components.displays.OLEDDisplay.OLEDInterface;
import org.diylc.components.displays.OLEDDisplay.Version;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the three OLED modules. The interface is not orthogonal to the size: the
* 0.91" is sold as two different boards, so every check that reads a board figure reads it for a
* size and an interface together.
*/
public class OLEDDisplayTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static OLEDDisplay of(Version version, OLEDInterface oledInterface) {
OLEDDisplay display = new OLEDDisplay();
display.setVersion(version);
display.setOledInterface(oledInterface);
return display;
}
@Test
public void boardSizeFollowsTheVersionAndInterface() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
Rectangle2D bounds = of(version, oledInterface).getBodyShape().getBounds2D();
Assert.assertEquals(version + " " + oledInterface + " width", layout.getBoardWidthMm(),
mm(bounds.getWidth()), 0.01d);
Assert.assertEquals(version + " " + oledInterface + " length", layout.getBoardLengthMm(),
mm(bounds.getHeight()), 0.01d);
}
}
}
/**
* On the 0.91" the two interfaces are not one board with a longer header but two boards,
* differing in outline, in which edge the header sits on and in whether there are mounting holes
* at all. The other two sizes do carry one board across both interfaces.
*/
@Test
public void theSmallBoardIsTwoDifferentBoards() {
Layout i2c = Version.SSD1306_0_91.getLayout(OLEDInterface.I2C_4Pin);
Layout spi = Version.SSD1306_0_91.getLayout(OLEDInterface.SPI_7Pin);
Assert.assertEquals("I2C width", 35.8d, i2c.getBoardWidthMm(), 0.01d);
Assert.assertEquals("I2C length", 12.0d, i2c.getBoardLengthMm(), 0.01d);
Assert.assertEquals("SPI width", 32.5d, spi.getBoardWidthMm(), 0.01d);
Assert.assertEquals("SPI length", 20.5d, spi.getBoardLengthMm(), 0.01d);
Assert.assertTrue("the I2C column should stand on the left edge", i2c.isHeaderOnLeftEdge());
Assert.assertFalse("the SPI row should lie along the top edge", spi.isHeaderOnLeftEdge());
Assert.assertFalse("the I2C board has no mounting holes", i2c.hasMountingHoles());
Assert.assertTrue("the SPI board has four mounting holes", spi.hasMountingHoles());
for (Version version : new Version[] {Version.SSD1306_0_96, Version.SH1106_1_3}) {
Assert.assertSame(version + " should carry one board across both interfaces",
version.getLayout(OLEDInterface.I2C_4Pin), version.getLayout(OLEDInterface.SPI_7Pin));
}
}
@Test
public void controlPointsAreTheI2CPinsInOrder() {
OLEDDisplay display = of(Version.SSD1306_0_96, OLEDInterface.I2C_4Pin);
String[] expected = new String[] {"GND", "VCC", "SCL", "SDA"};
Assert.assertEquals(expected.length, display.getControlPointCount());
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
@Test
public void controlPointsAreTheSpiPinsInOrder() {
OLEDDisplay display = of(Version.SSD1306_0_96, OLEDInterface.SPI_7Pin);
String[] expected = new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS"};
Assert.assertEquals(expected.length, display.getControlPointCount());
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
@Test
public void controlPointsAreTheEightPinRowInOrder() {
OLEDDisplay display = of(Version.SSD1306_0_96, OLEDInterface.SPI_8Pin);
String[] expected = new String[] {"SDA", "SCL", "DC", "RES", "CS", "VDD", "VIN", "GND"};
Assert.assertEquals(expected.length, display.getControlPointCount());
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
/** Every size is sold in every interface, so no size may carry a shorter pin array. */
@Test
public void everyInterfaceExistsAtEverySize() {
for (Version version : Version.values()) {
Assert.assertEquals(version + " I2C", 4,
of(version, OLEDInterface.I2C_4Pin).getControlPointCount());
Assert.assertEquals(version + " SPI", 7,
of(version, OLEDInterface.SPI_7Pin).getControlPointCount());
Assert.assertEquals(version + " SPI-6", 6,
of(version, OLEDInterface.SPI_6Pin).getControlPointCount());
Assert.assertEquals(version + " SPI-8", 8,
of(version, OLEDInterface.SPI_8Pin).getControlPointCount());
}
}
/**
* The 6-pin board is the 7-pin one with the chip select tied low, so its row is the other's
* without that pin and in the same order. It borrows the 7-pin board's outline, which is a
* derivation rather than a measurement.
*/
@Test
public void theSixPinBoardDropsOnlyTheChipSelect() {
String[] seven = OLEDInterface.SPI_7Pin.getPinNames();
String[] six = OLEDInterface.SPI_6Pin.getPinNames();
Assert.assertEquals("one pin fewer", seven.length - 1, six.length);
Assert.assertEquals("CS is the pin that goes", "CS", seven[seven.length - 1]);
for (int i = 0; i < six.length; i++) {
Assert.assertEquals("pin " + i, seven[i], six[i]);
}
for (Version version : Version.values()) {
Assert.assertSame(version + " shares the SPI board",
version.getLayout(OLEDInterface.SPI_7Pin), version.getLayout(OLEDInterface.SPI_6Pin));
}
}
/**
* The eight-pin row cannot be compared in order, following the controller's pin numbering rather
* than the silk the others print, so what has to hold is that the set has grown by exactly VDD.
* VDD and VIN are separate rails, so neither may be renamed to the other or to VCC, which a set
* comparison alone would let through.
*/
@Test
public void theEightPinBoardAddsOnlyTheLogicSupply() {
Set<String> seven = new HashSet<String>(Arrays.asList(OLEDInterface.SPI_7Pin.getPinNames()));
Set<String> eight = new HashSet<String>(Arrays.asList(OLEDInterface.SPI_8Pin.getPinNames()));
Assert.assertEquals("no pin names repeat in the row", 8,
OLEDInterface.SPI_8Pin.getPinNames().length);
Assert.assertEquals("no pin names repeat in the row", 8, eight.size());
Assert.assertTrue("VIN replaces the shorter rows' VCC", eight.contains("VIN"));
Assert.assertTrue("the logic rail is the pin that is added", eight.contains("VDD"));
Assert.assertFalse("VCC belongs to the rows with one supply", eight.contains("VCC"));
seven.remove("VCC");
eight.remove("VIN");
eight.remove("VDD");
Assert.assertEquals("the signal lines are the seven-pin row's", seven, eight);
}
/** Switching either property must resize the array, not leave the longer one's points behind. */
@Test
public void switchingInterfaceResizesTheControlPoints() {
OLEDDisplay display = new OLEDDisplay();
Assert.assertEquals(4, display.getControlPointCount());
display.setOledInterface(OLEDInterface.SPI_7Pin);
Assert.assertEquals(7, display.getControlPointCount());
display.setVersion(Version.SSD1306_0_91);
Assert.assertEquals(7, display.getControlPointCount());
display.setOledInterface(OLEDInterface.SPI_6Pin);
Assert.assertEquals(6, display.getControlPointCount());
display.setOledInterface(OLEDInterface.SPI_8Pin);
Assert.assertEquals(8, display.getControlPointCount());
display.setOledInterface(OLEDInterface.I2C_4Pin);
Assert.assertEquals(4, display.getControlPointCount());
}
@Test
public void pinsSitAtHeaderPitchAlongOneLine() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
OLEDDisplay display = of(version, oledInterface);
boolean vertical = version.getLayout(oledInterface).isHeaderOnLeftEdge();
for (int i = 1; i < display.getControlPointCount(); i++) {
Point2D previous = display.getControlPoint(i - 1);
Point2D current = display.getControlPoint(i);
String pin = version + " " + oledInterface + " pin " + i;
Assert.assertEquals(pin + " pitch", 2.54d,
mm(vertical ? current.getY() - previous.getY() : current.getX() - previous.getX()),
0.01d);
Assert.assertEquals(pin + " left the line", vertical ? previous.getX() : previous.getY(),
vertical ? current.getX() : current.getY(), 0.01d);
}
}
}
}
/**
* Nothing else in the geometry would notice if a board were placed from its first pin instead of
* being centred: a four-pin row would sit in a corner and a seven-pin one would look very nearly
* centred anyway.
*/
@Test
public void headerRowIsCentredOnTheTopEdge() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
if (layout.isHeaderOnLeftEdge()) {
continue;
}
OLEDDisplay display = of(version, oledInterface);
Rectangle2D board = display.getBodyShape().getBounds2D();
Point2D first = display.getControlPoint(0);
Point2D last = display.getControlPoint(display.getControlPointCount() - 1);
Assert.assertEquals(version + " " + oledInterface + " margins",
board.getMaxX() - last.getX(), first.getX() - board.getX(), 0.01d);
Assert.assertEquals(version + " " + oledInterface + " clearance from the top edge",
layout.getHeaderOffsetMm(), mm(first.getY() - board.getY()), 0.01d);
}
}
}
/**
* The column stands its clearance in from the left edge and is centred on the height. A board
* drawn down and left from its first pin instead would keep every pitch and size assertion above.
*/
@Test
public void headerColumnStandsAgainstTheLeftEdge() {
OLEDDisplay display = of(Version.SSD1306_0_91, OLEDInterface.I2C_4Pin);
Rectangle2D board = display.getBodyShape().getBounds2D();
Point2D first = display.getControlPoint(0);
Point2D last = display.getControlPoint(display.getControlPointCount() - 1);
Assert.assertEquals("clearance from the left edge", 1.5d, mm(first.getX() - board.getX()),
0.01d);
Assert.assertEquals("the column is centred on the height", board.getMaxY() - last.getY(),
first.getY() - board.getY(), 0.01d);
}
/**
* A lit area's aspect is very nearly the pixel aspect: 2:1 on the two 128 x 64 panels and 4:1 on
* the 128 x 32. The two derived panels sit exactly on it; the measured 0.96" comes out a little
* under, which the tolerance allows while still catching a transposition.
*/
@Test
public void litAreaMatchesThePixelAspect() {
Assert.assertEquals(2.0d, aspect(Version.SSD1306_0_96), 0.05d);
Assert.assertEquals(4.0d, aspect(Version.SSD1306_0_91), 0.01d);
Assert.assertEquals(2.0d, aspect(Version.SH1106_1_3), 0.01d);
}
private static double aspect(Version version) {
return version.getActiveWidthMm() / version.getActiveLengthMm();
}
@Test
public void litAreaSitsWithinThePanel() {
for (Version version : Version.values()) {
Assert.assertTrue(version + " lit area is wider than its panel",
version.getActiveLeftInPanelMm() + version.getActiveWidthMm() <= version
.getPanelWidthMm());
Assert.assertTrue(version + " lit area is longer than its panel",
version.getActiveLengthMm() <= version.getPanelLengthMm());
Assert.assertTrue(version + " lit area starts off the panel",
version.getActiveLeftInPanelMm() >= 0);
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
String board = version + " " + oledInterface;
Assert.assertTrue(board + " lit area starts above its panel",
layout.getActiveTopMm() >= layout.getPanelTopMm());
Assert.assertTrue(board + " lit area runs off the bottom of its panel",
layout.getActiveTopMm() + version.getActiveLengthMm() <= layout.getPanelTopMm()
+ version.getPanelLengthMm());
}
}
}
@Test
public void panelFitsWithinTheBoard() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
String board = version + " " + oledInterface;
Assert.assertTrue(board + " panel runs off the right edge", layout.getPanelLeftMm()
+ version.getPanelWidthMm() <= layout.getBoardWidthMm());
Assert.assertTrue(board + " panel runs off the bottom edge", layout.getPanelTopMm()
+ version.getPanelLengthMm() <= layout.getBoardLengthMm());
}
}
}
/**
* Where the lit area lands on the board. Getting either half wrong moves it while leaving every
* size and clearance assertion intact, and swapping the two between the boards leaves both
* plausible -- which is what {@link #pinsStayOffThePanel} is for.
*/
@Test
public void litAreaSitsWhereItWasMeasured() {
Assert.assertEquals("0.91\" I2C", 7.1d,
litAreaLeftMm(Version.SSD1306_0_91, OLEDInterface.I2C_4Pin), 0.01d);
Assert.assertEquals("0.91\" SPI", 3.35d,
litAreaLeftMm(Version.SSD1306_0_91, OLEDInterface.SPI_7Pin), 0.01d);
}
/**
* No pin may land on the glass. Every other check measures features against each other, so the
* two 0.91" panel offsets could be swapped between the boards and pass all of them -- and
* swapped, the I2C board's four pins are drawn on the display.
*/
@Test
public void pinsStayOffThePanel() {
double pinRadius = AbstractMakerBoard.PIN_SIZE.convertToPixels() / 2.0;
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
OLEDDisplay display = of(version, oledInterface);
Rectangle2D board = display.getBodyShape().getBounds2D();
Rectangle2D panel = new Rectangle2D.Double(
board.getX() + mmToPx(layout.getPanelLeftMm()),
board.getY() + mmToPx(layout.getPanelTopMm()),
mmToPx(version.getPanelWidthMm()), mmToPx(version.getPanelLengthMm()));
for (int i = 0; i < display.getControlPointCount(); i++) {
Point2D pin = display.getControlPoint(i);
Rectangle2D pad = new Rectangle2D.Double(pin.getX() - pinRadius, pin.getY() - pinRadius,
2 * pinRadius, 2 * pinRadius);
Assert.assertFalse(version + " " + oledInterface + " pin " + i + " sits on the panel",
panel.intersects(pad));
}
}
}
}
private static double mmToPx(double millimetres) {
return millimetres * PX_PER_MM;
}
private static double litAreaLeftMm(Version version, OLEDInterface oledInterface) {
return version.getLayout(oledInterface).getPanelLeftMm() + version.getActiveLeftInPanelMm();
}
/**
* The 0.91" SPI board's measured 1.25 mm panel offset is exactly centred on its measured 32.5 mm
* width, two independent readings agreeing. The vertical pair do not: the panel hangs below the
* header rather than sitting centred, so there is nothing to cross-check down the board.
*/
@Test
public void theSmallSpiBoardCentresItsPanelAcrossTheBoard() {
Version version = Version.SSD1306_0_91;
Layout layout = version.getLayout(OLEDInterface.SPI_7Pin);
Assert.assertEquals("the panel is off centre across the board",
(layout.getBoardWidthMm() - version.getPanelWidthMm()) / 2.0, layout.getPanelLeftMm(),
0.01d);
}
/**
* Every lit area is measured down from its board's top edge rather than centred on the panel,
* the driver's bonding region taking the bottom of the glass. The centred position is asserted
* beside each measurement because a stale one would still land inside the panel and clear every
* pin and hole.
*/
@Test
public void measuredLitAreasSitHighInTheirPanels() {
assertLitAreaTop(Version.SSD1306_0_96, OLEDInterface.I2C_4Pin, 6.14d, 8.0d);
assertLitAreaTop(Version.SSD1306_0_91, OLEDInterface.I2C_4Pin, 2.35d, 3.208d);
assertLitAreaTop(Version.SSD1306_0_91, OLEDInterface.SPI_7Pin, 6.6d, 7.208d);
assertLitAreaTop(Version.SH1106_1_3, OLEDInterface.I2C_4Pin, 7.35d, 9.4d);
}
private static void assertLitAreaTop(Version version, OLEDInterface oledInterface,
double measuredMm, double centredMm) {
Layout layout = version.getLayout(oledInterface);
Assert.assertEquals(version + " measured from the top edge", measuredMm,
layout.getActiveTopMm(), 0.01d);
Assert.assertEquals(version + " centring would drop it", centredMm, layout.getPanelTopMm()
+ (version.getPanelLengthMm() - version.getActiveLengthMm()) / 2.0, 0.01d);
}
/**
* The panel shares the holes' horizontal span on every board, so only the vertical axis can keep
* them apart and a symmetric check would fail on a board that is drawn correctly. The 0.91" SPI
* board is the tight one, clearing its header-side holes by 0.5 mm.
*/
@Test
public void panelClearsTheMountingHolesVertically() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
if (!layout.hasMountingHoles()) {
continue;
}
double holeRadius = layout.getHoleSizeMm() / 2.0;
double panelTop = layout.getPanelTopMm();
double panelBottom = panelTop + version.getPanelLengthMm();
String board = version + " " + oledInterface;
Assert.assertTrue(board + " panel overlaps the header-side holes",
panelTop >= layout.getHoleInsetMm() + holeRadius);
Assert.assertTrue(board + " panel overlaps the far holes", panelBottom <= layout
.getBoardLengthMm() - layout.getHoleInsetMm() - holeRadius);
}
}
}
/**
* The strip the pin names have to fit into. The 0.96"'s 2.27 mm is the shallowest, and the
* reason these boards place their own labels rather than using the shared helper.
*/
@Test
public void panelClearsThePinRow() {
Assert.assertEquals("0.96\"", 2.27d, stripDepthMm(Version.SSD1306_0_96), 0.01d);
Assert.assertEquals("0.91\" SPI", 2.5d, stripDepthMm(Version.SSD1306_0_91), 0.01d);
Assert.assertEquals("1.3\"", 3.75d, stripDepthMm(Version.SH1106_1_3), 0.01d);
}
private static double stripDepthMm(Version version) {
Layout layout = version.getLayout(OLEDInterface.SPI_7Pin);
return layout.getPanelTopMm() - layout.getHeaderOffsetMm();
}
@Test
public void mountingHolesSitInsideTheBoard() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
if (!layout.hasMountingHoles()) {
continue;
}
Assert.assertTrue(version + " " + oledInterface + " holes breach an edge",
layout.getHoleInsetMm() >= layout.getHoleSizeMm() / 2.0);
}
}
}
/**
* Two panels sit centred on their board, and their stored offsets are that centred position
* rather than a measurement. Checking it keeps the two in step, so that a later correction to a
* panel or a board cannot leave a stale offset behind.
*/
@Test
public void centredPanelsStoreTheCentredOffsets() {
for (Version version : new Version[] {Version.SSD1306_0_96, Version.SH1106_1_3}) {
Layout layout = version.getLayout(OLEDInterface.I2C_4Pin);
Assert.assertEquals(version + " panel is off centre horizontally",
(layout.getBoardWidthMm() - version.getPanelWidthMm()) / 2.0, layout.getPanelLeftMm(),
0.01d);
Assert.assertEquals(version + " panel is off centre vertically",
(layout.getBoardLengthMm() - version.getPanelLengthMm()) / 2.0, layout.getPanelTopMm(),
0.01d);
Assert.assertEquals(version + " lit area is off centre within its panel",
(version.getPanelWidthMm() - version.getActiveWidthMm()) / 2.0,
version.getActiveLeftInPanelMm(), 0.01d);
}
}
/**
* Every board prints its pin names and none prints one on the glass. No geometry assertion here
* would notice a label drawn over the display, and the shared helper's offset would put one
* there on the 0.96".
*
* <p>The silkscreen colour is repointed at a colour nothing else on these boards draws in, since
* mounting holes are filled in the canvas's white and would otherwise count as lettering.
*/
@Test
public void pinNamesAreDrawnAndStayOffThePanel() {
Color silk = AbstractMakerBoard.SILK_COLOR;
AbstractMakerBoard.SILK_COLOR = Color.MAGENTA;
try {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
Layout layout = version.getLayout(oledInterface);
OLEDDisplay display = of(version, oledInterface);
int[] pixels = MakerBoardTestSupport.renderPixels(display);
int side = (int) Math.round(Math.sqrt(pixels.length));
Rectangle2D outline = display.getBodyShape().getBounds2D();
Rectangle2D glass = new Rectangle2D.Double(
outline.getX() + mmToPx(layout.getPanelLeftMm()),
outline.getY() + mmToPx(layout.getPanelTopMm()),
mmToPx(version.getPanelWidthMm()), mmToPx(version.getPanelLengthMm()));
int printed = 0;
int onGlass = 0;
for (int i = 0; i < pixels.length; i++) {
if (!isSilk(pixels[i])) {
continue;
}
printed++;
if (glass.contains(i % side, i / side)) {
onGlass++;
}
}
String board = version + " " + oledInterface;
Assert.assertTrue(board + " prints no pin names at all", printed > 0);
Assert.assertEquals(board + " prints a pin name on the panel", 0, onGlass);
}
}
} finally {
AbstractMakerBoard.SILK_COLOR = silk;
}
}
/**
* Matching hue rather than value catches lettering that antialiasing has dimmed where it clips
* the glass. Nothing else drawn on these boards leaves red and blue both well ahead of green.
*/
private static boolean isSilk(int argb) {
int red = (argb >> 16) & 0xFF;
int green = (argb >> 8) & 0xFF;
int blue = argb & 0xFF;
return red > 60 && blue > 60 && red > green * 1.5 && blue > green * 1.5;
}
/** Without this the BOM collapses six different modules into one or two rows. */
@Test
public void bomValueCarriesSizeAndInterface() {
Assert.assertEquals("0.96\" SSD1306 128x64, I2C",
of(Version.SSD1306_0_96, OLEDInterface.I2C_4Pin).getValueForDisplay());
Assert.assertEquals("1.3\" SH1106 128x64, SPI-7",
of(Version.SH1106_1_3, OLEDInterface.SPI_7Pin).getValueForDisplay());
Set<String> values = new HashSet<String>();
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
values.add(of(version, oledInterface).getValueForDisplay());
}
}
Assert.assertEquals("every board should get its own BOM row",
Version.values().length * OLEDInterface.values().length, values.size());
}
/**
* Rendered rather than asked for: a screen that found no room for the text would still answer
* the getter.
*/
@Test
public void everyPanelPrintsItsDescription() {
for (Version version : Version.values()) {
for (OLEDInterface oledInterface : OLEDInterface.values()) {
OLEDDisplay printed = of(version, oledInterface);
OLEDDisplay blank = of(version, oledInterface);
blank.setScreen(Display.NONE);
Assert.assertEquals(Display.VALUE, printed.getScreen());
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
}
}
}
/**
* The pixels are the only thing the colour touches -- there is no backlight behind them and no
* ink in front -- and the text has to come through whichever one is picked.
*/
@Test
public void theLitAreaFollowsThePixelColour() {
OLEDDisplay white = new OLEDDisplay();
white.setPixelColor(Color.decode("#F5F5F5"));
OLEDDisplay whiteBlank = new OLEDDisplay();
whiteBlank.setPixelColor(Color.decode("#F5F5F5"));
whiteBlank.setScreen(Display.NONE);
OLEDDisplay yellow = new OLEDDisplay();
yellow.setPixelColor(Color.decode("#FFD54F"));
OLEDDisplay yellowBlank = new OLEDDisplay();
yellowBlank.setPixelColor(Color.decode("#FFD54F"));
yellowBlank.setScreen(Display.NONE);
MakerBoardTestSupport.assertScreenTextIsDrawn(white, whiteBlank);
MakerBoardTestSupport.assertScreenTextIsDrawn(yellow, yellowBlank);
}
/** A project saved before the pixel colour existed deserialises with none set. */
@Test
public void aMissingPixelColourFallsBackToBlue() {
OLEDDisplay display = new OLEDDisplay();
display.setPixelColor(null);
Assert.assertEquals(OLEDDisplay.PIXEL_BLUE, display.getPixelColor());
}
}
@@ -0,0 +1,240 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.displays.SevenSegmentDisplay.Common;
import org.diylc.components.displays.SevenSegmentDisplay.DisplayType;
import org.diylc.components.displays.SevenSegmentDisplay.Punctuation;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Basic checks for the seven-segment displays. All but one are DIP packages straddling their pins
* with a row either side; the last is a module carrying a single header down one edge.
*/
public class SevenSegmentDisplayTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static SevenSegmentDisplay of(DisplayType displayType) {
SevenSegmentDisplay display = new SevenSegmentDisplay();
display.setDisplayType(displayType);
return display;
}
@Test
public void bodySizeFollowsThePackage() {
for (DisplayType displayType : DisplayType.values()) {
Rectangle2D bounds = of(displayType).getBodyShape().getBounds2D();
Assert.assertEquals(displayType + " width", displayType.getBodyWidthMm(),
mm(bounds.getWidth()), 0.01d);
Assert.assertEquals(displayType + " length", displayType.getBodyLengthMm(),
mm(bounds.getHeight()), 0.01d);
}
}
@Test
public void pinCountFollowsThePackage() {
Assert.assertEquals(10, of(DisplayType.SingleDigit_10Pin).getControlPointCount());
Assert.assertEquals(10, of(DisplayType.TwoDigit_0_56_10Pin).getControlPointCount());
Assert.assertEquals(12, of(DisplayType.FourDigit_0_36_12Pin).getControlPointCount());
Assert.assertEquals(12, of(DisplayType.FourDigit_0_56_12Pin).getControlPointCount());
Assert.assertEquals(4, of(DisplayType.TM1637_Module_4Pin).getControlPointCount());
}
/**
* Pin arrays are in DIP order: pin 1 bottom left, along the bottom row, then back along the top.
*/
@Test
public void pinNamesAreInDipOrder() {
Assert.assertArrayEquals(new String[] {"E", "D", "COM1", "C", "DP", "B", "A", "COM2", "F", "G"},
SevenSegmentDisplay.PIN_NAMES_1DIGIT);
Assert.assertArrayEquals(new String[] {"E", "D", "DP", "C", "G", "B", "D2", "D1", "F", "A"},
SevenSegmentDisplay.PIN_NAMES_2DIGIT);
Assert.assertArrayEquals(
new String[] {"E", "D", "DP", "C", "G", "D4", "B", "D3", "D2", "F", "A", "D1"},
SevenSegmentDisplay.PIN_NAMES_4DIGIT);
for (DisplayType displayType : DisplayType.values()) {
SevenSegmentDisplay display = of(displayType);
Set<String> names = new HashSet<String>();
for (int i = 0; i < display.getControlPointCount(); i++) {
String name = display.getControlPointNodeName(i);
Assert.assertFalse(displayType + " pin " + i + " is unnamed", name.isEmpty());
Assert.assertTrue(displayType + " duplicate node name " + name, names.add(name));
}
}
}
/** The 3641AS and the 5641AS differ in size but not in pin function, so one array serves both. */
@Test
public void bothFourDigitPackagesSharePinNames() {
SevenSegmentDisplay small = of(DisplayType.FourDigit_0_36_12Pin);
SevenSegmentDisplay large = of(DisplayType.FourDigit_0_56_12Pin);
for (int i = 0; i < small.getControlPointCount(); i++) {
Assert.assertEquals("pin " + i, small.getControlPointNodeName(i),
large.getControlPointNodeName(i));
}
Assert.assertNotEquals(small.getBodyShape().getBounds2D(), large.getBodyShape().getBounds2D());
Assert.assertSame("shared by identity, not by two matching copies",
DisplayType.FourDigit_0_36_12Pin.getPinNames(),
DisplayType.FourDigit_0_56_12Pin.getPinNames());
}
/**
* The digit count is the variant's own figure rather than something the drawing infers from
* which constant it is looking at, which is what lets a two-, six- or eight-digit part be a
* constant beside these.
*/
@Test
public void digitCountComesFromThePackage() {
Assert.assertEquals(1, DisplayType.SingleDigit_10Pin.getDigitCount());
Assert.assertEquals(2, DisplayType.TwoDigit_0_56_10Pin.getDigitCount());
Assert.assertEquals(4, DisplayType.FourDigit_0_36_12Pin.getDigitCount());
Assert.assertEquals(4, DisplayType.FourDigit_0_56_12Pin.getDigitCount());
Assert.assertEquals(4, DisplayType.TM1637_Module_4Pin.getDigitCount());
for (DisplayType displayType : DisplayType.values()) {
Assert.assertTrue(displayType + " shows no digits", displayType.getDigitCount() >= 1);
Assert.assertEquals(displayType + " pin count", displayType.getPinNames().length,
of(displayType).getControlPointCount());
}
}
/**
* A driven module brings out no common pins, so its polarity is not something you order and the
* BOM line omits it. Paired with the pin arrangement so the two cannot become one question: they
* agree on every variant today, but a single-in-line bare package would separate them.
*/
@Test
public void onlyModulesCarryABoard() {
for (DisplayType displayType : DisplayType.values()) {
Assert.assertEquals(displayType + " module", displayType == DisplayType.TM1637_Module_4Pin,
displayType.isModule());
Assert.assertEquals(displayType + " board figures", displayType.isModule(),
displayType.getModule() != null);
Assert.assertNotEquals(displayType + " row arrangement", displayType.isModule(),
displayType.isDualRow());
}
}
@Test
public void dipPackagesCarryTwoRowsAndTheModuleOne() {
for (DisplayType displayType : DisplayType.values()) {
SevenSegmentDisplay display = of(displayType);
int pinCount = display.getControlPointCount();
if (displayType.isDualRow()) {
int perRow = pinCount / 2;
MakerBoardTestSupport.assertRow(display, 0, perRow - 1);
MakerBoardTestSupport.assertRow(display, perRow, pinCount - 1);
Assert.assertEquals(displayType + " row spacing", displayType.getRowSpacingMm(),
mm(Math.abs(display.getControlPoint(0).getY()
- display.getControlPoint(pinCount - 1).getY())),
0.01d);
} else {
MakerBoardTestSupport.assertRow(display, 0, pinCount - 1);
}
}
}
/** A package only works if its pin rows sit within its body. */
@Test
public void everyPinSitsOnTheBody() {
for (DisplayType displayType : DisplayType.values()) {
SevenSegmentDisplay display = of(displayType);
Rectangle2D body = display.getBodyShape().getBounds2D();
for (int i = 0; i < display.getControlPointCount(); i++) {
Assert.assertTrue(displayType + " pin " + i + " is off the body",
body.contains(display.getControlPoint(i)));
}
}
}
@Test
public void propertiesRoundTripThroughTheirSetters() {
SevenSegmentDisplay display = new SevenSegmentDisplay();
Assert.assertEquals(DisplayType.SingleDigit_10Pin, display.getDisplayType());
Assert.assertEquals(Common.Cathode, display.getCommon());
Assert.assertEquals(Punctuation.DecimalPoints, display.getPunctuation());
for (DisplayType displayType : DisplayType.values()) {
Assert.assertEquals(displayType, of(displayType).getDisplayType());
}
for (Common common : Common.values()) {
display.setCommon(common);
Assert.assertEquals(common, display.getCommon());
}
for (Punctuation punctuation : Punctuation.values()) {
display.setPunctuation(punctuation);
Assert.assertEquals(punctuation, display.getPunctuation());
}
}
/** The module's polarity is not part of what you order, so its BOM value leaves it out. */
@Test
public void bomValueNamesThePackageAndWhatVariesWithIt() {
SevenSegmentDisplay module = of(DisplayType.TM1637_Module_4Pin);
module.setCommon(Common.Anode);
module.setPunctuation(Punctuation.Colon);
Assert.assertEquals(DisplayType.TM1637_Module_4Pin + ", " + Punctuation.Colon,
module.getValueForDisplay());
SevenSegmentDisplay bare = of(DisplayType.SingleDigit_10Pin);
bare.setCommon(Common.Anode);
bare.setPunctuation(Punctuation.None);
Assert.assertEquals(
DisplayType.SingleDigit_10Pin + ", " + Common.Anode + ", " + Punctuation.None,
bare.getValueForDisplay());
}
@Test
public void drawsCleanlyInEveryState() {
for (DisplayType displayType : DisplayType.values()) {
for (Punctuation punctuation : Punctuation.values()) {
SevenSegmentDisplay display = of(displayType);
display.setPunctuation(punctuation);
MakerBoardTestSupport.assertDrawsCleanly(display);
}
}
}
}
@@ -0,0 +1,673 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.FontMetrics;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.Display;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.displays.TFTDisplay.Controller;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the controller variants. The pin count differs between them, so the checks
* that matter are the ones that catch an array and an outline disagreeing: that the header still
* fits across its board, and that the screen and the mounting holes stay off each other.
*/
public class TFTDisplayTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static double px(double millimetres) {
return new Size(millimetres, SizeUnit.mm).convertToPixels();
}
private static TFTDisplay of(Controller controller) {
TFTDisplay display = new TFTDisplay();
display.setController(controller);
return display;
}
@Test
public void boardSizeFollowsTheController() {
for (Controller controller : Controller.values()) {
Rectangle2D bounds = of(controller).getBodyShape().getBounds2D();
Assert.assertEquals(controller + " width", controller.getBoardWidthMm(),
mm(bounds.getWidth()), 0.01d);
Assert.assertEquals(controller + " length", controller.getBoardLengthMm(),
mm(bounds.getHeight()), 0.01d);
}
}
@Test
public void controlPointCountFollowsThePinArray() {
for (Controller controller : Controller.values()) {
int secondary =
controller.hasSecondaryHeader() ? controller.getSecondaryPinNames().length : 0;
Assert.assertEquals(controller.toString(), controller.getPinNames().length + secondary,
of(controller).getControlPointCount());
}
Assert.assertEquals(14, Controller.ILI9341_2_8.getPinNames().length);
Assert.assertEquals(14, Controller.ILI9341_2_4.getPinNames().length);
Assert.assertEquals(8, Controller.ST7735_0_96.getPinNames().length);
Assert.assertEquals(8, Controller.ST7735_1_8.getPinNames().length);
Assert.assertEquals(8, Controller.ST7735_1_8_SD.getPinNames().length);
Assert.assertEquals(8, Controller.ST7789_1_54.getPinNames().length);
Assert.assertEquals(7, Controller.GC9A01_1_28.getPinNames().length);
Assert.assertEquals("the 2.4\" carries a second row", 18,
of(Controller.ILI9341_2_4).getControlPointCount());
Assert.assertEquals("the 2.8\" carries the same second row", 18,
of(Controller.ILI9341_2_8).getControlPointCount());
Assert.assertEquals("the 1.8\" with the socket carries one too", 12,
of(Controller.ST7735_1_8_SD).getControlPointCount());
}
/**
* The 2.4" is the 2.8" board with a smaller panel, so it shares the pin array. Asserted by
* identity: a copy would pass every name check and drift the day one of the two is corrected.
*/
@Test
public void bothILI9341BoardsShareOnePinArray() {
Assert.assertSame(Controller.ILI9341_2_8.getPinNames(), Controller.ILI9341_2_4.getPinNames());
Assert.assertEquals(TFTDisplay.SILK_NAMES_ILI9341.length,
Controller.ILI9341_2_4.getPinNames().length);
}
/**
* Neither row may print a name the default label would mangle: it keeps what stands before the
* underscore, which would leave five pins reading "T" and four reading "SD". Widths are asserted
* against the font the row is drawn in, a flat label having the 0.1" pitch and no more.
*/
@Test
public void everyPrintedPinNameIsDistinctAndFitsThePitch() {
FontMetrics metrics = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics()
.getFontMetrics(AbstractMakerBoard.PIN_ROW_FLAT_FONT);
double pitch = AbstractMakerBoard.PIN_SPACING.convertToPixels();
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Set<String> rowNames = new HashSet<String>();
int primaryCount = controller.getPinNames().length;
for (int i = 0; i < display.getControlPointCount(); i++) {
if (i == primaryCount) {
rowNames.clear();
}
String label = display.getSilkPinLabel(i);
Assert.assertTrue(controller + " pin " + i + " prints nothing",
label != null && !label.isEmpty());
Assert.assertTrue(controller + " prints " + label + " twice on one row",
rowNames.add(label));
Assert.assertTrue(controller + " " + label + " is wider than the pitch",
metrics.stringWidth(label) <= pitch);
}
}
}
/** Eighteen pins on one part, so a repeated name would reach the netlist as one node. */
@Test
public void everyNodeNameIsUnique() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Set<String> names = new HashSet<String>();
for (int i = 0; i < display.getControlPointCount(); i++) {
String name = display.getControlPointNodeName(i);
Assert.assertNotNull(controller + " pin " + i + " has no name", name);
Assert.assertTrue(controller + " duplicate node name " + name, names.add(name));
}
}
}
/**
* What a module of this size brings out does not vary with the driver behind it. Shared by
* identity rather than by three copies, so a correction to one reaches all of them.
*/
@Test
public void theSmallBoardsShareOnePinArray() {
Controller[] small = {Controller.ST7735_0_96, Controller.ST7735_1_8,
Controller.ST7735_1_8_SD, Controller.ST7789_1_54};
String[] expected = new String[] {"GND", "VCC", "SCL", "SDA", "RES", "DC", "CS", "BLK"};
for (Controller controller : small) {
Assert.assertSame(controller.toString(), TFTDisplay.PIN_NAMES_SPI_8PIN,
controller.getPinNames());
TFTDisplay display = of(controller);
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals(controller + " pin " + i, expected[i],
display.getControlPointNodeName(i));
}
}
}
/** Switching variants must resize the array, not leave the longer one's points behind. */
@Test
public void switchingControllerResizesTheControlPoints() {
TFTDisplay display = new TFTDisplay();
Assert.assertEquals(18, display.getControlPointCount());
display.setController(Controller.ST7735_1_8);
Assert.assertEquals(8, display.getControlPointCount());
display.setController(Controller.ILI9341_2_8);
Assert.assertEquals(18, display.getControlPointCount());
}
@Test
public void pinsSitAtHeaderPitchAlongEachRow() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
int primaryCount = controller.getPinNames().length;
MakerBoardTestSupport.assertRow(display, 0, primaryCount - 1);
if (controller.hasSecondaryHeader()) {
MakerBoardTestSupport.assertRow(display, primaryCount,
primaryCount + controller.getSecondaryPinNames().length - 1);
}
}
}
/**
* The SD row faces the 14-pin row and is centred on the width like it. Both are worth pinning:
* the across-the-board figure is what the two offsets leave of the length rather than a
* measurement, and the horizontal placement was not supplied at all.
*/
@Test
public void theSdRowStandsOffTheOppositeEdge() {
for (Controller controller : Controller.values()) {
if (!controller.hasSecondaryHeader()) {
continue;
}
TFTDisplay display = of(controller);
Rectangle2D board = display.getBodyShape().getBounds2D();
int primaryCount = controller.getPinNames().length;
Point2D firstSd = display.getControlPoint(primaryCount);
double fromItsEdge = controller.isHeaderAtTop() ? board.getMaxY() - firstSd.getY()
: firstSd.getY() - board.getY();
Assert.assertEquals(controller + " SD row offset from its own edge",
controller.getSecondaryHeaderOffsetMm(), mm(fromItsEdge), 0.01d);
double between = controller.isHeaderAtTop()
? firstSd.getY() - display.getControlPoint(0).getY()
: display.getControlPoint(0).getY() - firstSd.getY();
Assert.assertEquals(controller + " rows are not on opposite edges",
controller.getBoardLengthMm() - controller.getHeaderOffsetMm()
- controller.getSecondaryHeaderOffsetMm(),
mm(between), 0.01d);
Point2D lastSd = display.getControlPoint(display.getControlPointCount() - 1);
Assert.assertEquals(controller + " SD row is not centred across the board",
firstSd.getX() - board.getX(), board.getMaxX() - lastSd.getX(), 0.01d);
// Short enough to pass between the mounting holes it shares an edge with, which is why the
// top pair keeps the shared inset where the 2.4"'s header-side pair could not.
double holeRim = controller.getHoleSideInsetMm() + controller.getHoleSizeMm() / 2.0;
Assert.assertTrue(controller + " SD row runs into the holes on its edge",
mm(firstSd.getX() - board.getX()) > holeRim);
Assert.assertTrue(controller + " SD row runs into the holes on its edge",
mm(board.getMaxX() - lastSd.getX()) > holeRim);
}
}
/** A pin array longer than its board would hang the header off both ends. */
@Test
public void headerFitsAcrossTheBoard() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Rectangle2D board = display.getBodyShape().getBounds2D();
for (int i = 0; i < display.getControlPointCount(); i++) {
Point2D pin = display.getControlPoint(i);
Assert.assertTrue(controller + " pin " + i + " hangs off the board",
pin.getX() >= board.getX() && pin.getX() <= board.getMaxX());
}
}
}
@Test
public void screenFitsWithinTheBoard() {
for (Controller controller : Controller.values()) {
Assert.assertTrue(controller + " screen is wider than its board",
controller.getScreenWidthMm() < controller.getBoardWidthMm());
Assert.assertTrue(controller + " screen is longer than its board",
controller.getScreenLengthMm() < controller.getBoardLengthMm());
}
}
/**
* The screen is centred between the hole rows, so moving a board's holes has to be rechecked: on
* the 1.8" the two pairs are only 3 mm in and the panel very nearly reaches them. The rows need
* not share an inset, so the band is taken from both figures rather than from the board's middle.
*/
@Test
public void screenClearsTheMountingHoles() {
for (Controller controller : Controller.values()) {
if (!controller.hasMountingHoles()) {
continue;
}
double holeRadius = px(controller.getHoleSizeMm()) / 2.0;
double boardH = px(controller.getBoardLengthMm());
double bandTop = px(topInsetOf(controller));
double bandBottom = boardH - px(bottomInsetOf(controller));
double panelH = panelLengthOf(controller);
double panelTop = panelTopOf(controller);
double panelBottom = panelTop + panelH;
Assert.assertTrue(controller + " panel overlaps the holes on its top edge",
panelTop >= bandTop + holeRadius);
Assert.assertTrue(controller + " panel overlaps the holes on its bottom edge",
panelBottom <= bandBottom - holeRadius);
}
}
/**
* No mounting hole may be drilled where a pin is. This has to be two-dimensional to say anything
* useful: the 2.4"'s 14-pin row spans 33 mm of a 42.72 mm board, so its end pads reach the hole
* columns and that pair had to move, while the wider 2.8"'s row passes inside them. A one-axis
* clearance would have forced a deeper inset on boards that do not need one.
*/
@Test
public void mountingHolesDoNotOverlapAnyPin() {
double pin = AbstractMakerBoard.PIN_SIZE.convertToPixels();
for (Controller controller : Controller.values()) {
if (!controller.hasMountingHoles()) {
continue;
}
TFTDisplay display = of(controller);
Rectangle2D board = display.getBodyShape().getBounds2D();
double inset = px(controller.getHoleInsetMm());
double sideInset = px(controller.getHoleSideInsetMm());
double headerSide = px(controller.getHeaderSideHoleInsetMm());
double size = px(controller.getHoleSizeMm());
double topInset = controller.isHeaderAtTop() ? headerSide : inset;
double bottomInset = controller.isHeaderAtTop() ? inset : headerSide;
Rectangle2D[] holes = new Rectangle2D[] {
hole(board.getX() + sideInset, board.getY() + topInset, size),
hole(board.getMaxX() - sideInset, board.getY() + topInset, size),
hole(board.getX() + sideInset, board.getMaxY() - bottomInset, size),
hole(board.getMaxX() - sideInset, board.getMaxY() - bottomInset, size)};
for (int i = 0; i < display.getControlPointCount(); i++) {
Point2D p = display.getControlPoint(i);
Rectangle2D pad =
new Rectangle2D.Double(p.getX() - pin / 2.0, p.getY() - pin / 2.0, pin, pin);
for (Rectangle2D h : holes) {
Assert.assertFalse(controller + " a mounting hole is drilled through pin " + i,
h.intersects(pad));
}
}
}
}
private static Rectangle2D hole(double cx, double cy, double size) {
return new Rectangle2D.Double(cx - size / 2.0, cy - size / 2.0, size, size);
}
private static double topInsetOf(Controller controller) {
return controller.isHeaderAtTop() ? controller.getHeaderSideHoleInsetMm()
: controller.getHoleInsetMm();
}
private static double bottomInsetOf(Controller controller) {
return controller.isHeaderAtTop() ? controller.getHoleInsetMm()
: controller.getHeaderSideHoleInsetMm();
}
/** The dark panel where there is one, and the lit area alone where there is not. */
private static double panelLengthOf(Controller controller) {
return px(controller.hasGlass() ? controller.getGlass().getLengthMm()
: controller.getScreenLengthMm());
}
/** Mirrors the drawing rule: every panel is centred between the hole rows. */
private static double panelTopOf(Controller controller) {
double bandTop = controller.hasMountingHoles() ? px(topInsetOf(controller))
: px(controller.getHeaderOffsetMm());
double bandBottom = controller.hasMountingHoles()
? px(controller.getBoardLengthMm() - bottomInsetOf(controller))
: px(controller.getBoardLengthMm());
return (bandTop + bandBottom) / 2.0 - panelLengthOf(controller) / 2.0;
}
/**
* The panel has to leave the pin row uncovered. Being placed between the hole rows rather than
* from an edge, a long panel on a board whose holes sit close to the edges can reach back over
* its own header: the 1.54" clears by 3.51 mm with its row only 1.5 mm in. The 2.4" is the only
* board that must clear a row at each end.
*/
@Test
public void panelLeavesThePinRowUncovered() {
for (Controller controller : Controller.values()) {
if (controller.isRound()) {
continue;
}
double boardH = controller.getBoardLengthMm();
double panelTop = mm(panelTopOf(controller));
double panelBottom = panelTop + mm(panelLengthOf(controller));
double mainRow = controller.isHeaderAtTop() ? controller.getHeaderOffsetMm()
: boardH - controller.getHeaderOffsetMm();
if (controller.isHeaderAtTop()) {
Assert.assertTrue(controller + " panel covers the main row", panelTop > mainRow);
} else {
Assert.assertTrue(controller + " panel covers the main row", panelBottom < mainRow);
}
if (controller.hasSecondaryHeader()) {
Assert.assertTrue(controller + " panel covers the SD row",
panelTop > controller.getSecondaryHeaderOffsetMm());
}
}
}
/**
* Every board with a bezel but the 0.96" states its own lit area: their panels carry the driver's
* bonding region along the bottom, so the centring rule would drop it, by 4.85 mm on the 2.4"
* down to 1.5 mm on the 1.54". The assertion is that the two disagree -- if a correction ever
* makes them agree, the field has stopped earning its place.
*/
@Test
public void theLitAreaIsMeasuredOnlyWhereCentringWouldBeWrong() {
for (Controller controller : Controller.values()) {
Assert.assertEquals(controller + " measured lit area",
controller.hasGlass() && controller != Controller.ST7735_0_96,
controller.hasMeasuredScreenTop());
if (!controller.hasMeasuredScreenTop()) {
continue;
}
double glassH = controller.getGlass().getLengthMm();
double glassTop = mm(panelTopOf(controller));
double screenTop = controller.getScreenTopMm();
Assert.assertTrue(controller + " lit area starts above its glass", screenTop >= glassTop);
Assert.assertTrue(controller + " lit area runs past its glass",
screenTop + controller.getScreenLengthMm() <= glassTop + glassH);
Assert.assertTrue(controller + " is measured where centring would have done",
Math.abs(screenTop - (glassTop + (glassH - controller.getScreenLengthMm()) / 2.0)) > 1d);
// Each figure is stored from the board's top edge but was arrived at against the bezel, so it
// holds only while the glass stays where the hole rows put it. Move a hole row and these are
// the assertions that say the stored figure has to be rederived.
if (controller == Controller.ILI9341_2_4 || controller == Controller.ILI9341_2_8) {
double frameAbove = controller == Controller.ILI9341_2_4 ? 2.76d : 3.05d;
Assert.assertEquals(controller + " frame above the lit area", frameAbove,
screenTop - glassTop, 0.01d);
} else {
double lift = controller == Controller.ST7789_1_54 ? 1.5d : 2.0d;
Assert.assertEquals(controller + " lift off the centred lit area", lift,
glassTop + (glassH - controller.getScreenLengthMm()) / 2.0 - screenTop, 0.01d);
}
}
}
/**
* Two boards carry a bezel narrower than their PCB and only the 0.96" has it off centre, pushed
* right so the driver's circuitry has the bare L it leaves. The offset is across the board only,
* and is a measurement: centring it across too would move it 1.15 mm. The lit area is centred
* inside it with a 1 mm frame, which would silently break if it went back to being centred on
* the board.
*/
@Test
public void theOffsetPanelIsPlacedFromTheRightEdge() {
for (Controller controller : Controller.values()) {
boolean narrower = controller == Controller.ST7735_0_96
|| controller == Controller.ST7735_1_8 || controller == Controller.ST7735_1_8_SD;
Assert.assertEquals(controller + " panel spans the board", !narrower,
!controller.hasGlass() || controller.getGlass().spansTheBoard());
Assert.assertEquals(controller + " panel is placed from the right edge",
controller == Controller.ST7735_0_96,
controller.hasGlass() && controller.getGlass().isPlacedFromTheRightEdge());
}
Controller controller = Controller.ST7735_0_96;
TFTDisplay.Glass glass = controller.getGlass();
double leftMargin = controller.getBoardWidthMm() - glass.getRightInsetMm() - glass.getWidthMm();
Assert.assertEquals("left margin", 4.3d, leftMargin, 0.01d);
Assert.assertTrue("the bezel is where centring would have put it",
Math.abs(leftMargin - (controller.getBoardWidthMm() - glass.getWidthMm()) / 2.0) > 0.1d);
Assert.assertEquals("frame left and right", 1.0d,
(glass.getWidthMm() - controller.getScreenWidthMm()) / 2.0, 0.01d);
Assert.assertEquals("frame top and bottom", 1.0d,
(glass.getLengthMm() - controller.getScreenLengthMm()) / 2.0, 0.01d);
Assert.assertFalse("its lit area is measured, not derived from a centred glass",
controller.hasMeasuredScreenTop());
}
@Test
public void mountingHolesSitInsideTheBoard() {
for (Controller controller : Controller.values()) {
if (!controller.hasMountingHoles()) {
continue;
}
double holeRadius = controller.getHoleSizeMm() / 2.0;
Assert.assertTrue(controller + " holes breach an edge",
controller.getHoleInsetMm() >= holeRadius);
Assert.assertTrue(controller + " far holes breach the bottom edge",
controller.getHoleInsetMm() + holeRadius <= controller.getBoardLengthMm());
}
}
/**
* Each board's holes are one drill at one inset from each of the two edges nearest them, but the
* figures are the board's own rather than the package's. The 2.8" had a narrower 2.5 mm drill
* for a while and this guards against it coming back. The ILI9341s' deeper header-side pair is
* asserted separately so the two cannot be read as one rule.
*/
@Test
public void mountingHoleFiguresAreEachBoardsOwn() {
double[][] expected = {{3.0d, 3.0d}, {3.0d, 3.0d}, {2.5d, 2.0d}, {2.5d, 2.0d}, {3.0d, 2.0d},
{2.5d, 2.0d}};
Controller[] withHoles = {Controller.ILI9341_2_8, Controller.ILI9341_2_4,
Controller.ST7735_0_96, Controller.ST7735_1_8, Controller.ST7735_1_8_SD,
Controller.ST7789_1_54};
for (int i = 0; i < withHoles.length; i++) {
Assert.assertEquals(withHoles[i] + " inset", expected[i][0], withHoles[i].getHoleInsetMm(),
0.01d);
Assert.assertEquals(withHoles[i] + " diameter", expected[i][1], withHoles[i].getHoleSizeMm(),
0.01d);
}
for (Controller controller : Controller.values()) {
double expectedHeaderSide = controller.getHoleInsetMm();
if (controller == Controller.ILI9341_2_4) {
expectedHeaderSide = 6.92d;
} else if (controller == Controller.ILI9341_2_8) {
expectedHeaderSide = 6.50d;
}
Assert.assertEquals(controller + " header-side inset", expectedHeaderSide,
controller.getHeaderSideHoleInsetMm(), 0.01d);
double expectedSide = controller == Controller.ST7735_1_8_SD ? 2.75d
: controller.getHoleInsetMm();
Assert.assertEquals(controller + " side inset", expectedSide,
controller.getHoleSideInsetMm(), 0.01d);
}
// The one board whose holes are not square to the outline was given as two hole-to-hole spans
// rather than as insets, so those are what the three figures have to come back to.
Controller sd = Controller.ST7735_1_8_SD;
Assert.assertEquals("1.8\" SD holes across", 28.5d,
sd.getBoardWidthMm() - 2 * sd.getHoleSideInsetMm(), 0.01d);
Assert.assertEquals("1.8\" SD holes down", 52.0d,
sd.getBoardLengthMm() - sd.getHoleInsetMm() - sd.getHeaderSideHoleInsetMm(), 0.01d);
}
/** Without this the BOM collapses every variant of the part into a single row. */
@Test
public void bomValueCarriesTheController() {
for (Controller controller : Controller.values()) {
Assert.assertEquals(controller.toString(), of(controller).getValueForDisplay());
}
Assert.assertNotEquals(of(Controller.ILI9341_2_8).getValueForDisplay(),
of(Controller.ST7735_1_8).getValueForDisplay());
}
/**
* The 1.8" is the board whose bezel was measured down the board as well as across it, and the
* family's centring rule is what places it, so the two have to keep agreeing.
*/
@Test
public void glassPanelFollowsTheModule() {
Assert.assertTrue(Controller.ILI9341_2_8.hasGlass());
Assert.assertTrue(Controller.ST7789_1_54.hasGlass());
Assert.assertTrue(Controller.ST7735_1_8.hasGlass());
Assert.assertFalse(Controller.GC9A01_1_28.hasGlass());
Assert.assertEquals("1.8\" bezel from the top edge", 5.1d,
mm(panelTopOf(Controller.ST7735_1_8)), 0.02d);
}
@Test
public void onlyTheRoundBoardHasATab() {
Assert.assertFalse(Controller.ILI9341_2_8.isRound());
Assert.assertFalse(Controller.ST7735_1_8.isRound());
Assert.assertFalse(Controller.ST7789_1_54.isRound());
Assert.assertTrue(Controller.GC9A01_1_28.isRound());
Assert.assertEquals(22.9d, Controller.GC9A01_1_28.getTabWidthMm(), 0.01d);
}
/** The projection is the outline's length beyond its width, not a figure of its own. */
@Test
public void tabProjectionFollowsFromTheOutline() {
Assert.assertEquals(7.5d, Controller.GC9A01_1_28.getTabProjectionMm(), 0.01d);
}
@Test
public void roundBoardHasNoMountingHoles() {
Assert.assertFalse(Controller.GC9A01_1_28.hasMountingHoles());
}
/** The round board reads RST first, which is to say leftmost. */
@Test
public void roundPinNamesComeFromTheController() {
TFTDisplay display = of(Controller.GC9A01_1_28);
String[] expected = new String[] {"RST", "CS", "DC", "SDA", "SCL", "GND", "VCC"};
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i], display.getControlPointNodeName(i));
}
}
/**
* A disc with a narrower tab below it, so the corners of the bounding box are off the board
* while the middles of the top and bottom edges are on it. Taking the tab only as far as the
* bottom of the disc would leave a notch either side of the tangent point, which these corners
* would not catch but the centre-line check would.
*/
@Test
public void roundBodyIsADiscWithATab() {
TFTDisplay display = of(Controller.GC9A01_1_28);
Shape body = display.getBodyShape();
Rectangle2D bounds = body.getBounds2D();
Assert.assertEquals("disc diameter across", 38.0d, mm(bounds.getWidth()), 0.01d);
Assert.assertEquals("disc top to tab tip", 45.5d, mm(bounds.getHeight()), 0.01d);
double x = bounds.getX();
double y = bounds.getY();
Assert.assertTrue("the top of the disc is off the board",
body.contains(x + px(19.0d), y + px(1.0d)));
Assert.assertTrue("the middle of the tab is off the board",
body.contains(x + px(19.0d), y + px(44.5d)));
Assert.assertFalse("the top left corner is on the board",
body.contains(x + px(1.0d), y + px(1.0d)));
Assert.assertFalse("the bottom left corner is on the board",
body.contains(x + px(1.0d), y + px(44.5d)));
// at the centre line the board is wider than the tab, which proves the union took the disc's
// outline; a third further down only the tab is left and the same point is off the board
Assert.assertTrue("the disc is no wider than its tab",
body.contains(x + px(2.0d), y + px(19.0d)));
Assert.assertFalse("the board is as wide as its disc below the centre line",
body.contains(x + px(2.0d), y + px(30.0d)));
}
/**
* Which edge the main row leaves by is a fact about each board rather than a drawing convention:
* with the pin order fixed left to right, a header at the top and one at the bottom are mirror
* images and not the same part. Asserted for every variant because nothing else in the geometry
* would notice one being flipped.
*/
@Test
public void headerSitsOnItsOwnEdge() {
for (Controller controller : Controller.values()) {
TFTDisplay display = of(controller);
Rectangle2D bounds = display.getBodyShape().getBounds2D();
Point2D firstPin = display.getControlPoint(0);
double fromEdge = controller.isHeaderAtTop() ? firstPin.getY() - bounds.getY()
: bounds.getMaxY() - firstPin.getY();
Assert.assertEquals(controller + " header offset", controller.getHeaderOffsetMm(),
mm(fromEdge), 0.01d);
}
// A roll call rather than a rule: a future variant must be sourced from the part too rather
// than inheriting whichever way the majority happens to go.
Assert.assertTrue("0.96\"", Controller.ST7735_0_96.isHeaderAtTop());
Assert.assertTrue("1.8\"", Controller.ST7735_1_8.isHeaderAtTop());
Assert.assertTrue("1.54\"", Controller.ST7789_1_54.isHeaderAtTop());
Assert.assertFalse("2.8\"", Controller.ILI9341_2_8.isHeaderAtTop());
Assert.assertFalse("2.4\"", Controller.ILI9341_2_4.isHeaderAtTop());
Assert.assertFalse("1.28\" round", Controller.GC9A01_1_28.isHeaderAtTop());
Assert.assertFalse("1.8\" with the socket", Controller.ST7735_1_8_SD.isHeaderAtTop());
}
/**
* The round one is the only variant whose text has to fit a circle, which it does by being given
* the square inscribed in the glass.
*/
@Test
public void everyPanelPrintsItsDescription() {
for (Controller controller : Controller.values()) {
TFTDisplay printed = of(controller);
TFTDisplay blank = of(controller);
blank.setScreen(Display.NONE);
Assert.assertEquals(Display.VALUE, printed.getScreen());
MakerBoardTestSupport.assertScreenTextIsDrawn(printed, blank);
}
}
}
@@ -0,0 +1,215 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the single-LED breakout. Three things compete for a board half an inch by
* four tenths: a three-pad column up each short edge, the two mounting holes between them, and
* the package in the middle. The holes leave 0.27 mm of board outside each rim and 0.31 mm
* between a rim and the package, so what is asserted is that none has taken another's room.
*/
public class WS2812BBreakoutTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static Rectangle2D padRect(WS2812BBreakout breakout, int index) {
Point2D pad = breakout.getControlPoint(index);
double w = WS2812BBreakout.PAD_WIDTH.convertToPixels();
double h = WS2812BBreakout.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
}
private static Rectangle2D holeRect(WS2812BBreakout breakout, boolean bottom) {
Point2D hole = breakout.getHoleCentre(bottom);
double r = WS2812BBreakout.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(hole.getX() - r, hole.getY() - r, r * 2, r * 2);
}
private static Rectangle2D ledPackage(WS2812BBreakout breakout) {
Point2D led = breakout.getLedCentre();
double half = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, half * 2, half * 2);
}
@Test
public void boardIsHalfAnInchByFourTenths() {
MakerBoardTestSupport.assertBoardSize(new WS2812BBreakout(), WS2812BBreakout.BOARD_WIDTH,
WS2812BBreakout.BOARD_HEIGHT);
}
/** Ground, power and a data line up each long edge, and no two pads sharing a name. */
@Test
public void eachEdgeCarriesThreeDistinctlyNamedPads() {
WS2812BBreakout breakout = new WS2812BBreakout();
Assert.assertEquals(6, breakout.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Assert.assertTrue("duplicate node name " + breakout.getControlPointNodeName(i),
names.add(breakout.getControlPointNodeName(i)));
}
Assert.assertEquals("IN", breakout.getControlPointNodeName(2));
Assert.assertEquals("OUT", breakout.getControlPointNodeName(5));
}
/** The two grounds are one net on the board, as are the two supplies. */
@Test
public void theNumberedPadsReadAsOneName() {
WS2812BBreakout breakout = new WS2812BBreakout();
for (int i : new int[] {0, 3}) {
Assert.assertEquals("GND",
AbstractMakerBoard.getDisplayPinLabel(breakout.getControlPointNodeName(i)));
}
for (int i : new int[] {1, 4}) {
Assert.assertEquals("VIN",
AbstractMakerBoard.getDisplayPinLabel(breakout.getControlPointNodeName(i)));
}
}
@Test
public void padColumnsRunUpTheShortEdgesAtHeaderPitch() {
WS2812BBreakout breakout = new WS2812BBreakout();
MakerBoardTestSupport.assertRow(breakout, 0, 2);
MakerBoardTestSupport.assertRow(breakout, 3, 5);
Rectangle2D board = breakout.getBodyShape().getBounds2D();
for (int i = 0; i < 3; i++) {
Assert.assertEquals("left pad " + i, board.getX(), padRect(breakout, i).getX(), 0.01d);
Assert.assertEquals("right pad " + i, board.getMaxX(), padRect(breakout, 3 + i).getMaxX(),
0.01d);
}
}
@Test
public void holesSitOnTheCentreLineASpanApartAstrideTheLed() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D board = breakout.getBodyShape().getBounds2D();
Point2D top = breakout.getHoleCentre(false);
Point2D bottom = breakout.getHoleCentre(true);
Assert.assertEquals("span", WS2812BBreakout.MOUNTING_HOLE_SPACING.convertToPixels(),
bottom.getY() - top.getY(), 0.01d);
Assert.assertEquals("not centred across the board", board.getCenterX(), top.getX(), 0.01d);
Assert.assertEquals("not centred along the board", board.getCenterY(),
(top.getY() + bottom.getY()) / 2.0, 0.01d);
}
/**
* Asserted as the figure it works out to rather than as mere containment, a quarter of a
* millimetre being the whole of it.
*/
@Test
public void aQuarterMillimetreOfBoardSurvivesOutsideEachHole() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D board = breakout.getBodyShape().getBounds2D();
Assert.assertEquals("above the top hole", 0.27d,
mm(holeRect(breakout, false).getY() - board.getY()), 0.01d);
Assert.assertEquals("below the bottom hole", 0.27d,
mm(board.getMaxY() - holeRect(breakout, true).getMaxY()), 0.01d);
}
/**
* 0.31 mm of board separates a rim from the package, so this fails on any revision that brings
* the two together rather than letting them overlap unnoticed.
*/
@Test
public void thePackageClearsTheHolesAndThePads() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D pack = ledPackage(breakout);
for (boolean bottom : new boolean[] {false, true}) {
Assert.assertFalse("the package sits over a mounting hole",
pack.intersects(holeRect(breakout, bottom)));
}
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Assert.assertFalse("the package runs under pad " + i,
pack.intersects(padRect(breakout, i)));
}
}
@Test
public void theHolesClearThePads() {
WS2812BBreakout breakout = new WS2812BBreakout();
for (boolean bottom : new boolean[] {false, true}) {
Rectangle2D hole = holeRect(breakout, bottom);
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Assert.assertFalse("a mounting hole runs under pad " + i,
hole.intersects(padRect(breakout, i)));
}
}
}
@Test
public void everythingStaysOnTheBoard() {
WS2812BBreakout breakout = new WS2812BBreakout();
Rectangle2D board = breakout.getBodyShape().getBounds2D();
for (boolean bottom : new boolean[] {false, true}) {
Rectangle2D hole = holeRect(breakout, bottom);
Assert.assertTrue("a mounting hole breaches the board edge",
board.contains(hole.getX(), hole.getY(), hole.getWidth(), hole.getHeight()));
}
Rectangle2D pack = ledPackage(breakout);
Assert.assertTrue("the package hangs off the board",
board.contains(pack.getX(), pack.getY(), pack.getWidth(), pack.getHeight()));
for (int i = 0; i < breakout.getControlPointCount(); i++) {
Rectangle2D pad = padRect(breakout, i);
Assert.assertTrue("pad " + i + " runs off the board",
board.contains(pad.getX(), pad.getY(), pad.getWidth(), pad.getHeight()));
}
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BBreakout());
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BBreakout::new);
}
}
@@ -0,0 +1,257 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the Jewel. The arrangement matters more than the dimensions: five pads share
* six gaps between the ring LEDs, so one gap is empty, and getting the wrong one empty would
* still place five pads on the right circle and pass any count or radius check.
*/
public class WS2812BJewelTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static double mmToPx(double value) {
return new Size(value, SizeUnit.mm).convertToPixels();
}
private static Point2D centerOf(WS2812BJewel jewel) {
Rectangle2D bounds = jewel.getBodyShape().getBounds2D();
return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
}
/** The ring LED centres, recomputed from the constants the way {@code draw} places them. */
private static Point2D[] ringLeds(WS2812BJewel jewel) {
Point2D center = centerOf(jewel);
double ledR = WS2812BJewel.LED_CIRCLE_DIAMETER.convertToPixels() / 2.0;
Point2D[] leds = new Point2D[WS2812BJewel.RING_LED_COUNT];
for (int i = 0; i < leds.length; i++) {
double angle = 2 * Math.PI * i / WS2812BJewel.RING_LED_COUNT - Math.PI / 2.0;
leds[i] = new Point2D.Double(center.getX() + ledR * Math.cos(angle),
center.getY() + ledR * Math.sin(angle));
}
return leds;
}
private static Rectangle2D ledRect(Point2D led) {
double half = WS2812BJewel.RGB_LED_SIZE.convertToPixels() / 2.0;
double size = half * 2;
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, size, size);
}
/** The shared round pad's bounding box; pads are not turned to face the centre. */
private static Rectangle2D padRect(Point2D pad) {
double size = WS2812BJewel.PAD_SIZE.convertToPixels();
return new Rectangle2D.Double(pad.getX() - size / 2.0, pad.getY() - size / 2.0, size, size);
}
@Test
public void fivePadsAreTheControlPoints() {
Assert.assertEquals(5, new WS2812BJewel().getControlPointCount());
}
@Test
public void padNamesAreTheClockwiseOrderFromOut() {
WS2812BJewel jewel = new WS2812BJewel();
String[] expected = new String[] {"OUT", "G_1", "G_2", "PWR", "IN"};
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pad " + i, expected[i], jewel.getControlPointNodeName(i));
}
}
/** The two grounds are distinct nodes; the silk would read both as GND. */
@Test
public void theTwoGroundsAreSeparateNodes() {
WS2812BJewel jewel = new WS2812BJewel();
Assert.assertNotEquals(jewel.getControlPointNodeName(1), jewel.getControlPointNodeName(2));
}
@Test
public void bodyIsATwentyThreeMillimetreDisc() {
Rectangle2D bounds = new WS2812BJewel().getBodyShape().getBounds2D();
Assert.assertEquals("diameter across", 23.0d, mm(bounds.getWidth()), 0.01d);
Assert.assertEquals("a disc, not an ellipse", bounds.getWidth(), bounds.getHeight(), 0.01d);
}
/**
* Ties {@code getPadRadius} to {@code getCenter}: if the two disagreed the disc would be drawn
* off its own control points.
*/
@Test
public void everyPadSitsOnTheNineMillimetreCircle() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Assert.assertEquals("pad " + i + " off the pad circle", 4.5d,
mm(center.distance(jewel.getControlPoint(i))), 0.01d);
}
}
/**
* Pads belong in the gaps between consecutive ring LEDs, and with five pads in six gaps the one
* left empty must be the one due left, where the left mounting hole is.
*/
@Test
public void padsFillEveryGapButTheOneDueLeft() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
boolean[] occupied = new boolean[WS2812BJewel.RING_LED_COUNT];
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Point2D pad = jewel.getControlPoint(i);
double angle = Math.atan2(pad.getY() - center.getY(), pad.getX() - center.getX());
// gap g is centred half a step past LED g, LED 0 being at the top
double steps = (angle + Math.PI / 2.0) / (2 * Math.PI / WS2812BJewel.RING_LED_COUNT) - 0.5;
long whole = Math.round(steps);
Assert.assertEquals("pad " + i + " does not sit in the middle of a gap", whole, steps,
0.001d);
int gap = (int) (((whole % WS2812BJewel.RING_LED_COUNT) + WS2812BJewel.RING_LED_COUNT)
% WS2812BJewel.RING_LED_COUNT);
Assert.assertFalse("two pads in gap " + gap, occupied[gap]);
occupied[gap] = true;
}
// gap 4 spans LED 4 to LED 5, so on a hexagon anchored at the top it is the one pointing left
for (int gap = 0; gap < occupied.length; gap++) {
if (gap == 4) {
Assert.assertFalse("the empty gap should be the one due left", occupied[gap]);
} else {
Assert.assertTrue("gap " + gap + " has no pad", occupied[gap]);
}
}
}
@Test
public void theEmptyGapPointsAtAMountingHole() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
// the left hole sits at pi from the centre, and no pad may lie along that radius
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Point2D pad = jewel.getControlPoint(i);
double angle = Math.atan2(pad.getY() - center.getY(), pad.getX() - center.getX());
Assert.assertTrue("pad " + i + " lies along the left mounting hole's radius",
Math.PI - Math.abs(angle) > 0.1d);
}
}
/** No pad may sit under a package, the centre LED included: it is nearest to the pads. */
@Test
public void noPadSitsUnderAnLed() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
Point2D[] leds = ringLeds(jewel);
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Rectangle2D pad = padRect(jewel.getControlPoint(i));
Assert.assertFalse("pad " + i + " sits under the centre LED",
pad.intersects(ledRect(center)));
for (int l = 0; l < leds.length; l++) {
Assert.assertFalse("pad " + i + " sits under ring LED " + l,
pad.intersects(ledRect(leds[l])));
}
}
}
@Test
public void everyPadAndLedSitsOnTheDisc() {
WS2812BJewel jewel = new WS2812BJewel();
Shape disc = jewel.getBodyShape();
Point2D center = centerOf(jewel);
double outerR = WS2812BJewel.OUTER_DIAMETER.convertToPixels() / 2.0;
for (int i = 0; i < jewel.getControlPointCount(); i++) {
Assert.assertTrue("pad " + i + " hangs off the disc",
disc.contains(padRect(jewel.getControlPoint(i))));
}
for (Point2D led : ringLeds(jewel)) {
Assert.assertTrue("an LED package hangs off the rim",
center.distance(led) + WS2812BJewel.RGB_LED_SIZE.convertToPixels() / 2.0 <= outerR);
}
}
/**
* The holes are 19 mm apart on a 23 mm disc, clearing the rim by 0.5 mm and coming within a
* fraction of a millimetre of the neighbouring packages, which is why this asserts containment
* rather than any comfortable clearance.
*/
@Test
public void mountingHolesSitInsideTheDisc() {
WS2812BJewel jewel = new WS2812BJewel();
Point2D center = centerOf(jewel);
double holeOffset = WS2812BJewel.MOUNTING_HOLE_SPACING.convertToPixels() / 2.0;
double holeR = WS2812BJewel.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
double outerR = WS2812BJewel.OUTER_DIAMETER.convertToPixels() / 2.0;
Assert.assertEquals("holes 19 mm apart", 19.0d, mm(holeOffset * 2), 0.01d);
Assert.assertTrue("mounting holes breach the rim", holeOffset + holeR <= outerR);
Assert.assertTrue("a hole is not inside the disc",
jewel.getBodyShape().contains(center.getX() - holeOffset - holeR, center.getY() - holeR,
holeR * 2, holeR * 2));
}
/** The package it is fitted with is the only thing to tell a buyer about this one. */
@Test
public void bomValueNamesThePackage() {
Assert.assertEquals("RGB", new WS2812BJewel().getValueForDisplay());
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BJewel::new);
}
@Test
public void padsAreAllDistinctPositions() {
WS2812BJewel jewel = new WS2812BJewel();
for (int i = 0; i < jewel.getControlPointCount(); i++) {
for (int j = i + 1; j < jewel.getControlPointCount(); j++) {
Assert.assertTrue("pads " + i + " and " + j + " coincide",
jewel.getControlPoint(i).distance(jewel.getControlPoint(j)) > mmToPx(0.5d));
}
}
}
}
@@ -0,0 +1,276 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.displays.WS2812BPanel.Silk;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the NeoPixel panel. Dimensions can be checked against the board file they
* came from; the arrangement cannot, so that is what is pinned here -- the two ports are each
* other turned about the centre, and ports and holes sit in the gaps of the pixel grid rather
* than on bare margin. Each of those passes whatever the dimensions are.
*/
public class WS2812BPanelTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double px(double millimetres) {
return millimetres * PX_PER_MM;
}
private static final int ORDER = WS2812BPanel.MATRIX_ORDER;
/** The board file's hole pattern, in millimetres from the middle of the board. */
private static final double[][] HOLES_MM = new double[][] {
{-33.02d, 17.78d}, {-33.02d, 0d}, {-33.02d, -17.78d},
{33.02d, 17.78d}, {33.02d, 0d}, {33.02d, -17.78d},
{-8.89d, 26.67d}, {8.89d, 26.67d},
{-8.89d, 0d}, {8.89d, 0d},
{-8.89d, -26.67d}, {8.89d, -26.67d}};
/** Where a pixel's centre sits, counted along the data chain, in component coordinates. */
private static Point2D ledCentre(WS2812BPanel panel, int index) {
Rectangle2D board = panel.getBodyShape().getBounds2D();
double pitch = WS2812BPanel.PIXEL_PITCH.convertToPixels();
double margin = (board.getWidth() - (ORDER - 1) * pitch) / 2.0;
return new Point2D.Double(board.getX() + margin + (index % ORDER) * pitch,
board.getY() + margin + (index / ORDER) * pitch);
}
@Test
public void theBoardIsTheSquareItWasMeasuredAs() {
Rectangle2D board = new WS2812BPanel().getBodyShape().getBounds2D();
Assert.assertEquals(px(71.12d), board.getWidth(), 0.01d);
Assert.assertEquals("the board is not square", board.getWidth(), board.getHeight(), 0.01d);
}
/**
* The board is exactly eight pitches across, which puts a half-pitch of margin outside the outer
* pixels and lands the port row in the first row gap. A property of this board rather than of
* panels in general, so a later panel that does not share it should fail here.
*/
@Test
public void theBoardIsAWholeNumberOfPitchesAcross() {
Assert.assertEquals(ORDER * WS2812BPanel.PIXEL_PITCH.convertToPixels(),
WS2812BPanel.BOARD_SIZE.convertToPixels(), 0.01d);
}
@Test
public void thePixelGridIsCentredOnTheBoard() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
Point2D first = ledCentre(panel, 0);
Point2D last = ledCentre(panel, ORDER * ORDER - 1);
Assert.assertEquals("left margin", first.getX() - board.getX(),
board.getMaxX() - last.getX(), 0.01d);
Assert.assertEquals("top margin", first.getY() - board.getY(),
board.getMaxY() - last.getY(), 0.01d);
}
/**
* A progressive raster rather than the serpentine most panels use. The colours are handed out
* along the chain, so getting it wrong shows as a gradient that folds back every row.
*/
@Test
public void theChainIsAProgressiveRaster() {
WS2812BPanel panel = new WS2812BPanel();
Assert.assertEquals("the row does not advance along the chain", ledCentre(panel, 0).getY(),
ledCentre(panel, ORDER - 1).getY(), 0.01d);
Assert.assertEquals("the chain does not return to the left edge",
ledCentre(panel, 0).getX(), ledCentre(panel, ORDER).getX(), 0.01d);
Assert.assertEquals("the chain does not step down a row",
WS2812BPanel.PIXEL_PITCH.convertToPixels(),
ledCentre(panel, ORDER).getY() - ledCentre(panel, 0).getY(), 0.01d);
}
@Test
public void controlPointsAreTheTwoPortsInOrder() {
WS2812BPanel panel = new WS2812BPanel();
Assert.assertEquals(6, panel.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < panel.getControlPointCount(); i++) {
String name = panel.getControlPointNodeName(i);
Assert.assertNotNull("pin " + i, name);
Assert.assertTrue("duplicate node name " + name, names.add(name));
}
Assert.assertEquals("DIN", panel.getControlPointNodeName(0));
Assert.assertEquals("DOUT", panel.getControlPointNodeName(3));
}
/**
* Half a turn about the middle of the board, which is what lets panels be chained. Nothing in the
* drawing shows this is wrong; the output simply ends up where a wire cannot reach.
*/
@Test
public void thePortsAreEachOtherTurnedAboutTheCentre() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
for (int i = 0; i < 3; i++) {
Point2D in = panel.getControlPoint(i);
Point2D out = panel.getControlPoint(3 + i);
Assert.assertEquals("pad " + i + " x", 2 * board.getCenterX() - in.getX(), out.getX(),
0.01d);
Assert.assertEquals("pad " + i + " y", 2 * board.getCenterY() - in.getY(), out.getY(),
0.01d);
}
}
@Test
public void portPadsSitAtHeaderPitchAndStayOnTheBoard() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
double padHalf = AbstractMakerBoard.PAD_SIZE.convertToPixels() / 2.0;
MakerBoardTestSupport.assertRow(panel, 0, 2);
MakerBoardTestSupport.assertRow(panel, 3, 5);
for (int i = 0; i < panel.getControlPointCount(); i++) {
Point2D pad = panel.getControlPoint(i);
Assert.assertTrue("pad " + i + " runs off the board",
board.contains(pad.getX() - padHalf, pad.getY() - padHalf, padHalf * 2, padHalf * 2));
}
}
/**
* This board has no bare strip -- the grid reaches within half a pitch of every edge -- so a port
* drawn a row out would land under the packages.
*/
@Test
public void portsLieInTheGapBetweenTwoPixelRows() {
WS2812BPanel panel = new WS2812BPanel();
double firstRow = ledCentre(panel, 0).getY();
double secondRow = ledCentre(panel, ORDER).getY();
Assert.assertEquals("input port", (firstRow + secondRow) / 2.0,
panel.getControlPoint(0).getY(), 0.01d);
}
/**
* The holes are drilled in the gaps of the grid, leaving 3.89 mm of bare board for a 2.8 mm hole.
* A hole over a package is only visible in a render.
*/
@Test
public void mountingHolesClearThePixelsAndStayOnTheBoard() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
double holeR = WS2812BPanel.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
double packageHalf = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
// the holes are private to the part, so they are reached the way the drawing reaches them
for (int i = 0; i < ORDER * ORDER; i++) {
Point2D led = ledCentre(panel, i);
Rectangle2D pack = new Rectangle2D.Double(led.getX() - packageHalf,
led.getY() - packageHalf, packageHalf * 2, packageHalf * 2);
for (double[] hole : HOLES_MM) {
double hx = board.getCenterX() + px(hole[0]);
double hy = board.getCenterY() + px(hole[1]);
Assert.assertTrue("hole breaches the board edge",
board.contains(hx - holeR, hy - holeR, holeR * 2, holeR * 2));
Assert.assertFalse("hole sits under a package",
pack.intersects(hx - holeR, hy - holeR, holeR * 2, holeR * 2));
}
}
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BPanel());
}
@Test
public void bomValueNamesTheMatrix() {
Assert.assertEquals("8x8, 64 LEDs, RGB", new WS2812BPanel().getValueForDisplay());
}
/**
* Every printed line lies in a gap between two rows of pixels, the only bare board this part has.
* The positions come from the board file, but how much room a line takes is the font's business,
* so a size change could put lettering under a package without anything else noticing.
*/
@Test
public void silkscreenClearsThePixelsAndTheBoardEdge() {
WS2812BPanel panel = new WS2812BPanel();
Rectangle2D board = panel.getBodyShape().getBounds2D();
double packageHalf = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
Graphics2D g2d = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics();
FontRenderContext context = g2d.getFontRenderContext();
try {
for (Silk silk : WS2812BPanel.SILKSCREEN) {
// the inked rectangle rather than the line box, since leading and descender space the
// string does not use cannot collide with anything
Rectangle2D glyphs =
silk.getFont().createGlyphVector(context, silk.getText()).getVisualBounds();
Rectangle2D line = new Rectangle2D.Double(board.getCenterX() + px(silk.getXMm()),
board.getCenterY() + px(silk.getYMm()) - glyphs.getHeight() / 2.0, glyphs.getWidth(),
glyphs.getHeight());
Assert.assertTrue(silk.getText() + " runs off the board",
board.contains(line.getX(), line.getY(), line.getWidth(), line.getHeight()));
for (int i = 0; i < ORDER * ORDER; i++) {
Point2D led = ledCentre(panel, i);
Assert.assertFalse(silk.getText() + " runs under a package",
line.intersects(led.getX() - packageHalf, led.getY() - packageHalf,
packageHalf * 2, packageHalf * 2));
}
}
} finally {
g2d.dispose();
}
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BPanel::new);
}
}
@@ -0,0 +1,197 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard.LedType;
import org.diylc.components.displays.WS2812BRing.RingSize;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Basic checks for the NeoPixel ring. {@link RingSize}'s constructor guards the pad arrangement,
* throwing unless each ring's LED gaps sum to its LED count, so what is left is that the three
* sizes carry the pads and diameters they were measured with and that the body is concentric.
*/
public class WS2812BRingTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static WS2812BRing of(RingSize ringSize) {
WS2812BRing ring = new WS2812BRing();
ring.setRingSize(ringSize);
return ring;
}
@Test
public void bodyIsTheOuterDiameterOfEachRing() {
for (RingSize ringSize : RingSize.values()) {
Rectangle2D bounds = of(ringSize).getBodyShape().getBounds2D();
Assert.assertEquals(ringSize + " width", ringSize.getOuterDiameterMm(), mm(bounds.getWidth()),
0.01d);
Assert.assertEquals(ringSize + " height", ringSize.getOuterDiameterMm(),
mm(bounds.getHeight()), 0.01d);
}
}
/** Four pads on the 12-LED ring, six on the two larger ones, and no two sharing a name. */
@Test
public void padCountFollowsTheRingSize() {
Assert.assertEquals(4, of(RingSize._12_LED).getControlPointCount());
Assert.assertEquals(6, of(RingSize._16_LED).getControlPointCount());
Assert.assertEquals(6, of(RingSize._24_LED).getControlPointCount());
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Set<String> names = new HashSet<String>();
for (int i = 0; i < ring.getControlPointCount(); i++) {
String name = ring.getControlPointNodeName(i);
Assert.assertNotNull(ringSize + " pad " + i, name);
Assert.assertFalse(ringSize + " pad " + i + " is unnamed", name.isEmpty());
Assert.assertTrue(ringSize + " duplicate node name " + name, names.add(name));
}
}
}
/**
* Pads sit on a circle of their own rather than the LEDs'. This also says the body is concentric
* with them: the outline is placed by working backwards from the first pad, so a disagreement
* would show up as an off-centre radius here.
*/
@Test
public void everyPadSitsOnItsOwnCircleInsideTheRim() {
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Rectangle2D bounds = ring.getBodyShape().getBounds2D();
Point2D centre = new Point2D.Double(bounds.getCenterX(), bounds.getCenterY());
double expected = ringSize.getOuterDiameterMm() / 2.0
- mm(WS2812BRing.PAD_EDGE_INSET.convertToPixels());
for (int i = 0; i < ring.getControlPointCount(); i++) {
Assert.assertEquals(ringSize + " pad " + i + " radius", expected,
mm(centre.distance(ring.getControlPoint(i))), 0.01d);
}
}
}
@Test
public void ringSizeRoundTripsThroughTheSetter() {
Assert.assertEquals(RingSize._16_LED, new WS2812BRing().getRingSize());
for (RingSize ringSize : RingSize.values()) {
WS2812BRing ring = of(ringSize);
Assert.assertEquals(ringSize, ring.getRingSize());
Assert.assertTrue(ring.getValueForDisplay().startsWith(ringSize.toString()));
}
}
/** A ring saved before the property existed deserializes without one and has to read as RGB. */
@Test
public void ledTypeDefaultsToRgb() {
Assert.assertEquals(LedType.RGB, new WS2812BRing().getLedType());
WS2812BRing ring = new WS2812BRing();
ring.setLedType(null);
Assert.assertEquals(LedType.RGB, ring.getLedType());
}
/** Both halves name a different thing to buy, so the BOM's value column has to carry both. */
@Test
public void variantLabelCarriesSizeAndLedType() {
for (RingSize ringSize : RingSize.values()) {
for (LedType ledType : LedType.values()) {
WS2812BRing ring = of(ringSize);
ring.setLedType(ledType);
Assert.assertEquals(ringSize + ", " + ledType, ring.getValueForDisplay());
}
}
}
/** The packages are pin-compatible, so fitting a different one moves no pad. */
@Test
public void ledTypeLeavesTheFootprintAlone() {
for (RingSize ringSize : RingSize.values()) {
WS2812BRing reference = of(ringSize);
for (LedType ledType : LedType.values()) {
WS2812BRing ring = of(ringSize);
ring.setLedType(ledType);
MakerBoardTestSupport.assertSharedFootprint(reference, ring, ring.getControlPointCount());
}
}
}
/**
* Comparing renderings is what makes this a check on the drawing: a draw() that ignored the
* property would pass the getter.
*/
@Test
public void eachPackageIsDrawnDifferently() {
int[] rgb = renderWith(LedType.RGB);
Assert.assertFalse("RGBW should show its white die, and does not",
Arrays.equals(rgb, renderWith(LedType.RGBW_WARM)));
Assert.assertFalse("A WWA ring should not be drawn lit in colour",
Arrays.equals(rgb, renderWith(LedType.WWA)));
}
/** The three RGBW tints differ only in what you buy, so they are drawn the same. */
@Test
public void theRgbwTintsShareOneDrawing() {
int[] warm = renderWith(LedType.RGBW_WARM);
Assert.assertArrayEquals(warm, renderWith(LedType.RGBW_NATURAL));
Assert.assertArrayEquals(warm, renderWith(LedType.RGBW_COOL));
}
private static int[] renderWith(LedType ledType) {
WS2812BRing ring = of(RingSize._16_LED);
ring.setLedType(ledType);
return MakerBoardTestSupport.renderPixels(ring);
}
@Test
public void drawsCleanlyInEveryState() {
for (RingSize ringSize : RingSize.values()) {
for (LedType ledType : LedType.values()) {
WS2812BRing ring = of(ringSize);
ring.setLedType(ledType);
MakerBoardTestSupport.assertDrawsCleanly(ring);
}
}
}
}
@@ -0,0 +1,195 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the NeoPixel stick. Everything competes for 10.22 mm of height: the holes
* take the top, the packages what is left, and the pads run the whole of it at each end. None can
* move without crowding another, and nothing but a render shows it.
*/
public class WS2812BStickTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static Rectangle2D ledPackage(WS2812BStick stick, int index) {
Point2D led = stick.getLedCentre(index);
double half = AbstractMakerBoard.RGB_LED_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(led.getX() - half, led.getY() - half, half * 2, half * 2);
}
private static Rectangle2D padRect(WS2812BStick stick, int index) {
Point2D pad = stick.getControlPoint(index);
double w = WS2812BStick.PAD_WIDTH.convertToPixels();
double h = WS2812BStick.PAD_LENGTH.convertToPixels();
return new Rectangle2D.Double(pad.getX() - w / 2.0, pad.getY() - h / 2.0, w, h);
}
private static Rectangle2D holeRect(WS2812BStick stick, boolean right) {
Point2D hole = stick.getHoleCentre(right);
double r = WS2812BStick.MOUNTING_HOLE_SIZE.convertToPixels() / 2.0;
return new Rectangle2D.Double(hole.getX() - r, hole.getY() - r, r * 2, r * 2);
}
@Test
public void boardIsTheMeasuredSize() {
MakerBoardTestSupport.assertBoardSize(new WS2812BStick(), WS2812BStick.BOARD_WIDTH,
WS2812BStick.BOARD_HEIGHT);
}
/** Both grounds on an end are the same net on the board, so the node names must still differ. */
@Test
public void eachEndCarriesFourDistinctlyNamedPads() {
WS2812BStick stick = new WS2812BStick();
Assert.assertEquals(8, stick.getControlPointCount());
Set<String> names = new HashSet<String>();
for (int i = 0; i < stick.getControlPointCount(); i++) {
Assert.assertTrue("duplicate node name " + stick.getControlPointNodeName(i),
names.add(stick.getControlPointNodeName(i)));
}
Assert.assertEquals("DIN", stick.getControlPointNodeName(1));
Assert.assertEquals("DOUT", stick.getControlPointNodeName(5));
}
/**
* Surface copper flush with the cut ends rather than plated holes set in from them, so each
* column's outer edge lands exactly on the board edge: half a pad out and it overhangs, half a
* pad in and there is a bare strip the real board does not have.
*/
@Test
public void padsAreFlushWithTheCutEnds() {
WS2812BStick stick = new WS2812BStick();
Rectangle2D board = stick.getBodyShape().getBounds2D();
for (int i = 0; i < 4; i++) {
Assert.assertEquals("input pad " + i, board.getX(), padRect(stick, i).getX(), 0.01d);
Assert.assertEquals("output pad " + i, board.getMaxX(), padRect(stick, 4 + i).getMaxX(),
0.01d);
}
for (int i = 0; i < stick.getControlPointCount(); i++) {
Rectangle2D pad = padRect(stick, i);
Assert.assertTrue("pad " + i + " runs off the board",
board.contains(pad.getX(), pad.getY(), pad.getWidth(), pad.getHeight()));
}
}
/** A 1.5 mm pad on a 0.1 inch pitch leaves about a millimetre; any taller and they merge. */
@Test
public void padsInAColumnDoNotTouch() {
WS2812BStick stick = new WS2812BStick();
for (int i = 0; i < 3; i++) {
Assert.assertFalse("input pads " + i + " and " + (i + 1) + " overlap",
padRect(stick, i).intersects(padRect(stick, i + 1)));
Assert.assertFalse("output pads " + i + " and " + (i + 1) + " overlap",
padRect(stick, 4 + i).intersects(padRect(stick, 5 + i)));
}
}
@Test
public void mountingHolesAreASpanApartAstrideTheCentre() {
WS2812BStick stick = new WS2812BStick();
Rectangle2D board = stick.getBodyShape().getBounds2D();
Point2D left = stick.getHoleCentre(false);
Point2D right = stick.getHoleCentre(true);
Assert.assertEquals("span", WS2812BStick.MOUNTING_HOLE_SPACING.convertToPixels(),
right.getX() - left.getX(), 0.01d);
Assert.assertEquals("not centred", board.getCenterX(), (left.getX() + right.getX()) / 2.0,
0.01d);
Assert.assertEquals("depth from the top edge",
WS2812BStick.MOUNTING_HOLE_TOP_OFFSET.convertToPixels(), left.getY() - board.getY(),
0.01d);
}
/** The holes and the packages cannot both have the middle, so the packages were moved down. */
@Test
public void packagesClearTheMountingHoles() {
WS2812BStick stick = new WS2812BStick();
for (int i = 0; i < 8; i++) {
Rectangle2D pack = ledPackage(stick, i);
Assert.assertFalse("package " + i + " sits over the left hole",
pack.intersects(holeRect(stick, false)));
Assert.assertFalse("package " + i + " sits over the right hole",
pack.intersects(holeRect(stick, true)));
}
}
@Test
public void packagesAndHolesStayOnTheBoard() {
WS2812BStick stick = new WS2812BStick();
Rectangle2D board = stick.getBodyShape().getBounds2D();
for (boolean right : new boolean[] {false, true}) {
Rectangle2D hole = holeRect(stick, right);
Assert.assertTrue("a mounting hole breaches the board edge",
board.contains(hole.getX(), hole.getY(), hole.getWidth(), hole.getHeight()));
}
for (int i = 0; i < 8; i++) {
Rectangle2D pack = ledPackage(stick, i);
Assert.assertTrue("package " + i + " hangs off the board",
board.contains(pack.getX(), pack.getY(), pack.getWidth(), pack.getHeight()));
}
}
/** The pads are drawn after the packages, so an overlap would be hidden under copper. */
@Test
public void packagesClearThePads() {
WS2812BStick stick = new WS2812BStick();
for (int i = 0; i < 8; i++) {
Rectangle2D pack = ledPackage(stick, i);
for (int pad = 0; pad < stick.getControlPointCount(); pad++) {
Assert.assertFalse("package " + i + " runs under pad " + pad,
pack.intersects(padRect(stick, pad)));
}
}
}
@Test
public void drawsCleanlyInEveryState() {
MakerBoardTestSupport.assertDrawsCleanly(new WS2812BStick());
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BStick::new);
}
}
@@ -0,0 +1,286 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.HashSet;
import java.util.Set;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.displays.WS2812BStrip.Density;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* Geometry tests for the strip. The assertions that matter are the ones the numeric checks miss:
* that no pad hangs off the tape, and that no pad sits under an LED package.
*/
public class WS2812BStripTest {
private static final double PX_PER_MM = 200.0d / 25.4d;
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
private static double mm(double px) {
return px / PX_PER_MM;
}
private static double padHalfWidth() {
return WS2812BStrip.PAD_WIDTH.convertToPixels() / 2.0d;
}
/**
* The copper a cut leaves: half the pill, reaching inboard from the cut the control point sits
* on. This is what has to stay on the tape and off the packages.
*/
private static Rectangle2D padRect(WS2812BStrip strip, int index) {
Point2D pad = strip.getControlPoint(index);
double w = WS2812BStrip.PAD_WIDTH.convertToPixels();
double depth = strip.getPadDepth();
boolean input = index < 3;
return new Rectangle2D.Double(input ? pad.getX() : pad.getX() - depth, pad.getY() - w / 2.0,
depth, w);
}
private static double ledHalfWidth() {
return new Size(5.0d, SizeUnit.mm).convertToPixels() / 2.0d;
}
@Test
public void pitchFollowsFromDensity() {
Assert.assertEquals(1000.0d / 30.0d, Density._30.getPitchMm(), 0.01d);
Assert.assertEquals(1000.0d / 60.0d, Density._60.getPitchMm(), 0.01d);
Assert.assertEquals(1000.0d / 144.0d, Density._144.getPitchMm(), 0.01d);
}
@Test
public void tapeWidthFollowsFromDensity() {
for (Density density : Density.values()) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
Assert.assertEquals("tape width for " + density, density.getWidthMm(),
mm(strip.getBodyShape().getBounds2D().getHeight()), 0.01d);
}
}
/** The count drives the length, and the two lead-ins do not scale, so it is linear not doubled. */
@Test
public void lengthIsLinearInLedCount() {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(Density._60);
strip.setLedCount(8);
double eight = strip.getBodyShape().getBounds2D().getWidth();
strip.setLedCount(16);
double sixteen = strip.getBodyShape().getBounds2D().getWidth();
double pitch = new Size(Density._60.getPitchMm(), SizeUnit.mm).convertToPixels();
Assert.assertEquals("eight more LEDs adds eight pitches", 8 * pitch, sixteen - eight, 0.5d);
}
/**
* Tape is a repeating cell one pitch long, so a length of it is exactly as many pitches as it has
* LEDs, the two half-cells a pair of cuts leave making one whole one between them.
*/
@Test
public void aLengthOfTapeIsOnePitchPerLed() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 40}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
double pitch = new Size(density.getPitchMm(), SizeUnit.mm).convertToPixels();
Assert.assertEquals(density + " x " + count, count * pitch,
strip.getBodyShape().getBounds2D().getWidth(), 0.01d);
Assert.assertEquals(density + " lead-in", pitch / 2.0, strip.getLeadIn(), 0.01d);
}
}
}
/**
* The pill is the nominal length where the density leaves room and otherwise as much as fits,
* never shorter than its own width. At 144/m it bottoms out at a circle.
*/
@Test
public void theDensestTapeCarriesCircularPads() {
WS2812BStrip strip = new WS2812BStrip();
double width = WS2812BStrip.PAD_WIDTH.convertToPixels();
strip.setDensity(Density._30);
Assert.assertEquals("30/m should get the nominal pill",
WS2812BStrip.PAD_LENGTH.convertToPixels(), strip.getPadLength(), 0.01d);
strip.setDensity(Density._144);
Assert.assertTrue("144/m pad should be about as long as it is wide",
Math.abs(strip.getPadLength() - width) < width * 0.1d);
Assert.assertTrue("a pad may never be shorter than it is wide",
strip.getPadLength() >= width - 0.01d);
}
/** Six pads whatever the count: the LEDs are drawn, not wired. */
@Test
public void controlPointCountIsFixedAtSix() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 60, 144}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
Assert.assertEquals("control points at " + density + " x " + count, 6,
strip.getControlPointCount());
}
}
}
/**
* Both cuts carry all three lines, which is what lets a length be chained to the next. An end
* short of its supply pads cannot be chained, and nothing in the drawing says so.
*/
@Test
public void bothCutsCarryAllThreeLines() {
WS2812BStrip strip = new WS2812BStrip();
Set<String> names = new HashSet<String>();
for (int i = 0; i < strip.getControlPointCount(); i++) {
Assert.assertTrue("duplicate node name " + strip.getControlPointNodeName(i),
names.add(strip.getControlPointNodeName(i)));
}
Assert.assertEquals("DIN", strip.getControlPointNodeName(1));
Assert.assertEquals("DOUT", strip.getControlPointNodeName(4));
// the rails are continuous copper down the tape, so each sits in the same row at both ends
for (int i = 0; i < 3; i++) {
Assert.assertEquals("row " + i + " is not level across the tape",
strip.getControlPoint(i).getY(), strip.getControlPoint(3 + i).getY(), 0.01d);
}
}
/** A cut leaves half a pad on the very edge rather than a pad set in from it. */
@Test
public void padsAreFlushWithTheCuts() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 40}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
for (int i = 0; i < 3; i++) {
Assert.assertEquals(density + " x " + count + " input pad " + i, tape.getX(),
strip.getControlPoint(i).getX(), 0.01d);
Assert.assertEquals(density + " x " + count + " output pad " + i, tape.getMaxX(),
strip.getControlPoint(3 + i).getX(), 0.01d);
}
}
}
}
@Test
public void ledCountIsClamped() {
WS2812BStrip strip = new WS2812BStrip();
strip.setLedCount(100000);
Assert.assertEquals(WS2812BStrip.MAX_LED_COUNT, strip.getLedCount());
strip.setLedCount(0);
Assert.assertEquals(WS2812BStrip.MIN_LED_COUNT, strip.getLedCount());
strip.setLedCount(-5);
Assert.assertEquals(WS2812BStrip.MIN_LED_COUNT, strip.getLedCount());
}
@Test
public void everyPadSitsOnTheTape() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 40}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
for (int i = 0; i < strip.getControlPointCount(); i++) {
Rectangle2D pad = padRect(strip, i);
String where = density + " x " + count + " pad " + i;
Assert.assertTrue(where + " runs off the left cut", pad.getX() >= tape.getX() - 0.5d);
Assert.assertTrue(where + " runs off the right cut",
pad.getMaxX() <= tape.getMaxX() + 0.5d);
Assert.assertTrue(where + " runs off the top edge", pad.getY() >= tape.getY() - 0.5d);
Assert.assertTrue(where + " runs off the bottom edge",
pad.getMaxY() <= tape.getMaxY() + 0.5d);
}
}
}
}
/**
* The output pad must clear the last LED package. At 144/m the pitch is 6.94 mm against a 5 mm
* package, so there is very little room to be wrong in.
*/
@Test
public void outputPadClearsTheLastLed() {
for (Density density : Density.values()) {
for (int count : new int[] {1, 8, 40}) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(count);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
double pitch = new Size(density.getPitchMm(), SizeUnit.mm).convertToPixels();
double lastLed = tape.getX() + strip.getLeadIn() + (count - 1) * pitch;
for (int i = 0; i < 3; i++) {
Assert.assertTrue(density + " x " + count + ": output pad " + i + " overlaps the last LED",
padRect(strip, 3 + i).getX() >= lastLed + ledHalfWidth() - 0.5d);
}
}
}
}
/** The same, mirrored, at the input end. */
@Test
public void inputPadsClearTheFirstLed() {
for (Density density : Density.values()) {
WS2812BStrip strip = new WS2812BStrip();
strip.setDensity(density);
strip.setLedCount(8);
Rectangle2D tape = strip.getBodyShape().getBounds2D();
double firstLed = tape.getX() + strip.getLeadIn();
for (int i = 0; i < 3; i++) {
Assert.assertTrue(density + ": input pad " + i + " overlaps the first LED",
padRect(strip, i).getMaxX() <= firstLed - ledHalfWidth() + 0.5d);
}
}
}
private static double mmToPx(double value) {
return new Size(value, SizeUnit.mm).convertToPixels();
}
@Test
public void honoursTheLedPackage() {
MakerBoardTestSupport.assertAddressableLedTypes(WS2812BStrip::new);
}
}
@@ -0,0 +1,152 @@
/*
*
* 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.maker;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.core.IDIYComponent;
import org.diylc.netlist.Node;
import org.diylc.components.displays.WS2812BStrip;
import org.diylc.components.micro.ArduinoUno;
import org.diylc.components.micro.ESP32DevKit;
import org.diylc.components.micro.ESP8266NodeMCU;
import org.diylc.components.micro.RaspberryPi;
import org.diylc.components.micro.RaspberryPiPico;
import org.diylc.components.micro.Teensy;
/**
* Pins a board joins inside itself, as the netlist decides them through
* {@link Node#railName(String)}.
* <p>
* The negative cases carry the weight. An underscore does three unrelated jobs across these
* pinouts - it disambiguates a repeated rail, it namespaces a connector's signals, and it
* qualifies a separate net - so a rule keyed on the text before the underscore would short an
* Ethernet or USB header together.
*
* @author Branislav Stojkovic
*/
public class BoardRailTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
@Test
public void repeatedRailsAreOneNode() throws Exception {
ESP32DevKit esp = new ESP32DevKit();
assertJoined(esp, "GND_1", "GND_2");
RaspberryPi pi = new RaspberryPi();
assertJoined(pi, "GND (Pin 6)", "GND (Pin 9)");
assertJoined(pi, "5V (Pin 2)", "5V (Pin 4)");
assertJoined(pi, "3.3V (Pin 1)", "3.3V (Pin 17)");
RaspberryPiPico pico = new RaspberryPiPico();
assertJoined(pico, "GND_1", "GND_8");
WS2812BStrip strip = new WS2812BStrip();
assertJoined(strip, "GND_1", "GND_2");
assertJoined(strip, "+5V_1", "+5V_2");
}
@Test
public void annotatedRailsAreOneNode() throws Exception {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
assertJoined(teensy, "GND_1", "GND_4 (Mid)");
assertJoined(teensy, "3.3V_1", "3.3V_2 (250mA)");
}
@Test
public void connectorSignalsSharingAPrefixAreNotJoined() throws Exception {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
assertSeparate(teensy, "ETH_TX-", "ETH_RX+");
assertSeparate(teensy, "ETH_TX-", "ETH_GND");
assertSeparate(teensy, "USB_D-", "USB_D+");
assertSeparate(teensy, "USB_5V", "USB_GND1");
}
@Test
public void qualifiedNamesAreSeparateNets() throws Exception {
ArduinoUno uno = new ArduinoUno();
uno.setVersion(ArduinoUno.ArduinoUnoVersion.REV3);
// the USB bridge has its own ICSP header; it is not the main MCU's SPI
assertSeparate(uno, "MISO", "MISO_16U2");
assertSeparate(uno, "SCK", "SCK_16U2");
RaspberryPiPico pico = new RaspberryPiPico();
// the regulator's enable input is not its output
assertSeparate(pico, "3V3_EN", "3V3 (OUT)");
}
@Test
public void absentConnectionsAreNotANet() throws Exception {
ESP32DevKit esp = new ESP32DevKit();
esp.setVersion(ESP32DevKit.DevKitVersion.ESP32_C6_DevKitC_1);
assertSeparate(esp, "NC_1", "NC_2");
ESP8266NodeMCU nodeMcu = new ESP8266NodeMCU();
assertSeparate(nodeMcu, "RSV_1", "RSV_2");
}
@Test
public void signalPinsAreNeverJoined() throws Exception {
RaspberryPiPico pico = new RaspberryPiPico();
assertSeparate(pico, "GP0", "GP1");
assertSeparate(pico, "GP0", "GP28");
}
private static void assertJoined(IDIYComponent<?> board, String first, String second) {
Assert.assertTrue(board.getClass().getSimpleName() + ": \"" + first + "\" and \"" + second
+ "\" should be one node",
sameRail(board, indexOf(board, first), indexOf(board, second)));
}
private static void assertSeparate(IDIYComponent<?> board, String first, String second) {
Assert.assertFalse(board.getClass().getSimpleName() + ": \"" + first + "\" and \"" + second
+ "\" should stay separate nodes",
sameRail(board, indexOf(board, first), indexOf(board, second)));
}
private static boolean sameRail(IDIYComponent<?> board, int index1, int index2) {
String rail = Node.railName(board.getControlPointNodeName(index1));
return rail != null && rail.equals(Node.railName(board.getControlPointNodeName(index2)));
}
private static int indexOf(IDIYComponent<?> board, String nodeName) {
for (int i = 0; i < board.getControlPointCount(); i++) {
if (nodeName.equals(board.getControlPointNodeName(i))) {
return i;
}
}
throw new IllegalArgumentException(
board.getClass().getSimpleName() + " has no pin named \"" + nodeName + "\"");
}
}
@@ -19,15 +19,19 @@
* 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;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.function.Supplier;
import org.diylc.common.Orientation;
import org.diylc.components.AbstractAddressableLedBoard;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.AbstractMakerBoard.LedType;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
@@ -38,14 +42,16 @@ import org.junit.Assert;
/**
* Shared checks for the maker board tests.
*
* <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:
* its pinout, its geometry and the way those change from one version to the next.
* <p>What every board has in common -- naming its pins, keeping them apart, drawing, rotating --
* is asserted for all of them in {@code MakerComponentsTest}, but only for the version each board
* starts with. The per-board tests cover a board's own pinout and geometry.
*/
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();
// large enough for the longest board in the package, with its origin in the middle
private static final int RENDER_SIZE = 1400;
private static final Project PROJECT = new Project();
@@ -73,7 +79,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 +87,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 +101,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 +109,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 +120,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());
@@ -123,8 +129,79 @@ class MakerBoardTestSupport {
}
}
/**
* Renders the board alone at a fixed position. Two renderings differing in one property can then
* be compared, which is the only way to catch a drawing change that moves no geometry.
*/
public static int[] renderPixels(AbstractMakerBoard board) {
BufferedImage image = new BufferedImage(RENDER_SIZE, RENDER_SIZE, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
board.setControlPoint(new Point2D.Double(RENDER_SIZE / 2.0, RENDER_SIZE / 2.0), 0);
try {
board.draw(g2d, ComponentState.NORMAL, false, PROJECT, OBSERVER);
} finally {
g2d.dispose();
}
return image.getRGB(0, 0, RENDER_SIZE, RENDER_SIZE, null, 0, RENDER_SIZE);
}
/**
* Asserts that a board prints its description on its lit area and that turning the property off
* takes it away again. A screen that silently refused to print would pass the getter.
*/
public static void assertScreenTextIsDrawn(AbstractMakerBoard withText,
AbstractMakerBoard withoutText) {
Assert.assertFalse("The lit area should carry the description, and does not",
Arrays.equals(renderPixels(withText), renderPixels(withoutText)));
}
/**
* Asserts that a board built out of addressable LEDs honours the package it is fitted with: a
* board saved before the property existed reads as RGB, the packages are pin-compatible so
* fitting one in place of another moves no pad, RGBW is drawn with the white die that is the
* only thing telling it from RGB on sight, and WWA is drawn off the warm white palette rather
* than the colour wheel it cannot emit.
*
* @param factory Builds a fresh board, since each assertion needs one of its own
*/
public static void assertAddressableLedTypes(Supplier<AbstractAddressableLedBoard> factory) {
Assert.assertEquals("A board with no stored package should read as RGB", LedType.RGB,
factory.get().getLedType());
AbstractAddressableLedBoard stale = factory.get();
stale.setLedType(null);
Assert.assertEquals("A board saved before the property existed should read as RGB",
LedType.RGB, stale.getLedType());
AbstractAddressableLedBoard reference = factory.get();
for (LedType ledType : LedType.values()) {
AbstractAddressableLedBoard board = factory.get();
board.setLedType(ledType);
assertSharedFootprint(reference, board, reference.getControlPointCount());
Assert.assertTrue(ledType + " should be named in the BOM's value column",
board.getValueForDisplay().contains(ledType.toString()));
}
int[] rgb = renderWithLedType(factory, LedType.RGB);
Assert.assertFalse("RGBW should show the white die RGB does not carry",
Arrays.equals(rgb, renderWithLedType(factory, LedType.RGBW_WARM)));
Assert.assertFalse("WWA should not be drawn lit in colour",
Arrays.equals(rgb, renderWithLedType(factory, LedType.WWA)));
// the three RGBW tints name different parts to buy but the same thing to draw
int[] warm = renderWithLedType(factory, LedType.RGBW_WARM);
Assert.assertArrayEquals(warm, renderWithLedType(factory, LedType.RGBW_NATURAL));
Assert.assertArrayEquals(warm, renderWithLedType(factory, LedType.RGBW_COOL));
}
private static int[] renderWithLedType(Supplier<AbstractAddressableLedBoard> factory,
LedType ledType) {
AbstractAddressableLedBoard board = factory.get();
board.setLedType(ledType);
return renderPixels(board);
}
/** 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,21 @@ public class MakerComponentsTest {
ESP8266NodeMCU.class,
WemosD1Mini.class,
// Displays & Outputs
CharacterLCD.class,
OLEDDisplay.class,
SevenSegmentDisplay.class,
LEDMatrix.class,
LEDBarGraph.class,
TFTDisplay.class,
WS2812BStick.class,
WS2812BRing.class,
WS2812BStrip.class,
WS2812BJewel.class,
WS2812BPanel.class,
WS2812BBreakout.class,
Nokia5110LCD.class,
// Electro-Mechanical
BatteryHolder18650.class
);
@@ -90,15 +105,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,
@@ -186,13 +192,9 @@ public class MakerComponentsTest {
IDIYComponent<?> component = clazz.getDeclaredConstructor().newInstance();
component.setControlPoint(new Point2D.Double(200, 200), 0);
// Normal mode
component.draw(g2d, ComponentState.NORMAL, false, project, observer);
// Selected mode
component.draw(g2d, ComponentState.SELECTED, false, project, observer);
// Outline mode
component.draw(g2d, ComponentState.NORMAL, true, project, observer);
// Icon drawing
component.drawIcon(g2d, 32, 32);
}
g2d.dispose();
@@ -222,7 +224,6 @@ public class MakerComponentsTest {
Assert.assertTrue("Component " + clazz.getSimpleName() + " should be rotatable", transformer.canRotate(component));
// Rotate 4 times clockwise (360 degrees back to original)
for (int r = 0; r < 4; r++) {
transformer.rotate(component, center, 1);
}
@@ -0,0 +1,252 @@
/*
*
* 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.maker;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.ComponentType;
import org.diylc.common.PropertyWrapper;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.presenter.ComponentProcessor;
/**
* Sweeps every registered maker component across the cross product of its enum properties.
* <p>
* {@link MakerComponentsTest} and the per-class tests exercise the default variant only. The
* variants are where the geometry differs, and a release makes control-point counts and positions
* permanent, so this walks all of them. The cross product rather than one property at a time,
* because a variant is usually a pair and only the combination decides the pin count.
*
* @author Branislav Stojkovic
*/
public class MakerVariantSweepTest {
private static final String[] PACKAGES =
{"org.diylc.components.displays", "org.diylc.components.micro"};
/**
* Orientation is swept inside the drawing check rather than joining the cross product: it moves
* the control points but cannot change how many there are or what they are called.
*/
private static final String ORIENTATION = "Orientation";
private static final Project PROJECT = new Project();
private static final IDrawingObserver OBSERVER = new IDrawingObserver() {
@Override
public void stopTracking() {}
@Override
public void startTracking() {}
@Override
public void startTrackingContinuityArea(boolean positive) {}
@Override
public void stopTrackingContinuityArea() {}
@Override
public boolean isTrackingContinuityArea() {
return false;
}
@Override
public void setContinuityMarker(String marker) {}
};
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
/** Problems are collected so that a run reports every bad variant rather than just the first. */
@Test
public void everyVariantHasSoundControlPointsAndDraws() throws Exception {
List<String> problems = new ArrayList<String>();
int swept = 0;
for (Class<? extends IDIYComponent<?>> clazz : makerComponentClasses()) {
List<PropertyWrapper> enums = new ArrayList<PropertyWrapper>();
PropertyWrapper orientation = null;
for (PropertyWrapper property : ComponentProcessor.getInstance().extractProperties(clazz)) {
if (!property.getType().isEnum()) {
continue;
}
if (ORIENTATION.equals(property.getName())) {
orientation = property;
} else {
enums.add(property);
}
}
int combinations = 1;
for (PropertyWrapper property : enums) {
combinations *= property.getType().getEnumConstants().length;
}
for (int index = 0; index < combinations; index++) {
IDIYComponent<?> component = clazz.getDeclaredConstructor().newInstance();
StringBuilder where = new StringBuilder(clazz.getSimpleName());
int remaining = index;
boolean complete = true;
for (PropertyWrapper property : enums) {
Object[] values = property.getType().getEnumConstants();
Object value = values[remaining % values.length];
remaining /= values.length;
where.append(" / ").append(property.getName()).append("=").append(value);
PropertyWrapper applied = (PropertyWrapper) property.clone();
applied.setValue(value);
try {
applied.writeTo(component);
} catch (Exception e) {
problems.add(where + ": will not apply -- " + e.getClass().getSimpleName());
complete = false;
break;
}
}
if (!complete) {
continue;
}
swept++;
checkNodeNamesAreUnique(component, where.toString(), problems);
checkControlPointsAreDistinct(component, where.toString(), problems);
checkDrawsInEveryOrientation(component, orientation, where.toString(), problems);
}
}
Assert.assertTrue("No maker components were discovered", swept > 0);
Assert.assertTrue(swept + " variants swept, " + problems.size() + " problem(s):\n"
+ String.join("\n", problems), problems.isEmpty());
}
private static void checkNodeNamesAreUnique(IDIYComponent<?> component, String where,
List<String> problems) {
Set<String> seen = new HashSet<String>();
for (int i = 0; i < component.getControlPointCount(); i++) {
String name = component.getControlPointNodeName(i);
// A null name is a point that carries no node, which is not a collision.
if (name != null && !seen.add(name)) {
problems.add(where + ": duplicate node name \"" + name + "\" at point " + i);
}
}
}
private static void checkControlPointsAreDistinct(IDIYComponent<?> component, String where,
List<String> problems) {
int count = component.getControlPointCount();
for (int i = 0; i < count; i++) {
Point2D first = component.getControlPoint(i);
for (int j = i + 1; j < count; j++) {
if (first.distance(component.getControlPoint(j)) <= 0.001) {
problems.add(where + ": control points " + i + " and " + j + " coincide");
}
}
}
}
private static void checkDrawsInEveryOrientation(IDIYComponent<?> component,
PropertyWrapper orientation, String where, List<String> problems)
throws CloneNotSupportedException {
Object[] orientations =
orientation == null ? new Object[] {null} : orientation.getType().getEnumConstants();
for (Object value : orientations) {
String at = where;
if (value != null) {
at = where + " / " + orientation.getName() + "=" + value;
PropertyWrapper applied = (PropertyWrapper) orientation.clone();
applied.setValue(value);
try {
applied.writeTo(component);
} catch (Exception e) {
problems.add(at + ": will not apply -- " + e.getClass().getSimpleName());
continue;
}
}
BufferedImage image = new BufferedImage(600, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// Translating the canvas rather than moving control point 0 keeps the component's geometry
// intact: a drag sets every control point, so moving only the first leaves the rest stale.
g2d.translate(300, 300);
try {
component.draw(g2d, ComponentState.NORMAL, false, PROJECT, OBSERVER);
component.draw(g2d, ComponentState.SELECTED, false, PROJECT, OBSERVER);
component.draw(g2d, ComponentState.NORMAL, true, PROJECT, OBSERVER);
component.drawIcon(g2d, 32, 32);
} catch (Exception e) {
problems.add(at + ": draw threw " + e.getClass().getSimpleName() + " -- " + e.getMessage());
} finally {
g2d.dispose();
}
}
}
private static List<Class<? extends IDIYComponent<?>>> makerComponentClasses() {
List<Class<? extends IDIYComponent<?>>> classes =
new ArrayList<Class<? extends IDIYComponent<?>>>();
for (List<ComponentType> category : ComponentProcessor.getInstance().getComponentTypes()
.values()) {
for (ComponentType type : category) {
String name = type.getInstanceClass().getName();
for (String prefix : PACKAGES) {
if (name.startsWith(prefix + ".")) {
classes.add(type.getInstanceClass());
}
}
}
}
// discovery order is not stable, and a failure message naming variants reads better sorted
Collections.sort(classes, new Comparator<Class<? extends IDIYComponent<?>>>() {
@Override
public int compare(Class<? extends IDIYComponent<?>> a, Class<? extends IDIYComponent<?>> b) {
return a.getName().compareTo(b.getName());
}
});
return classes;
}
}
@@ -0,0 +1,101 @@
/*
*
* 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.maker;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import org.diylc.common.Display;
import org.diylc.components.MakerBoardPainter;
import org.junit.Assert;
import org.junit.Test;
/**
* What {@code MakerBoardPainter.drawScreenText} promises, independently of its callers: it prints
* nothing at all rather than a crop, so a panel too small for its description is left dark.
*/
public class ScreenTextTest {
private static final String VALUE = "0.96\" SSD1306 128x64, I2C";
private static int inkedPixels(Rectangle2D area, Display display, String name, String value) {
BufferedImage image = new BufferedImage(600, 400, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
try {
MakerBoardPainter.drawScreenText(g2d, area, Color.WHITE, display, name, value);
} finally {
g2d.dispose();
}
int count = 0;
for (int rgb : image.getRGB(0, 0, 600, 400, null, 0, 600)) {
if ((rgb >>> 24) != 0) {
count++;
}
}
return count;
}
@Test
public void aPanelWithRoomPrintsItsDescription() {
Assert.assertTrue(
inkedPixels(new Rectangle2D.Double(10, 10, 400, 200), Display.VALUE, "DISP1", VALUE) > 0);
}
@Test
public void aPanelWithoutRoomPrintsNothing() {
Assert.assertEquals(0,
inkedPixels(new Rectangle2D.Double(10, 10, 24, 10), Display.VALUE, "DISP1", VALUE));
}
@Test
public void aPanelTooShallowForTheBlockPrintsNothing() {
// wide enough for every line, but with room for one of the two
Assert.assertEquals(0,
inkedPixels(new Rectangle2D.Double(10, 10, 400, 16), Display.VALUE, "DISP1", VALUE));
}
@Test
public void noneLeavesTheScreenDark() {
Assert.assertEquals(0,
inkedPixels(new Rectangle2D.Double(10, 10, 400, 200), Display.NONE, "DISP1", VALUE));
}
@Test
public void aPartWithNoVariantPrintsNothingForItsValue() {
Assert.assertEquals(0,
inkedPixels(new Rectangle2D.Double(10, 10, 400, 200), Display.VALUE, "DISP1", ""));
}
@Test
public void eachSettingPrintsSomethingDifferent() {
Rectangle2D area = new Rectangle2D.Double(10, 10, 400, 200);
int name = inkedPixels(area, Display.NAME, "DISP1", VALUE);
int value = inkedPixels(area, Display.VALUE, "DISP1", VALUE);
int both = inkedPixels(area, Display.BOTH, "DISP1", VALUE);
Assert.assertTrue(name > 0);
Assert.assertTrue(value > name);
Assert.assertTrue(both > value);
}
}
@@ -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;
@@ -46,8 +47,8 @@ public class ArduinoNanoTest {
Assert.assertEquals("MISO", classic.getControlPointNodeName(30));
// the silkscreen prints what is on the board: both grounds and both resets are printed alike
Assert.assertEquals("GND1", classic.getControlPointNodeName(3));
Assert.assertEquals("GND2", classic.getControlPointNodeName(16));
Assert.assertEquals("GND_1", classic.getControlPointNodeName(3));
Assert.assertEquals("GND_2", classic.getControlPointNodeName(16));
Assert.assertEquals("GND", classic.getSilkPinLabel(3));
Assert.assertEquals("GND", classic.getSilkPinLabel(16));
Assert.assertEquals("RST", classic.getSilkPinLabel(2));
@@ -21,9 +21,12 @@
*/
package org.diylc.components.micro;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.components.micro.ArduinoUno.ArduinoUnoVersion;
import org.junit.Assert;
import org.junit.Test;
@@ -52,8 +55,8 @@ public class ArduinoUnoTest {
// the silkscreen prints what is on the board rather than the node name: both grounds of the
// power header say "GND" and the reserved first pin says nothing at all
Assert.assertEquals("GND1", r3.getControlPointNodeName(5));
Assert.assertEquals("GND2", r3.getControlPointNodeName(6));
Assert.assertEquals("GND_1", r3.getControlPointNodeName(5));
Assert.assertEquals("GND_2", r3.getControlPointNodeName(6));
Assert.assertEquals("GND", r3.getSilkPinLabel(5));
Assert.assertEquals("GND", r3.getSilkPinLabel(6));
Assert.assertEquals("", r3.getSilkPinLabel(0));
@@ -119,6 +122,25 @@ public class ArduinoUnoTest {
}
}
/**
* The board colour follows the version only while it is still a default: the R3 and the R4 are
* genuinely different colours, so switching repaints a board the user has not touched, but a
* colour they chose survives.
*/
@Test
public void aChosenColourSurvivesAVersionSwitch() {
ArduinoUno uno = version(ArduinoUnoVersion.REV3);
Assert.assertEquals(AbstractMakerBoard.ARDUINO_TEAL, uno.getBodyColor());
uno.setVersion(ArduinoUnoVersion.R4_WIFI);
Assert.assertEquals("an untouched board should follow its version",
AbstractMakerBoard.ARDUINO_BLUE, uno.getBodyColor());
Color chosen = Color.decode("#8A2BE2");
uno.setBodyColor(chosen);
uno.setVersion(ArduinoUnoVersion.REV3);
Assert.assertEquals("a chosen colour should survive the switch", chosen, uno.getBodyColor());
}
private static ArduinoUno version(ArduinoUnoVersion version) {
ArduinoUno uno = new ArduinoUno();
uno.setVersion(version);
@@ -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;
@@ -35,7 +36,7 @@ public class TeensyTest {
Teensy teensy40 = new Teensy();
Assert.assertEquals(TeensyVersion.Teensy_4_0, teensy40.getVersion());
Assert.assertEquals(34, teensy40.getControlPointCount());
Assert.assertEquals("GND", teensy40.getControlPointNodeName(0));
Assert.assertEquals("GND_1", teensy40.getControlPointNodeName(0));
Assert.assertEquals("0 (RX1/CS1/CRX2)", teensy40.getControlPointNodeName(1));
Assert.assertEquals("12 (MISO/MQSL)", teensy40.getControlPointNodeName(13));
Assert.assertEquals("VIN (3.6-5.5V)", teensy40.getControlPointNodeName(14));
@@ -23,6 +23,8 @@ package org.diylc.components.micro;
import java.awt.geom.Rectangle2D;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@@ -42,7 +44,7 @@ public class WemosD1MiniTest {
Assert.assertEquals("D0 (GPIO16)", mcu.getControlPointNodeName(2));
Assert.assertEquals("3V3", mcu.getControlPointNodeName(7));
// right row, top to bottom; the board prints the ground pad as "G"
// right row in array order, which runs bottom to top on the board; the ground pad prints "G"
Assert.assertEquals("5V", mcu.getControlPointNodeName(8));
Assert.assertEquals("G (GND)", mcu.getControlPointNodeName(9));
Assert.assertEquals("D1 (GPIO5)", mcu.getControlPointNodeName(13));
@@ -67,4 +69,31 @@ public class WemosD1MiniTest {
new Size(0.275d, SizeUnit.in).convertToPixels(),
mcu.getControlPoint(0).getY() - bounds.getY(), 0.1);
}
/**
* The right row runs bottom to top, so its first pin is level with the left row's last. The
* silkscreen has to follow that, which is why the labels are taken from each control point
* rather than from a parallel array.
*/
@Test
public void theRightRowRunsBottomToTop() {
WemosD1Mini mcu = new WemosD1Mini();
Assert.assertEquals("the rows should start and end level", mcu.getControlPoint(7).getY(),
mcu.getControlPoint(8).getY(), 0.01);
Assert.assertTrue("pin 8 should sit below pin 15",
mcu.getControlPoint(8).getY() > mcu.getControlPoint(15).getY());
}
/** The silkscreen is the node names with their annotations stripped. */
@Test
public void silkLabelsComeFromTheNodeNames() {
WemosD1Mini mcu = new WemosD1Mini();
String[] expected = new String[] {"RST", "A0", "D0", "D5", "D6", "D7", "D8", "3V3", "5V", "G",
"D4", "D3", "D2", "D1", "RX", "TX"};
Assert.assertEquals(expected.length, mcu.getControlPointCount());
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i],
AbstractMakerBoard.getDisplayPinLabel(mcu.getControlPointNodeName(i)));
}
}
}