diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java new file mode 100644 index 00000000..484c4fb3 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java @@ -0,0 +1,304 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +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.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 = "NeoPixel Ring (WS2812B)", category = "Displays & Outputs", + author = "Branislav Stojkovic", description = "Addressable RGB WS2812B NeoPixel Ring (12/16/24 LEDs)", + instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class WS2812BRing extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum RingSize { + _12_LED("12 LEDs (37mm OD)", 12, 37.0, 27.0), + _16_LED("16 LEDs (44.5mm OD)", 16, 44.5, 32.0), + _24_LED("24 LEDs (66mm OD)", 24, 66.0, 52.0); + + 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"}; + + private RingSize ringSize = RingSize._16_LED; + + public WS2812BRing() { + super(); + this.bodyColor = NEO_BLACK; + updateControlPoints(); + } + + @EditableProperty(name = "Ring Size") + public RingSize getRingSize() { + return ringSize; + } + + public void setRingSize(RingSize ringSize) { + this.ringSize = ringSize; + updateControlPoints(); + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + 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; + } + + 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; + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double midR = getMidRadius(); + double theta0 = getPadAngle(0); + + double[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; 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)); + } + + rotatePoints(firstPoint, relativeOffsets); + } + + private Point2D getCenter() { + Point2D p0 = controlPoints[0]; + double midR = getMidRadius(); + 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)); + } + + @Override + public Shape getBodyShape() { + Point2D center = getCenter(); + double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels(); + 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 = 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); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(outerArea); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(outerArea); + + if (!outlineMode) { + int ledCount = ringSize.getLedCount(); + double ledSize = Math.max(8.0, Math.min(15.0, (2 * Math.PI * midR / ledCount) * 0.60)); + double halfLed = ledSize / 2.0; + + // Draw all LEDs around the circular ring + for (int i = 0; i < ledCount; i++) { + double angle = 2 * Math.PI * i / ledCount - Math.PI / 2.0; + double lx = cx + midR * Math.cos(angle); + double ly = cy + midR * Math.sin(angle); + + // 5050 White Package + g2d.setColor(LED_PACKAGE); + g2d.fill(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2)); + g2d.setColor(LED_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f)); + g2d.draw(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2)); + + // Milky phosphor lens + double diffR = halfLed * 0.7; + g2d.setColor(LED_DIFFUSER); + g2d.fill(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2)); + g2d.setColor(DIFFUSER_BORDER); + g2d.draw(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2)); + + // Tiny IC dot + g2d.setColor(CHIP_DOT_COLOR); + double dotS = Math.max(2.0, diffR * 0.4); + g2d.fill(new Rectangle2D.Double(lx - dotS / 2.0, ly - dotS / 2.0, dotS, dotS)); + } + + // 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); + + // 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); + } + } + + g2d.setTransform(oldTx); + + // Draw the 4 solder pads directly on the circular ring PCB + drawSolderPads(g2d, 0, 4, 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; + int innerR = outerR / 2; + + Area outer = new Area(new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2)); + Area inner = new Area(new Ellipse2D.Double(cx - innerR, cy - innerR, innerR * 2, innerR * 2)); + outer.subtract(inner); + g2d.setColor(NEO_BLACK); + g2d.fill(outer); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(outer); + + Color[] rainbow = new Color[] { + Color.decode("#FF3333"), + Color.decode("#FFD700"), + Color.decode("#00E676"), + Color.decode("#00E5FF"), + Color.decode("#E040FB"), + Color.decode("#FF6B35") + }; + + double midR = (outerR + innerR) / 2.0; + int dotR = Math.max(2, (int)(midR * 0.35)); + int dotCount = 6; + + for (int i = 0; i < dotCount; i++) { + double angle = 2 * Math.PI * i / dotCount - Math.PI / 2.0; + double lx = cx + midR * Math.cos(angle); + double ly = cy + midR * Math.sin(angle); + g2d.setColor(rainbow[i % rainbow.length]); + g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2)); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java new file mode 100644 index 00000000..b520b5ab --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java @@ -0,0 +1,337 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.electromechanical; + +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.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.appframework.miscutils.ConfigurationManager; +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.IPlugInPort; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractTransparentComponent; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.Theme; +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; + +@ComponentDescriptor(name = "18650 Battery Holder", category = "Electro-Mechanical", + author = "Branislav Stojkovic", description = "PCB-mount 18650 Li-ion Battery Holder (1S or 2S)", + instanceNamePrefix = "BTR", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class BatteryHolder18650 extends AbstractTransparentComponent { + + private static final long serialVersionUID = 1L; + + public enum CellCount { + _1S("1S (3.7V) — Single Cell", 1, 77.0, 21.0), + _2S("2S (7.4V) — Two Cells", 2, 77.0, 42.0); + + private final String label; + private final int cells; + private final double lengthMm; + private final double widthMm; + + CellCount(String label, int cells, double lengthMm, double widthMm) { + this.label = label; + this.cells = cells; + this.lengthMm = lengthMm; + this.widthMm = widthMm; + } + + @Override public String toString() { return label; } + public int getCells() { return cells; } + public double getLengthMm() { return lengthMm; } + public double getWidthMm() { return widthMm; } + } + + public static Color HOLDER_BODY = Color.decode("#2C2C2C"); + public static Color HOLDER_BODY_LIGHT = Color.decode("#3C3C3C"); + public static Color SPRING_COLOR = Color.decode("#C0C0C0"); + public static Color SPRING_BORDER = Color.decode("#909090"); + public static Color PLATE_COLOR = Color.decode("#D0D0D0"); + public static Color CELL_CAVITY = Color.decode("#1A1A1A"); + + private static final Font LABEL_FONT = new Font("SansSerif", Font.BOLD, 12); + + private String value = ""; + private CellCount cellCount = CellCount._1S; + private Orientation orientation = Orientation.DEFAULT; + + // Two control points: Point 0 = + (left), Point 1 = - (right) + private Point2D[] controlPoints = new Point2D[] { + new Point2D.Double(0, 0), + new Point2D.Double(0, 0) + }; + + public BatteryHolder18650() { + super(); + updateControlPoints(); + } + + @EditableProperty(name = "Cell Count") + public CellCount getCellCount() { + return cellCount; + } + + public void setCellCount(CellCount cellCount) { + this.cellCount = cellCount; + updateControlPoints(); + } + + @EditableProperty(name = "Orientation") + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + updateControlPoints(); + } + + private void updateControlPoints() { + double length = new Size(cellCount.getLengthMm(), SizeUnit.mm).convertToPixels(); + Point2D p0 = controlPoints[0]; + + double theta = orientation.toRadians(); + // + at p0 (left), - at p0 + (length, 0) rotated (right) + double dx = Math.cos(theta) * length; + double dy = Math.sin(theta) * length; + controlPoints[1] = new Point2D.Double(p0.getX() + dx, p0.getY() + dy); + } + + @Override + public int getControlPointCount() { return 2; } + + @Override + public Point2D getControlPoint(int index) { return controlPoints[index]; } + + @Override + public void setControlPoint(Point2D point, int index) { + controlPoints[index].setLocation(point); + if (index == 0) { + updateControlPoints(); + } + } + + @Override + public boolean isControlPointSticky(int index) { return true; } + + @Override + public VisibilityPolicy getControlPointVisibilityPolicy(int index) { + return VisibilityPolicy.WHEN_SELECTED; + } + + @Override + public boolean canPointMoveFreely(int pointIndex) { return false; } + + @Override + public boolean canControlPointOverlap(int index) { return false; } + + @Override + public String getControlPointNodeName(int index) { + return index == 0 ? "+ (VCC)" : "- (GND)"; + } + + @Override + public String getValue() { return value; } + + @Override + public void setValue(String value) { this.value = value; } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double length = new Size(cellCount.getLengthMm(), SizeUnit.mm).convertToPixels(); + double width = new Size(cellCount.getWidthMm(), SizeUnit.mm).convertToPixels(); + double cellD = new Size(18.5d, SizeUnit.mm).convertToPixels(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + Composite oldComposite = applyAlpha(g2d, componentState); + + // Border color + Color borderColor; + if (outlineMode) { + Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME); + borderColor = (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) + ? SELECTION_COLOR : (theme != null ? theme.getOutlineColor() : Color.BLACK); + } else { + borderColor = (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) + ? SELECTION_COLOR : HOLDER_BODY.darker(); + } + + double bodyY = y - width / 2.0; + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : HOLDER_BODY); + g2d.fill(new RoundRectangle2D.Double(x, bodyY, length, width, 10, 10)); + drawingObserver.stopTracking(); + + g2d.setColor(borderColor); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new RoundRectangle2D.Double(x, bodyY, length, width, 10, 10)); + + if (!outlineMode) { + int cells = cellCount.getCells(); + double cellWidth = (width - 8.0) / cells; + + for (int i = 0; i < cells; i++) { + double cavityX = x + 6; + double cavityY = bodyY + 4 + i * cellWidth; + double cavityW = length - 12; + double cavityH = cellWidth - 4; + + // Cell cavity (dark inset) + g2d.setColor(CELL_CAVITY); + g2d.fill(new RoundRectangle2D.Double(cavityX, cavityY, cavityW, cavityH, 6, 6)); + g2d.setColor(HOLDER_BODY_LIGHT); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(cavityX, cavityY, cavityW, cavityH, 6, 6)); + + // Positive flat plate button contact on left (x) + double plateX = x + 6; + double plateW = 6; + double plateH = Math.min(14, cellH(cellD)); + double contactY = cavityY + cavityH / 2.0; + g2d.setColor(PLATE_COLOR); + g2d.fill(new Rectangle2D.Double(plateX, contactY - plateH / 2.0, plateW, plateH)); + g2d.setColor(SPRING_BORDER); + g2d.draw(new Rectangle2D.Double(plateX, contactY - plateH / 2.0, plateW, plateH)); + + // Negative spring contact on right (x + length) + double springX = x + length - 16; + double springW = 10; + double springH = Math.min(14, cellH(cellD)); + g2d.setColor(SPRING_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Rectangle2D.Double(springX, contactY - springH / 2.0, springW, springH)); + g2d.draw(new Rectangle2D.Double(springX + 3, contactY - springH / 2.0 + 2, springW - 6, springH - 4)); + + // Polarity markings (+ on left, - on right) + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT); + StringUtils.drawCenteredText(g2d, "+", x + 20, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "-", x + length - 20, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // 18650 text in cavity + g2d.setColor(Color.decode("#555555")); + StringUtils.drawCenteredText(g2d, "18650", x + length / 2.0, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // Series connector bar between cells for 2S + if (cells > 1) { + g2d.setColor(SPRING_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f)); + double midY = bodyY + width / 2.0; + g2d.draw(new Rectangle2D.Double(x + length - 10, midY - 6, 6, 12)); + } + } + + g2d.setTransform(oldTx); + + // Draw control point solder tabs (Point 0 = + left, Point 1 = - right) + if (!outlineMode) { + for (Point2D cp : controlPoints) { + g2d.setColor(PLATE_COLOR); + g2d.fill(new Ellipse2D.Double(cp.getX() - 5, cp.getY() - 5, 10, 10)); + g2d.setColor(SPRING_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new Ellipse2D.Double(cp.getX() - 5, cp.getY() - 5, 10, 10)); + // Inner hole + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(cp.getX() - 2, cp.getY() - 2, 4, 4)); + } + } + + g2d.setComposite(oldComposite); + } + + private double cellH(double cellD) { + return cellD * 0.45; + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Dark holder body + g2d.setColor(HOLDER_BODY); + g2d.fill(new RoundRectangle2D.Double(2, height / 2.0 - 8, width - 4, 16, 4, 4)); + g2d.setColor(HOLDER_BODY.brighter()); + g2d.draw(new RoundRectangle2D.Double(2, height / 2.0 - 8, width - 4, 16, 4, 4)); + + // Cell cavity + g2d.setColor(CELL_CAVITY); + g2d.fill(new RoundRectangle2D.Double(5, height / 2.0 - 6, width - 10, 12, 3, 3)); + + // Plate on left (+), spring on right (-) + g2d.setColor(PLATE_COLOR); + g2d.fillRect(5, height / 2 - 3, 3, 6); + g2d.setColor(SPRING_COLOR); + g2d.fillRect(width - 9, height / 2 - 3, 4, 6); + + // Polarity + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + StringUtils.drawCenteredText(g2d, "+", 12, height / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "-", width - 13, height / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + @Override + public Rectangle2D getCachingBounds() { + double minX = Math.min(controlPoints[0].getX(), controlPoints[1].getX()); + double minY = Math.min(controlPoints[0].getY(), controlPoints[1].getY()); + double maxX = Math.max(controlPoints[0].getX(), controlPoints[1].getX()); + double maxY = Math.max(controlPoints[0].getY(), controlPoints[1].getY()); + double width = new Size(cellCount.getWidthMm(), SizeUnit.mm).convertToPixels(); + int margin = 50; + return new Rectangle2D.Double(minX - margin, minY - width / 2.0 - margin, + (maxX - minX) + 2 * margin, (maxY - minY) + width + 2 * margin); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java new file mode 100644 index 00000000..80784f37 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java @@ -0,0 +1,225 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +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.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 = "Active Buzzer Module (KY-012)", category = "Modules & Breakouts", + author = "Branislav Stojkovic", description = "KY-012 Active Buzzer Module — produces 2.5kHz continuous tone on HIGH signal", + instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ActiveBuzzerModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#1A6B1A"); + public static Color BUZZER_BLACK = Color.decode("#1A1A1A"); + public static Color BUZZER_VENT_COLOR = Color.decode("#333333"); + + public static Size BOARD_WIDTH = new Size(18.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(15.0d, SizeUnit.mm); + + // Standard Keyes KY-012 3-pin layout from left to right: - (GND), + (VCC), S (Signal) + private static final String[] PIN_NAMES = {"- (GND)", "+ (VCC)", "S (Signal)"}; + + public ActiveBuzzerModule() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @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(); + + // 3 pins along the bottom edge: - (GND), + (VCC), S (Signal) + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { spacing * 2, 0 } + }; + + 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 - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 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); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + double spacing = PIN_SPACING.convertToPixels(); + + 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) { + // Buzzer cylinder — circular element on the board + double buzzerD = new Size(11.0d, SizeUnit.mm).convertToPixels(); + double buzzerX = boardX + (boardW - buzzerD) / 2.0; + double buzzerY = boardY + 4; + + // Outer black cylinder + g2d.setColor(BUZZER_BLACK); + g2d.fill(new Ellipse2D.Double(buzzerX, buzzerY, buzzerD, buzzerD)); + g2d.setColor(BUZZER_BLACK.brighter()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(buzzerX, buzzerY, buzzerD, buzzerD)); + + // Vent hole ring (inner concentric) + double ventD = buzzerD * 0.55; + double ventX = buzzerX + (buzzerD - ventD) / 2.0; + double ventY = buzzerY + (buzzerD - ventD) / 2.0; + g2d.setColor(BUZZER_VENT_COLOR); + g2d.draw(new Ellipse2D.Double(ventX, ventY, ventD, ventD)); + + // Small central sound vent hole + double holeD = buzzerD * 0.15; + double holeX = buzzerX + (buzzerD - holeD) / 2.0; + double holeY = buzzerY + (buzzerD - holeD) / 2.0; + g2d.setColor(Color.decode("#111111")); + g2d.fill(new Ellipse2D.Double(holeX, holeY, holeD, holeD)); + + // Polarity "+" marking on buzzer casing + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 10)); + StringUtils.drawCenteredText(g2d, "+", buzzerX + 6, buzzerY + 7, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // "KY-012" model text in top-left silkscreen corner + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + StringUtils.drawCenteredText(g2d, "KY-012", boardX + 14, boardY + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen pin labels (-, +, S) directly above the 3 header pins + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + double labelY = y - 9; + StringUtils.drawCenteredText(g2d, "-", x, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "+", x + spacing, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "S", x + spacing * 2, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + drawPins(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver); + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + + // Black buzzer cylinder + int bD = width - 10; + int bX = (width - bD) / 2; + int bY = 5; + g2d.setColor(BUZZER_BLACK); + g2d.fillOval(bX, bY, bD, bD); + g2d.setColor(Color.DARK_GRAY); + g2d.drawOval(bX, bY, bD, bD); + + // Vent circle + int vD = bD / 2; + g2d.setColor(BUZZER_VENT_COLOR); + g2d.drawOval(bX + (bD - vD) / 2, bY + (bD - vD) / 2, vD, vD); + + // 3 pin dots + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 3; i++) { + g2d.fillRect(6 + i * 4, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java new file mode 100644 index 00000000..0dca2218 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java @@ -0,0 +1,245 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +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.Line2D; +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.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 = "GPS Module (NEO-6M)", category = "Modules & Breakouts", + author = "Branislav Stojkovic", description = "u-blox NEO-6M GPS Module with ceramic patch antenna", + instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class GPSModuleNEO6M extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#1A6B1A"); + public static Color ANTENNA_BEIGE = Color.decode("#D5C4A1"); + public static Color ANTENNA_BORDER = Color.decode("#928374"); + public static Color SHIELD_COLOR = Color.decode("#B8B8B8"); + public static Color SHIELD_BORDER = Color.decode("#888888"); + public static Color BATTERY_COLOR = Color.decode("#D0D3D4"); + + public static Size BOARD_WIDTH = new Size(35.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(25.5d, SizeUnit.mm); + + // Standard GY-GPS6MV2 pinout from pin 1 to 4: VCC, RX, TX, GND + private static final String[] PIN_NAMES = {"VCC", "RX", "TX", "GND"}; + + public GPSModuleNEO6M() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @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(); + + // 4 pins along the left edge: VCC, RX, TX, GND + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { 0, spacing }, + { 0, spacing * 2 }, + { 0, spacing * 3 } + }; + + 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 spacing = PIN_SPACING.convertToPixels(); + double boardX = x - 14.0; + double boardY = y - (boardH - 3 * spacing) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @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 spacing = PIN_SPACING.convertToPixels(); + double boardX = x - 14.0; + double boardY = y - (boardH - 3 * spacing) / 2.0; + + 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) { + // 4 Small Mounting holes in the corners + double holeMargin = 7.0; + drawMountingHole(g2d, boardX + holeMargin, boardY + holeMargin, 8.0); + drawMountingHole(g2d, boardX + holeMargin, boardY + boardH - holeMargin, 8.0); + drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + holeMargin, 8.0); + drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + boardH - holeMargin, 8.0); + + // Silkscreen pin labels (VCC, RX, TX, GND) placed neatly next to left header + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + String[] pinLabels = new String[] {"VCC", "RX", "TX", "GND"}; + for (int i = 0; i < 4; i++) { + StringUtils.drawCenteredText(g2d, pinLabels[i], x + 9, y + i * spacing, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + } + + // NEO-6M metal shield can (center of board, strictly to the right of pin labels) + double icShieldX = boardX + 46.0; + double icShieldW = 42.0; + double icShieldH = 36.0; + double icShieldY = boardY + (boardH - icShieldH) / 2.0 - 8.0; + + g2d.setColor(SHIELD_COLOR); + g2d.fill(new RoundRectangle2D.Double(icShieldX, icShieldY, icShieldW, icShieldH, 4, 4)); + g2d.setColor(SHIELD_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(icShieldX, icShieldY, icShieldW, icShieldH, 4, 4)); + + g2d.setColor(Color.decode("#333333")); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + StringUtils.drawCenteredText(g2d, "NEO-6M", icShieldX + icShieldW / 2.0, icShieldY + icShieldH / 2.0 - 4, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setFont(new Font("SansSerif", Font.PLAIN, 6)); + StringUtils.drawCenteredText(g2d, "GPS", icShieldX + icShieldW / 2.0, icShieldY + icShieldH / 2.0 + 5, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Rechargeable backup coin cell battery (CR1220 / MS621) below shield + double battD = 18.0; + double battX = boardX + 58.0; + double battY = boardY + boardH - battD - 7.0; + g2d.setColor(BATTERY_COLOR); + g2d.fill(new Ellipse2D.Double(battX, battY, battD, battD)); + g2d.setColor(BATTERY_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new Ellipse2D.Double(battX, battY, battD, battD)); + + // Ceramic patch antenna on right side (20x20mm) + double antSize = new Size(18.0d, SizeUnit.mm).convertToPixels(); + double antX = boardX + boardW - antSize - 8.0; + double antY = boardY + (boardH - antSize) / 2.0; + + g2d.setColor(ANTENNA_BEIGE); + g2d.fill(new RoundRectangle2D.Double(antX, antY, antSize, antSize, 2, 2)); + g2d.setColor(ANTENNA_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.2f)); + g2d.draw(new RoundRectangle2D.Double(antX, antY, antSize, antSize, 2, 2)); + + // Center silver electrode dot on antenna + double antCX = antX + antSize / 2.0; + double antCY = antY + antSize / 2.0; + g2d.setColor(Color.decode("#7F8C8D")); + g2d.fill(new Ellipse2D.Double(antCX - 2, antCY - 2, 4, 4)); + + // Status LED (top left area) + g2d.setColor(Color.RED); + g2d.fill(new Ellipse2D.Double(boardX + 24.0, boardY + 8.0, 5, 5)); + } + + g2d.setTransform(oldTx); + drawPins(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver); + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + + // Beige ceramic patch antenna (right half) + int antS = height - 12; + g2d.setColor(ANTENNA_BEIGE); + g2d.fillRect(width - antS - 3, (height - antS) / 2, antS, antS); + g2d.setColor(ANTENNA_BORDER); + g2d.drawRect(width - antS - 3, (height - antS) / 2, antS, antS); + + // Shield + g2d.setColor(SHIELD_COLOR); + g2d.fillRoundRect(4, (height - 10) / 2, 10, 10, 2, 2); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java new file mode 100644 index 00000000..861cf4dd --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java @@ -0,0 +1,317 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +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.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.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 = "Current/Power Sensor (INA219)", category = "Modules & Breakouts", + author = "Branislav Stojkovic", description = "INA219 I2C High-Side Current and Power Sensor Breakout", + instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class INA219CurrentSensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#115E1B"); + public static Color TERMINAL_BLUE = Color.decode("#1565C0"); + public static Color TERMINAL_BLUE_BORDER = Color.decode("#0D47A1"); + public static Color SHUNT_BODY = Color.decode("#7D6608"); + public static Color SHUNT_METAL = Color.decode("#D4AC0D"); + public static Color SHUNT_CORE = Color.decode("#2C3E50"); + public static Color SILK_YELLOW = Color.decode("#F4D03F"); + + public static Size BOARD_WIDTH = new Size(25.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(25.0d, SizeUnit.mm); + + // 6 header pins at bottom (0-5) + 2 screw terminal pins at top (6-7) + private static final String[] PIN_NAMES = { + "VCC", "GND", "SCL", "SDA", "VIN-", "VIN+", + "VIN- (Term)", "VIN+ (Term)" + }; + + public INA219CurrentSensor() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @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(); + double boardH = BOARD_HEIGHT.convertToPixels(); + + // 8 control points total: + // Pins 0-5: Bottom horizontal 6-pin header (VCC, GND, SCL, SDA, VIN-, VIN+) + // Pins 6-7: Top 2-pin screw terminal block (VIN-, VIN+) + double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels(); + double termCenterX = 2.5 * spacing; + double termY = -boardH + 24.0; + + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { spacing * 2, 0 }, + { spacing * 3, 0 }, + { spacing * 4, 0 }, + { spacing * 5, 0 }, + { termCenterX - termSpacing / 2.0, termY }, + { termCenterX + termSpacing / 2.0, termY } + }; + + 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 spacing = PIN_SPACING.convertToPixels(); + double boardX = x - (boardW - 5 * spacing) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @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 spacing = PIN_SPACING.convertToPixels(); + double boardX = x - (boardW - 5 * spacing) / 2.0; + double boardY = y - boardH + 12; + + 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) { + // 4 Large Gold Ring Mounting Holes in corners (20mm x 20mm pitch) + double holeMargin = 10.0; + drawMountingHole(g2d, boardX + holeMargin, boardY + holeMargin, 13.0); + drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + holeMargin, 13.0); + drawMountingHole(g2d, boardX + holeMargin, boardY + boardH - holeMargin, 13.0); + drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + boardH - holeMargin, 13.0); + + // Top Blue Screw Terminal Block (5.08mm 2-pin block) + double termW = 44.0; + double termH = 30.0; + double termX = boardX + (boardW - termW) / 2.0; + double termY = boardY + 2.0; + + // Blue outer block body + g2d.setColor(TERMINAL_BLUE); + g2d.fill(new RoundRectangle2D.Double(termX, termY, termW, termH, 3, 3)); + g2d.setColor(TERMINAL_BLUE_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(termX, termY, termW, termH, 3, 3)); + + // Terminal block wire entry slots on top + g2d.setColor(Color.decode("#0D47A1")); + g2d.fillRect((int) (termX + 4), (int) (termY + 1), (int) (termW - 8), 4); + + // Screw contacts + double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels(); + double screw1X = termX + termW / 2.0 - termSpacing / 2.0; + double screw2X = termX + termW / 2.0 + termSpacing / 2.0; + double screwY = termY + termH / 2.0 + 2.0; + double screwD = 14.0; + + g2d.setColor(SCREW_CIRCLE_COLOR); + g2d.fill(new Ellipse2D.Double(screw1X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.fill(new Ellipse2D.Double(screw2X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.setColor(SCREW_CIRCLE_COLOR.darker()); + g2d.draw(new Ellipse2D.Double(screw1X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.draw(new Ellipse2D.Double(screw2X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + // Screw diagonal slots + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.drawLine((int) (screw1X - 4), (int) (screwY - 4), (int) (screw1X + 4), (int) (screwY + 4)); + g2d.drawLine((int) (screw2X - 4), (int) (screwY - 4), (int) (screw2X + 4), (int) (screwY + 4)); + + // Shunt Resistor (Center-Upper, directly below terminal block) + double shuntW = 32.0; + double shuntH = 14.0; + double shuntX = boardX + (boardW - shuntW) / 2.0; + double shuntY = boardY + termH + 6.0; + + // Copper outer tabs + g2d.setColor(Color.decode("#C0392B")); + g2d.fill(new RoundRectangle2D.Double(shuntX, shuntY, 7, shuntH, 2, 2)); + g2d.fill(new RoundRectangle2D.Double(shuntX + shuntW - 7, shuntY, 7, shuntH, 2, 2)); + + // Metal sense element (arched center) + g2d.setColor(Color.decode("#BDC3C7")); + g2d.fill(new Rectangle2D.Double(shuntX + 6, shuntY + 1, shuntW - 12, shuntH - 2)); + g2d.setColor(Color.decode("#7F8C8D")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new Rectangle2D.Double(shuntX + 6, shuntY + 1, shuntW - 12, shuntH - 2)); + + // "R100" label on shunt + g2d.setColor(Color.decode("#2C3E50")); + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + StringUtils.drawCenteredText(g2d, "R100", shuntX + shuntW / 2.0, shuntY + shuntH / 2.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // INA219 SOIC-8 IC (Center-Lower, directly below shunt) + double icW = 20.0; + double icH = 14.0; + double icX = boardX + (boardW - icW) / 2.0; + double icY = shuntY + shuntH + 4.0; + + // IC body + g2d.setColor(Color.decode("#1C2833")); + g2d.fill(new RoundRectangle2D.Double(icX, icY, icW, icH, 2, 2)); + g2d.setColor(Color.decode("#111111")); + g2d.draw(new RoundRectangle2D.Double(icX, icY, icW, icH, 2, 2)); + + // 8 silver IC pins (4 top, 4 bottom) + g2d.setColor(Color.decode("#BDC3C7")); + for (int i = 0; i < 4; i++) { + double px = icX + 2.5 + i * 4.5; + g2d.fillRect((int) px, (int) (icY - 2), 2, 2); + g2d.fillRect((int) px, (int) (icY + icH), 2, 2); + } + // Pin 1 dot + g2d.setColor(Color.decode("#F4D03F")); + g2d.fill(new Ellipse2D.Double(icX + 2.5, icY + 2.5, 2.5, 2.5)); + + // Vertical silkscreen text on left edge + AffineTransform preTextTx = g2d.getTransform(); + g2d.translate(boardX + 14, boardY + boardH / 2.0); + g2d.rotate(-Math.PI / 2.0); + g2d.setColor(SILK_YELLOW); + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + StringUtils.drawCenteredText(g2d, "INA219 SENSOR", 0, 0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setTransform(preTextTx); + + // Vertical silkscreen labels above bottom 6 header pins (VCC, GND, SCL, SDA, VIN-, VIN+) + String[] headerLabels = new String[] {"VCC", "GND", "SCL", "SDA", "VIN-", "VIN+"}; + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + g2d.setColor(SILK_YELLOW); + + for (int i = 0; i < 6; i++) { + double pinX = x + i * spacing; + AffineTransform pTx = g2d.getTransform(); + g2d.translate(pinX, y - 11); + g2d.rotate(-Math.PI / 2.0); + StringUtils.drawCenteredText(g2d, headerLabels[i], 0, 0, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + g2d.setTransform(pTx); + } + } + + g2d.setTransform(oldTx); + + // Draw bottom 6 header pins + drawPins(g2d, 0, 6, false, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green square PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + + // Blue terminal block at top + g2d.setColor(TERMINAL_BLUE); + g2d.fillRoundRect(width / 2 - 8, 3, 16, 8, 2, 2); + + // Shunt in middle + g2d.setColor(Color.decode("#C0392B")); + g2d.fillRect(width / 2 - 6, 13, 12, 4); + g2d.setColor(Color.decode("#BDC3C7")); + g2d.fillRect(width / 2 - 3, 13, 6, 4); + + // Small IC below shunt + g2d.setColor(Color.decode("#1C2833")); + g2d.fillRect(width / 2 - 4, 19, 8, 5); + + // 6 pin dots at bottom + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 6; i++) { + g2d.fillRect(4 + i * 4, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java new file mode 100644 index 00000000..d95edb5a --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java @@ -0,0 +1,214 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +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.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 = "IR Receiver Module (KY-022)", category = "Sensors", + author = "Branislav Stojkovic", description = "38kHz Infrared Receiver Module (VS1838B) for remote control", + instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class IRReceiverModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#1A6B1A"); + public static Color SENSOR_METAL = Color.decode("#85929E"); + public static Color SENSOR_DOME = Color.decode("#1C2833"); + + public static Size BOARD_WIDTH = new Size(18.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(15.0d, SizeUnit.mm); + + // Standard Keyes KY-022 3-pin layout from left to right: - (GND), + (VCC), S (Signal) + private static final String[] PIN_NAMES = {"- (GND)", "+ (VCC)", "S (Signal)"}; + + public IRReceiverModule() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @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(); + + // 3 pins along the bottom edge: - (GND), + (VCC), S (Signal) + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { spacing * 2, 0 } + }; + + 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 - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 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); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + double spacing = PIN_SPACING.convertToPixels(); + + 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) { + // VS1838B sensor element — metal-shielded rectangular body with central black IR dome + double sensorW = new Size(7.5d, SizeUnit.mm).convertToPixels(); + double sensorH = new Size(6.0d, SizeUnit.mm).convertToPixels(); + double sensorX = boardX + (boardW - sensorW) / 2.0; + double sensorY = boardY + 6; + + // Metal shielded box + g2d.setColor(SENSOR_METAL); + g2d.fill(new RoundRectangle2D.Double(sensorX, sensorY, sensorW, sensorH, 3, 3)); + g2d.setColor(SENSOR_METAL.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(sensorX, sensorY, sensorW, sensorH, 3, 3)); + + // Central dark IR lens dome + double domeD = sensorH * 0.75; + g2d.setColor(SENSOR_DOME); + g2d.fill(new Ellipse2D.Double(sensorX + (sensorW - domeD) / 2.0, sensorY + (sensorH - domeD) / 2.0, domeD, domeD)); + g2d.setColor(Color.BLACK); + g2d.draw(new Ellipse2D.Double(sensorX + (sensorW - domeD) / 2.0, sensorY + (sensorH - domeD) / 2.0, domeD, domeD)); + + // Red status LED (top right) + g2d.setColor(Color.RED); + g2d.fill(new Ellipse2D.Double(boardX + boardW - 12, boardY + 6, 6, 6)); + + // "KY-022" silkscreen text in the open space between the sensor and pin labels + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + StringUtils.drawCenteredText(g2d, "KY-022", boardX + boardW / 2.0, boardY + 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen pin labels (-, +, S) directly above the 3 header pins + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + double labelY = y - 9; + StringUtils.drawCenteredText(g2d, "-", x, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "+", x + spacing, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "S", x + spacing * 2, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + drawPins(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver); + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + + // Metal shield + dome + g2d.setColor(SENSOR_METAL); + g2d.fillRoundRect(width / 2 - 5, 6, 10, 8, 2, 2); + g2d.setColor(SENSOR_DOME); + g2d.fillOval(width / 2 - 3, 7, 6, 6); + + // Red LED + g2d.setColor(Color.RED); + g2d.fillOval(width - 7, 7, 4, 4); + + // 3 pin dots + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 3; i++) { + g2d.fillRect(6 + i * 4, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java index 396722be..cf48ff52 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java +++ b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java @@ -33,6 +33,7 @@ import org.diylc.common.ComponentType; import org.diylc.common.DefaultComponentTransformer; import org.diylc.common.Orientation; import org.diylc.components.displays.*; +import org.diylc.components.electromechanical.*; import org.diylc.components.micro.*; import org.diylc.components.modules.*; import org.diylc.components.robotics.*; @@ -78,6 +79,7 @@ public class MakerComponentsTest { BME280Sensor.class, SoilMoistureSensor.class, TCRT5000Sensor.class, + IRReceiverModule.class, // Displays & Outputs CharacterLCD.class, @@ -86,6 +88,7 @@ public class MakerComponentsTest { LEDMatrix8x8.class, TFTDisplay.class, WS2812BStick.class, + WS2812BRing.class, // Modules & Breakouts LogicLevelConverter.class, @@ -106,6 +109,12 @@ public class MakerComponentsTest { ULN2003Driver.class, PCA9685ServoDriver.class, MOSFETSwitchModule.class, + ActiveBuzzerModule.class, + GPSModuleNEO6M.class, + INA219CurrentSensor.class, + + // Electro-Mechanical + BatteryHolder18650.class, // Robotics MicroServoSG90.class,