mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-15 19:52:08 +10:00
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:
+74
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+190
-88
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
+426
-237
@@ -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; }
|
||||
}
|
||||
}
|
||||
+138
-100
@@ -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]; }
|
||||
}
|
||||
}
|
||||
|
||||
+102
-90
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+129
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
+106
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+195
@@ -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());
|
||||
}
|
||||
}
|
||||
+240
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+215
@@ -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);
|
||||
}
|
||||
}
|
||||
+257
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+195
@@ -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);
|
||||
}
|
||||
}
|
||||
+286
@@ -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 + "\"");
|
||||
}
|
||||
}
|
||||
+90
-13
@@ -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();
|
||||
+15
-14
@@ -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);
|
||||
}
|
||||
|
||||
+252
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user