diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java index b7b9f054..e55ada71 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java @@ -21,14 +21,8 @@ */ package org.diylc.components.micro; -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.Point2D; -import java.awt.geom.RoundRectangle2D; +import java.awt.*; +import java.awt.geom.*; import org.diylc.awt.StringUtils; import org.diylc.common.HorizontalAlignment; @@ -48,7 +42,7 @@ import org.diylc.core.measures.SizeUnit; import org.diylc.utils.Constants; @ComponentDescriptor(name = "ESP32 DevKit", category = "Controllers", - author = "Branislav Stojkovic", description = "ESP32 NodeMCU DevKit V1 Wi-Fi and Bluetooth Development Board", + author = "Branislav Stojkovic", description = "ESP32 DevKit V1 DOIT Wi-Fi and Bluetooth Development Board", instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, enableCache = true) @@ -57,8 +51,12 @@ public class ESP32DevKit extends AbstractMakerBoard { private static final long serialVersionUID = 1L; public static Color ESP_BLACK = Color.decode("#1E1E1E"); - public static Size BOARD_WIDTH = new Size(28.5d, SizeUnit.mm); - public static Size BOARD_LENGTH = new Size(51.5d, SizeUnit.mm); + public static Size BOARD_WIDTH = new Size(28.2d, SizeUnit.mm); + public static Size BOARD_LENGTH = new Size(51.8d, SizeUnit.mm); + public static Size TOP_MARGIN = new Size(6.7d, SizeUnit.mm); + public static Size ANTENNA_LENGTH = new Size(6.2d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(20.0d, SizeUnit.mm); + public static Size ROW_SPACING = new Size(0.9d, SizeUnit.in); public static final String[] PIN_NAMES = new String[] { // Left row (pins 0..14) @@ -85,7 +83,7 @@ public class ESP32DevKit extends AbstractMakerBoard { protected void updateControlPoints() { Point2D firstPoint = controlPoints[0]; double spacing = PIN_SPACING.convertToPixels(); - double rowSpacing = new Size(0.9d, SizeUnit.in).convertToPixels(); // 180px + double rowSpacing = ROW_SPACING.convertToPixels(); // 180px double[][] relativeOffsets = new double[30][2]; for (int i = 0; i < 15; i++) { @@ -105,11 +103,12 @@ public class ESP32DevKit extends AbstractMakerBoard { Point2D p0 = controlPoints[0]; double x = p0.getX(); double y = p0.getY(); - double rowSpacing = new Size(0.9d, SizeUnit.in).convertToPixels(); - double boardW = rowSpacing + 26; - double boardH = 16 * PIN_SPACING.convertToPixels(); - double boardX = x - 13; - double boardY = y - 10; + double rowSpacing = ROW_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_LENGTH.convertToPixels(); + double topMargin = TOP_MARGIN.convertToPixels(); + double boardX = (x + rowSpacing / 2.0) - boardW / 2.0; + double boardY = y - topMargin; return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); } @@ -129,11 +128,17 @@ public class ESP32DevKit extends AbstractMakerBoard { g2d.rotate(orientation.toRadians(), x, y); } - double rowSpacing = new Size(0.9d, SizeUnit.in).convertToPixels(); - double boardW = rowSpacing + 26; - double boardH = 16 * PIN_SPACING.convertToPixels(); - double boardX = x - 13; - double boardY = y - 10; + double rowSpacing = ROW_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_LENGTH.convertToPixels(); + double topMargin = TOP_MARGIN.convertToPixels(); + double boardX = (x + rowSpacing / 2.0) - boardW / 2.0; + double boardY = y - topMargin; + + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH.convertToPixels(); + double antennaX = (x + rowSpacing / 2.0) - antennaW / 2.0; + double antennaY = boardY; Shape boardShape = getBodyShape(); @@ -149,28 +154,80 @@ public class ESP32DevKit extends AbstractMakerBoard { g2d.draw(boardShape); if (!outlineMode) { - // Micro-USB Jack at bottom - drawMetalConnector(g2d, boardX + (boardW - 60) / 2.0, boardY + boardH - 25, 60, 35, "USB"); + // 4 Corner Mounting Holes (diameter 2.8mm) + double holeDiameter = new Size(2.8d, SizeUnit.mm).convertToPixels(); + double topHoleY = boardY + new Size(3.5d, SizeUnit.mm).convertToPixels(); + double bottomHoleY = boardY + boardH - new Size(3.5d, SizeUnit.mm).convertToPixels(); + double leftHoleX = x; + double rightHoleX = x + rowSpacing; - // ESP-WROOM-32 metal shield module with PCB antenna at top - drawMetalConnector(g2d, boardX + 15, boardY + 40, boardW - 30, 120, "ESP-WROOM-32"); + drawMountingHole(g2d, leftHoleX, topHoleY, holeDiameter); + drawMountingHole(g2d, rightHoleX, topHoleY, holeDiameter); + drawMountingHole(g2d, leftHoleX, bottomHoleY, holeDiameter); + drawMountingHole(g2d, rightHoleX, bottomHoleY, holeDiameter); - // PCB antenna trace area (gold/copper) + // Antenna trace (gold/copper serpentine PCB trace) g2d.setColor(Color.decode("#DAA520")); - g2d.fillRect((int) (boardX + 25), (int) (boardY + 10), (int) (boardW - 50), 20); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + Path2D.Double antPath = new Path2D.Double(); + double antPadX = antennaX + 16; + double antPadW = antennaW - 32; + double antTopY = antennaY + 10; + double antMidY = antennaY + antennaH - 12; + antPath.moveTo(antPadX, antMidY); + antPath.lineTo(antPadX, antTopY); + antPath.lineTo(antPadX + antPadW * 0.25, antTopY); + antPath.lineTo(antPadX + antPadW * 0.25, antMidY); + antPath.lineTo(antPadX + antPadW * 0.50, antMidY); + antPath.lineTo(antPadX + antPadW * 0.50, antTopY); + antPath.lineTo(antPadX + antPadW * 0.75, antTopY); + antPath.lineTo(antPadX + antPadW * 0.75, antMidY); + antPath.lineTo(antPadX + antPadW, antMidY); + antPath.lineTo(antPadX + antPadW, antTopY); + g2d.draw(antPath); + + // ESP32-WROOM-32 metal shield module below antenna + double shieldW = new Size(18.0d, SizeUnit.mm).convertToPixels(); + double shieldH = new Size(18.0d, SizeUnit.mm).convertToPixels(); + double shieldX = (x + rowSpacing / 2.0) - shieldW / 2.0; + double shieldY = boardY + antennaH; + drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP32-WROOM-32"); + + // Micro-USB Jack at bottom + double usbW = 58; + double usbH = 34; + double usbX = (x + rowSpacing / 2.0) - usbW / 2.0; + double usbY = boardY + boardH - 22; + drawMetalConnector(g2d, usbX, usbY, usbW, usbH, "USB"); + + // EN & BOOT tactile buttons at bottom (flanking the Micro-USB port) + double btnW = 20; + double btnH = 20; + double btnY = boardY + boardH - 25; + double shift1mm = new Size(1.0d, SizeUnit.mm).convertToPixels(); + double btnLeftX = leftHoleX + 14 + shift1mm; + double btnRightX = rightHoleX - 14 - shift1mm - btnW; + + g2d.setColor(Color.decode("#383838")); + g2d.fill(new RoundRectangle2D.Double(btnLeftX, btnY, btnW, btnH, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(btnRightX, btnY, btnW, btnH, 3, 3)); + g2d.setColor(Color.decode("#666666")); + g2d.draw(new RoundRectangle2D.Double(btnLeftX, btnY, btnW, btnH, 3, 3)); + g2d.draw(new RoundRectangle2D.Double(btnRightX, btnY, btnW, btnH, 3, 3)); + + // Button actuators + g2d.setColor(Color.decode("#A0A0A0")); + g2d.fillOval((int) (btnLeftX + btnW / 2.0 - 4), (int) (btnY + btnH / 2.0 - 4), 8, 8); + g2d.fillOval((int) (btnRightX + btnW / 2.0 - 4), (int) (btnY + btnH / 2.0 - 4), 8, 8); - // EN & BOOT buttons - g2d.setColor(Color.decode("#444444")); - g2d.fill(new RoundRectangle2D.Double(boardX + 15, boardY + boardH - 45, 25, 18, 3, 3)); - g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 40, boardY + boardH - 45, 25, 18, 3, 3)); g2d.setColor(Color.WHITE); g2d.setFont(SILK_FONT_SMALL); - StringUtils.drawCenteredText(g2d, "EN", boardX + 27, boardY + boardH - 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); - StringUtils.drawCenteredText(g2d, "BOOT", boardX + boardW - 28, boardY + boardH - 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "EN", btnLeftX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "BOOT", btnRightX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); // Silkscreen g2d.setFont(SILK_FONT); - StringUtils.drawCenteredText(g2d, "ESP32", boardX + boardW / 2.0, boardY + 190, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "ESP32", x + rowSpacing / 2.0, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); } g2d.setTransform(oldTx); diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKitC.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKitC.java new file mode 100644 index 00000000..50963c1d --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKitC.java @@ -0,0 +1,273 @@ +/* + * + * 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.micro; + +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; + +import java.awt.*; +import java.awt.geom.*; + +@ComponentDescriptor(name = "ESP32 DevKitC", category = "Controllers", + author = "Branislav Stojkovic", description = "ESP32 DevKitC V4 Wi-Fi and Bluetooth Development Board", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ESP32DevKitC extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color ESP_BLACK = Color.decode("#1E1E1E"); + public static Size BOARD_WIDTH = new Size(27.9d, SizeUnit.mm); + public static Size BOARD_LENGTH = new Size(54.4d, SizeUnit.mm); + public static Size MAIN_BODY_LENGTH = new Size(48.2d, SizeUnit.mm); + public static Size ANTENNA_LENGTH = new Size(6.2d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(20.0d, SizeUnit.mm); + public static Size ROW_SPACING = new Size(1.0d, SizeUnit.in); + + public static final String[] PIN_NAMES = new String[] { + // Left row (pins 0..18, top to bottom) + "3V3", "EN", "VP (GPIO36)", "VN (GPIO39)", "GPIO34", "GPIO35", "GPIO32", "GPIO33", + "GPIO25", "GPIO26", "GPIO27", "GPIO14", "GPIO12", "GND1", "GPIO13", + "D2 (GPIO9)", "D3 (GPIO10)", "CMD (GPIO11)", "5V", + // Right row (pins 19..37, top to bottom) + "GND2", "GPIO23", "GPIO22", "TX0 (GPIO1)", "RX0 (GPIO3)", "GPIO21", "GND3", + "GPIO19", "GPIO18", "GPIO5", "GPIO17", "GPIO16", "GPIO4", "GPIO0", "GPIO2", + "GPIO15", "D1 (GPIO8)", "D0 (GPIO7)", "CLK (GPIO6)" + }; + + public ESP32DevKitC() { + super(); + this.bodyColor = ESP_BLACK; + updateControlPoints(); + } + + @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 rowSpacing = ROW_SPACING.convertToPixels(); + + double[][] relativeOffsets = new double[PIN_NAMES.length][2]; + for (int i = 0; i < 19; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 19; i++) { + relativeOffsets[19 + i][0] = rowSpacing; + relativeOffsets[19 + i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + double rowSpacing = ROW_SPACING.convertToPixels(); + double mainW = BOARD_WIDTH.convertToPixels(); + double mainH = MAIN_BODY_LENGTH.convertToPixels(); + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH.convertToPixels(); + + double mainX = (x + rowSpacing / 2.0) - mainW / 2.0; + double mainY = (y + 18 * PIN_SPACING.convertToPixels() / 2.0) - mainH / 2.0; + double antennaX = (x + rowSpacing / 2.0) - antennaW / 2.0; + double antennaY = mainY - antennaH; + + Area bodyArea = new Area(new RoundRectangle2D.Double(mainX, mainY, mainW, mainH, 8, 8)); + bodyArea.add(new Area(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH + 1))); + return bodyArea; + } + + @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 rowSpacing = ROW_SPACING.convertToPixels(); + double mainW = BOARD_WIDTH.convertToPixels(); + double mainH = MAIN_BODY_LENGTH.convertToPixels(); + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH.convertToPixels(); + + double mainX = (x + rowSpacing / 2.0) - mainW / 2.0; + double mainY = (y + 18 * PIN_SPACING.convertToPixels() / 2.0) - mainH / 2.0; + double antennaX = (x + rowSpacing / 2.0) - antennaW / 2.0; + double antennaY = mainY - antennaH; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + if (outlineMode) { + g2d.setColor(Constants.TRANSPARENT_COLOR); + g2d.fill(boardShape); + } else { + // Main board body + g2d.setColor(bodyColor); + g2d.fill(new RoundRectangle2D.Double(mainX, mainY, mainW, mainH, 8, 8)); + + // Antenna tab in slightly lighter color + Color antennaColor = new Color( + Math.min(255, bodyColor.getRed() + 32), + Math.min(255, bodyColor.getGreen() + 32), + Math.min(255, bodyColor.getBlue() + 32) + ); + g2d.setColor(antennaColor); + g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH)); + } + drawingObserver.stopTracking(); + + // Outline border around the full board + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Antenna trace (gold/copper serpentine PCB trace) + g2d.setColor(Color.decode("#DAA520")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + Path2D.Double antPath = new Path2D.Double(); + double antPadX = antennaX + 16; + double antPadW = antennaW - 32; + double antTopY = antennaY + 10; + double antMidY = antennaY + antennaH - 12; + antPath.moveTo(antPadX, antMidY); + antPath.lineTo(antPadX, antTopY); + antPath.lineTo(antPadX + antPadW * 0.25, antTopY); + antPath.lineTo(antPadX + antPadW * 0.25, antMidY); + antPath.lineTo(antPadX + antPadW * 0.50, antMidY); + antPath.lineTo(antPadX + antPadW * 0.50, antTopY); + antPath.lineTo(antPadX + antPadW * 0.75, antTopY); + antPath.lineTo(antPadX + antPadW * 0.75, antMidY); + antPath.lineTo(antPadX + antPadW, antMidY); + antPath.lineTo(antPadX + antPadW, antTopY); + g2d.draw(antPath); + + // Micro-USB Jack at bottom + double usbW = 58; + double usbH = 34; + double usbX = (x + rowSpacing / 2.0) - usbW / 2.0; + double usbY = mainY + mainH - 22; + drawMetalConnector(g2d, usbX, usbY, usbW, usbH, "USB"); + + // ESP32-WROOM-32 metal shield module below antenna + double shieldW = new Size(18.0d, SizeUnit.mm).convertToPixels(); + double shieldH = new Size(18.0d, SizeUnit.mm).convertToPixels(); + double shieldX = (x + rowSpacing / 2.0) - shieldW / 2.0; + double shieldY = mainY + 10; + drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP32-WROOM-32"); + + // EN & BOOT tactile buttons at bottom (flanking the Micro-USB port) + double btnW = 20; + double btnH = 20; + double btnY = mainY + mainH - 25; + double btnLeftX = mainX + 38; + double btnRightX = mainX + mainW - 38 - btnW; + + g2d.setColor(Color.decode("#383838")); + g2d.fill(new RoundRectangle2D.Double(btnLeftX, btnY, btnW, btnH, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(btnRightX, btnY, btnW, btnH, 3, 3)); + g2d.setColor(Color.decode("#666666")); + g2d.draw(new RoundRectangle2D.Double(btnLeftX, btnY, btnW, btnH, 3, 3)); + g2d.draw(new RoundRectangle2D.Double(btnRightX, btnY, btnW, btnH, 3, 3)); + + // Button actuators + g2d.setColor(Color.decode("#A0A0A0")); + g2d.fillOval((int) (btnLeftX + btnW / 2.0 - 4), (int) (btnY + btnH / 2.0 - 4), 8, 8); + g2d.fillOval((int) (btnRightX + btnW / 2.0 - 4), (int) (btnY + btnH / 2.0 - 4), 8, 8); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "EN", btnLeftX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "BOOT", btnRightX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "ESP32", x + rowSpacing / 2.0, mainY + shieldH + 45, 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) { + g2d.setColor(ESP_BLACK); + g2d.fill(new RoundRectangle2D.Double(5, 6, width - 10, height - 8, 3, 3)); + + // Antenna tab + g2d.setColor(Color.decode("#383838")); + g2d.fillRect(8, 2, width - 16, 5); + g2d.setColor(Color.GRAY); + g2d.draw(new RoundRectangle2D.Double(5, 6, width - 10, height - 8, 3, 3)); + + // Metal shield + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fillRect(8, 9, width - 16, 10); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "ESP32", width / 2, height / 2 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP8266NodeMCU.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP8266NodeMCU.java index 9c70f5ac..48e70ce0 100644 --- a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP8266NodeMCU.java +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP8266NodeMCU.java @@ -27,6 +27,7 @@ import java.awt.Font; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.RoundRectangle2D; @@ -56,9 +57,16 @@ public class ESP8266NodeMCU extends AbstractMakerBoard { private static final long serialVersionUID = 1L; - public static Color NODEMCU_BLACK = Color.decode("#1C1C1C"); - public static Size BOARD_WIDTH = new Size(25.4d, SizeUnit.mm); - public static Size BOARD_LENGTH = new Size(48.3d, SizeUnit.mm); + public static Color NODEMCU_BLACK = Color.decode("#1E1E1E"); + public static Size BOARD_WIDTH = new Size(25.7d, SizeUnit.mm); + public static Size BOARD_LENGTH = new Size(48.0d, SizeUnit.mm); + public static Size TOP_MARGIN = new Size(6.22d, SizeUnit.mm); + public static Size ANTENNA_LENGTH = new Size(6.0d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(16.0d, SizeUnit.mm); + public static Size ROW_SPACING = new Size(0.9d, SizeUnit.in); + public static Size HOLE_DISTANCE_X = new Size(21.0d, SizeUnit.mm); + public static Size HOLE_DISTANCE_Y = new Size(44.0d, SizeUnit.mm); + public static Size HOLE_DIAMETER = new Size(3.2d, SizeUnit.mm); public static final String[] PIN_NAMES = new String[] { // Left row (pins 0..14) @@ -85,7 +93,7 @@ public class ESP8266NodeMCU extends AbstractMakerBoard { protected void updateControlPoints() { Point2D firstPoint = controlPoints[0]; double spacing = PIN_SPACING.convertToPixels(); - double rowSpacing = new Size(0.9d, SizeUnit.in).convertToPixels(); // 180px + double rowSpacing = ROW_SPACING.convertToPixels(); // 180px double[][] relativeOffsets = new double[30][2]; for (int i = 0; i < 15; i++) { @@ -105,11 +113,12 @@ public class ESP8266NodeMCU extends AbstractMakerBoard { Point2D p0 = controlPoints[0]; double x = p0.getX(); double y = p0.getY(); - double rowSpacing = new Size(0.9d, SizeUnit.in).convertToPixels(); - double boardW = rowSpacing + 26; - double boardH = 16 * PIN_SPACING.convertToPixels(); - double boardX = x - 13; - double boardY = y - 10; + double rowSpacing = ROW_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_LENGTH.convertToPixels(); + double topMargin = TOP_MARGIN.convertToPixels(); + double boardX = (x + rowSpacing / 2.0) - boardW / 2.0; + double boardY = y - topMargin; return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); } @@ -129,11 +138,17 @@ public class ESP8266NodeMCU extends AbstractMakerBoard { g2d.rotate(orientation.toRadians(), x, y); } - double rowSpacing = new Size(0.9d, SizeUnit.in).convertToPixels(); - double boardW = rowSpacing + 26; - double boardH = 16 * PIN_SPACING.convertToPixels(); - double boardX = x - 13; - double boardY = y - 10; + double rowSpacing = ROW_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_LENGTH.convertToPixels(); + double topMargin = TOP_MARGIN.convertToPixels(); + double boardX = (x + rowSpacing / 2.0) - boardW / 2.0; + double boardY = y - topMargin; + + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH.convertToPixels(); + double antennaX = (x + rowSpacing / 2.0) - antennaW / 2.0; + double antennaY = boardY + new Size(1.0d, SizeUnit.mm).convertToPixels(); Shape boardShape = getBodyShape(); @@ -149,28 +164,88 @@ public class ESP8266NodeMCU extends AbstractMakerBoard { g2d.draw(boardShape); if (!outlineMode) { - // Micro-USB Jack at bottom - drawMetalConnector(g2d, boardX + (boardW - 60) / 2.0, boardY + boardH - 20, 60, 30, "USB"); + // 4 Corner Mounting Holes (diameter 3.2mm, spaced 44mm lengthwise, 21mm widthwise) + double holeDiameter = HOLE_DIAMETER.convertToPixels(); + double holeDistX = HOLE_DISTANCE_X.convertToPixels(); + double holeDistY = HOLE_DISTANCE_Y.convertToPixels(); + double topHoleY = boardY + (boardH - holeDistY) / 2.0; + double bottomHoleY = boardY + (boardH + holeDistY) / 2.0; + double leftHoleX = (x + rowSpacing / 2.0) - holeDistX / 2.0; + double rightHoleX = (x + rowSpacing / 2.0) + holeDistX / 2.0; - // ESP-12F Metal shield with antenna at top - drawMetalConnector(g2d, boardX + 20, boardY + 40, boardW - 40, 110, "ESP-12F"); + drawMountingHole(g2d, leftHoleX, topHoleY, holeDiameter); + drawMountingHole(g2d, rightHoleX, topHoleY, holeDiameter); + drawMountingHole(g2d, leftHoleX, bottomHoleY, holeDiameter); + drawMountingHole(g2d, rightHoleX, bottomHoleY, holeDiameter); - // Copper trace antenna + // Antenna trace (gold/copper serpentine PCB trace) g2d.setColor(Color.decode("#DAA520")); - g2d.fillRect((int) (boardX + 25), (int) (boardY + 10), (int) (boardW - 50), 20); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + Path2D.Double antPath = new Path2D.Double(); + double antPadX = antennaX + 12; + double antPadW = antennaW - 24; + double antTopY = antennaY + 4; + double antMidY = antennaY + antennaH - 6; + antPath.moveTo(antPadX, antMidY); + antPath.lineTo(antPadX, antTopY); + antPath.lineTo(antPadX + antPadW * 0.25, antTopY); + antPath.lineTo(antPadX + antPadW * 0.25, antMidY); + antPath.lineTo(antPadX + antPadW * 0.50, antMidY); + antPath.lineTo(antPadX + antPadW * 0.50, antTopY); + antPath.lineTo(antPadX + antPadW * 0.75, antTopY); + antPath.lineTo(antPadX + antPadW * 0.75, antMidY); + antPath.lineTo(antPadX + antPadW, antMidY); + antPath.lineTo(antPadX + antPadW, antTopY); + g2d.draw(antPath); + + // ESP-12 Metal shield module below antenna + double shieldW = new Size(16.0d, SizeUnit.mm).convertToPixels(); + double shieldH = new Size(15.0d, SizeUnit.mm).convertToPixels(); + double shieldX = (x + rowSpacing / 2.0) - shieldW / 2.0; + double shieldY = antennaY + antennaH + 2; + drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP8266"); + + // USB to UART chip (CP2102) + double chipSize = new Size(4.5d, SizeUnit.mm).convertToPixels(); + double chipX = (x + rowSpacing / 2.0) - chipSize / 2.0; + double chipY = shieldY + shieldH + new Size(4.0d, SizeUnit.mm).convertToPixels(); + drawChip(g2d, chipX, chipY, chipSize, chipSize, "CP2102"); + + // Micro-USB Jack at bottom + double usbW = 54; + double usbH = 32; + double usbX = (x + rowSpacing / 2.0) - usbW / 2.0; + double usbY = boardY + boardH - 22; + drawMetalConnector(g2d, usbX, usbY, usbW, usbH, "USB"); + + // RST & FLASH tactile buttons at bottom (flanking the Micro-USB port) + double btnW = 18; + double btnH = 18; + double btnY = boardY + boardH - 25; + double shift1mm = new Size(1.0d, SizeUnit.mm).convertToPixels(); + double btnLeftX = leftHoleX + 10 + shift1mm; + double btnRightX = rightHoleX - 10 - shift1mm - btnW; + + g2d.setColor(Color.decode("#383838")); + g2d.fill(new RoundRectangle2D.Double(btnLeftX, btnY, btnW, btnH, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(btnRightX, btnY, btnW, btnH, 3, 3)); + g2d.setColor(Color.decode("#666666")); + g2d.draw(new RoundRectangle2D.Double(btnLeftX, btnY, btnW, btnH, 3, 3)); + g2d.draw(new RoundRectangle2D.Double(btnRightX, btnY, btnW, btnH, 3, 3)); + + // Button actuators + g2d.setColor(Color.decode("#A0A0A0")); + g2d.fillOval((int) (btnLeftX + btnW / 2.0 - 3.5), (int) (btnY + btnH / 2.0 - 3.5), 7, 7); + g2d.fillOval((int) (btnRightX + btnW / 2.0 - 3.5), (int) (btnY + btnH / 2.0 - 3.5), 7, 7); - // Buttons - g2d.setColor(Color.decode("#555555")); - g2d.fill(new RoundRectangle2D.Double(boardX + 15, boardY + boardH - 45, 25, 18, 3, 3)); - g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 40, boardY + boardH - 45, 25, 18, 3, 3)); g2d.setColor(Color.WHITE); g2d.setFont(SILK_FONT_SMALL); - StringUtils.drawCenteredText(g2d, "RST", boardX + 27, boardY + boardH - 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); - StringUtils.drawCenteredText(g2d, "FLASH", boardX + boardW - 28, boardY + boardH - 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "RST", btnLeftX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "FLASH", btnRightX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); // Silkscreen g2d.setFont(SILK_FONT); - StringUtils.drawCenteredText(g2d, "NodeMCU", boardX + boardW / 2.0, boardY + 180, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "NodeMCU", x + rowSpacing / 2.0, chipY - 10, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); } g2d.setTransform(oldTx); @@ -187,6 +262,7 @@ public class ESP8266NodeMCU extends AbstractMakerBoard { g2d.setColor(Color.GRAY); g2d.draw(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3)); + // Metal shield g2d.setColor(METAL_SHIELD_COLOR); g2d.fillRect(8, 6, width - 16, 12); diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitCTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitCTest.java new file mode 100644 index 00000000..fa30b881 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitCTest.java @@ -0,0 +1,109 @@ +package org.diylc.components.micro; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class ESP32DevKitCTest { + + @Test + public void testControlPointCountAndNames() { + ESP32DevKitC devKit = new ESP32DevKitC(); + Assert.assertEquals(38, devKit.getControlPointCount()); + + for (int i = 0; i < devKit.getControlPointCount(); i++) { + String name = devKit.getControlPointNodeName(i); + Assert.assertNotNull("Pin " + i + " name should not be null", name); + Assert.assertFalse("Pin " + i + " name should not be empty", name.trim().isEmpty()); + } + + // Left row (0..18, top to bottom) + Assert.assertEquals("3V3", devKit.getControlPointNodeName(0)); + Assert.assertEquals("EN", devKit.getControlPointNodeName(1)); + Assert.assertEquals("VP (GPIO36)", devKit.getControlPointNodeName(2)); + Assert.assertEquals("VN (GPIO39)", devKit.getControlPointNodeName(3)); + Assert.assertEquals("GPIO34", devKit.getControlPointNodeName(4)); + Assert.assertEquals("GPIO35", devKit.getControlPointNodeName(5)); + Assert.assertEquals("GPIO32", devKit.getControlPointNodeName(6)); + Assert.assertEquals("GPIO33", devKit.getControlPointNodeName(7)); + Assert.assertEquals("GPIO25", devKit.getControlPointNodeName(8)); + Assert.assertEquals("GPIO26", devKit.getControlPointNodeName(9)); + Assert.assertEquals("GPIO27", devKit.getControlPointNodeName(10)); + Assert.assertEquals("GPIO14", devKit.getControlPointNodeName(11)); + Assert.assertEquals("GPIO12", devKit.getControlPointNodeName(12)); + Assert.assertEquals("GND1", devKit.getControlPointNodeName(13)); + Assert.assertEquals("GPIO13", devKit.getControlPointNodeName(14)); + Assert.assertEquals("D2 (GPIO9)", devKit.getControlPointNodeName(15)); + Assert.assertEquals("D3 (GPIO10)", devKit.getControlPointNodeName(16)); + Assert.assertEquals("CMD (GPIO11)", devKit.getControlPointNodeName(17)); + Assert.assertEquals("5V", devKit.getControlPointNodeName(18)); + + // Right row (19..37, top to bottom) + Assert.assertEquals("GND2", devKit.getControlPointNodeName(19)); + Assert.assertEquals("GPIO23", devKit.getControlPointNodeName(20)); + Assert.assertEquals("GPIO22", devKit.getControlPointNodeName(21)); + Assert.assertEquals("TX0 (GPIO1)", devKit.getControlPointNodeName(22)); + Assert.assertEquals("RX0 (GPIO3)", devKit.getControlPointNodeName(23)); + Assert.assertEquals("GPIO21", devKit.getControlPointNodeName(24)); + Assert.assertEquals("GND3", devKit.getControlPointNodeName(25)); + Assert.assertEquals("GPIO19", devKit.getControlPointNodeName(26)); + Assert.assertEquals("GPIO18", devKit.getControlPointNodeName(27)); + Assert.assertEquals("GPIO5", devKit.getControlPointNodeName(28)); + Assert.assertEquals("GPIO17", devKit.getControlPointNodeName(29)); + Assert.assertEquals("GPIO16", devKit.getControlPointNodeName(30)); + Assert.assertEquals("GPIO4", devKit.getControlPointNodeName(31)); + Assert.assertEquals("GPIO0", devKit.getControlPointNodeName(32)); + Assert.assertEquals("GPIO2", devKit.getControlPointNodeName(33)); + Assert.assertEquals("GPIO15", devKit.getControlPointNodeName(34)); + Assert.assertEquals("D1 (GPIO8)", devKit.getControlPointNodeName(35)); + Assert.assertEquals("D0 (GPIO7)", devKit.getControlPointNodeName(36)); + Assert.assertEquals("CLK (GPIO6)", devKit.getControlPointNodeName(37)); + } + + @Test + public void testPinGeometryAndSpacing() { + ESP32DevKitC devKit = new ESP32DevKitC(); + + // Left row pitch (20px per pin = 0.10") + for (int i = 0; i < 18; i++) { + Point2D p1 = devKit.getControlPoint(i); + Point2D p2 = devKit.getControlPoint(i + 1); + Assert.assertEquals(20.0, p1.distance(p2), 0.01); + Assert.assertEquals(p1.getX(), p2.getX(), 0.01); + } + + // Right row pitch (20px per pin = 0.10") + for (int i = 19; i < 37; i++) { + Point2D p1 = devKit.getControlPoint(i); + Point2D p2 = devKit.getControlPoint(i + 1); + Assert.assertEquals(20.0, p1.distance(p2), 0.01); + Assert.assertEquals(p1.getX(), p2.getX(), 0.01); + } + + // Row spacing: 1.00" = 200px between Left row and Right row + Point2D pLeftTop = devKit.getControlPoint(0); + Point2D pRightTop = devKit.getControlPoint(19); + Assert.assertEquals(200.0, pLeftTop.distance(pRightTop), 0.01); + Assert.assertEquals(pLeftTop.getY(), pRightTop.getY(), 0.01); + } + + @Test + public void testBodyShapeDimensions() { + ESP32DevKitC devKit = new ESP32DevKitC(); + + Shape body = devKit.getBodyShape(); + Assert.assertNotNull(body); + Rectangle2D bounds = body.getBounds2D(); + + double expectedWidth = new Size(27.9d, SizeUnit.mm).convertToPixels(); + double expectedTotalLength = new Size(54.4d, SizeUnit.mm).convertToPixels(); + + Assert.assertEquals(expectedWidth, bounds.getWidth(), 0.1); + Assert.assertEquals(expectedTotalLength, bounds.getHeight(), 0.1); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java new file mode 100644 index 00000000..085bb7d9 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java @@ -0,0 +1,105 @@ +package org.diylc.components.micro; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class ESP32DevKitTest { + + @Test + public void testControlPointCountAndNames() { + ESP32DevKit devKit = new ESP32DevKit(); + Assert.assertEquals(30, devKit.getControlPointCount()); + + for (int i = 0; i < devKit.getControlPointCount(); i++) { + String name = devKit.getControlPointNodeName(i); + Assert.assertNotNull("Pin " + i + " name should not be null", name); + Assert.assertFalse("Pin " + i + " name should not be empty", name.trim().isEmpty()); + } + + // Left row (0..14) + Assert.assertEquals("EN", devKit.getControlPointNodeName(0)); + Assert.assertEquals("VP (GPIO36)", devKit.getControlPointNodeName(1)); + Assert.assertEquals("VN (GPIO39)", devKit.getControlPointNodeName(2)); + Assert.assertEquals("D34", devKit.getControlPointNodeName(3)); + Assert.assertEquals("D35", devKit.getControlPointNodeName(4)); + Assert.assertEquals("D32", devKit.getControlPointNodeName(5)); + Assert.assertEquals("D33", devKit.getControlPointNodeName(6)); + Assert.assertEquals("D25", devKit.getControlPointNodeName(7)); + Assert.assertEquals("D26", devKit.getControlPointNodeName(8)); + Assert.assertEquals("D27", devKit.getControlPointNodeName(9)); + Assert.assertEquals("D14", devKit.getControlPointNodeName(10)); + Assert.assertEquals("D12", devKit.getControlPointNodeName(11)); + Assert.assertEquals("GND1", devKit.getControlPointNodeName(12)); + Assert.assertEquals("D13", devKit.getControlPointNodeName(13)); + Assert.assertEquals("3V3", devKit.getControlPointNodeName(14)); + + // Right row (15..29) + Assert.assertEquals("VIN", devKit.getControlPointNodeName(15)); + Assert.assertEquals("GND2", devKit.getControlPointNodeName(16)); + Assert.assertEquals("D15", devKit.getControlPointNodeName(17)); + Assert.assertEquals("D2", devKit.getControlPointNodeName(18)); + Assert.assertEquals("D4", devKit.getControlPointNodeName(19)); + Assert.assertEquals("D16 (RX2)", devKit.getControlPointNodeName(20)); + Assert.assertEquals("D17 (TX2)", devKit.getControlPointNodeName(21)); + Assert.assertEquals("D5", devKit.getControlPointNodeName(22)); + Assert.assertEquals("D18", devKit.getControlPointNodeName(23)); + Assert.assertEquals("D19", devKit.getControlPointNodeName(24)); + Assert.assertEquals("D21", devKit.getControlPointNodeName(25)); + Assert.assertEquals("RX0 (GPIO3)", devKit.getControlPointNodeName(26)); + Assert.assertEquals("TX0 (GPIO1)", devKit.getControlPointNodeName(27)); + Assert.assertEquals("D22", devKit.getControlPointNodeName(28)); + Assert.assertEquals("D23", devKit.getControlPointNodeName(29)); + } + + @Test + public void testPinGeometryAndSpacing() { + ESP32DevKit devKit = new ESP32DevKit(); + + // Left row pitch (20px per pin = 0.10") + for (int i = 0; i < 14; i++) { + Point2D p1 = devKit.getControlPoint(i); + Point2D p2 = devKit.getControlPoint(i + 1); + Assert.assertEquals(20.0, p1.distance(p2), 0.01); + Assert.assertEquals(p1.getX(), p2.getX(), 0.01); + } + + // Right row pitch (20px per pin = 0.10") + for (int i = 15; i < 29; i++) { + Point2D p1 = devKit.getControlPoint(i); + Point2D p2 = devKit.getControlPoint(i + 1); + Assert.assertEquals(20.0, p1.distance(p2), 0.01); + Assert.assertEquals(p1.getX(), p2.getX(), 0.01); + } + + // Row spacing: 0.90" = 180px between Left row and Right row + Point2D pLeftTop = devKit.getControlPoint(0); + Point2D pRightTop = devKit.getControlPoint(29); + Assert.assertEquals(180.0, pLeftTop.distance(pRightTop), 0.01); + Assert.assertEquals(pLeftTop.getY(), pRightTop.getY(), 0.01); + } + + @Test + public void testBodyShapeDimensions() { + ESP32DevKit devKit = new ESP32DevKit(); + + Shape body = devKit.getBodyShape(); + Assert.assertNotNull(body); + Rectangle2D bounds = body.getBounds2D(); + + double expectedWidth = new Size(28.2d, SizeUnit.mm).convertToPixels(); + double expectedHeight = new Size(51.8d, SizeUnit.mm).convertToPixels(); + double expectedTopMargin = new Size(6.7d, SizeUnit.mm).convertToPixels(); + + Assert.assertEquals(expectedWidth, bounds.getWidth(), 0.1); + Assert.assertEquals(expectedHeight, bounds.getHeight(), 0.1); + + Point2D p0 = devKit.getControlPoint(0); + Assert.assertEquals(expectedTopMargin, p0.getY() - bounds.getY(), 0.1); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java new file mode 100644 index 00000000..ec29649b --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java @@ -0,0 +1,105 @@ +package org.diylc.components.micro; + +import java.awt.Shape; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class ESP8266NodeMCUTest { + + @Test + public void testControlPointCountAndNames() { + ESP8266NodeMCU mcu = new ESP8266NodeMCU(); + Assert.assertEquals(30, mcu.getControlPointCount()); + + for (int i = 0; i < mcu.getControlPointCount(); i++) { + String name = mcu.getControlPointNodeName(i); + Assert.assertNotNull("Pin " + i + " name should not be null", name); + Assert.assertFalse("Pin " + i + " name should not be empty", name.trim().isEmpty()); + } + + // Left row (0..14) + Assert.assertEquals("A0 (ADC0)", mcu.getControlPointNodeName(0)); + Assert.assertEquals("RSV1", mcu.getControlPointNodeName(1)); + Assert.assertEquals("RSV2", mcu.getControlPointNodeName(2)); + Assert.assertEquals("SD3", mcu.getControlPointNodeName(3)); + Assert.assertEquals("SD2", mcu.getControlPointNodeName(4)); + Assert.assertEquals("SD1", mcu.getControlPointNodeName(5)); + Assert.assertEquals("CMD", mcu.getControlPointNodeName(6)); + Assert.assertEquals("SD0", mcu.getControlPointNodeName(7)); + Assert.assertEquals("CLK", mcu.getControlPointNodeName(8)); + Assert.assertEquals("GND1", mcu.getControlPointNodeName(9)); + Assert.assertEquals("3V3_1", mcu.getControlPointNodeName(10)); + Assert.assertEquals("EN", mcu.getControlPointNodeName(11)); + Assert.assertEquals("RST", mcu.getControlPointNodeName(12)); + Assert.assertEquals("GND2", mcu.getControlPointNodeName(13)); + Assert.assertEquals("VIN", mcu.getControlPointNodeName(14)); + + // Right row (15..29) + Assert.assertEquals("D0 (GPIO16)", mcu.getControlPointNodeName(15)); + Assert.assertEquals("D1 (GPIO5)", mcu.getControlPointNodeName(16)); + Assert.assertEquals("D2 (GPIO4)", mcu.getControlPointNodeName(17)); + Assert.assertEquals("D3 (GPIO0)", mcu.getControlPointNodeName(18)); + Assert.assertEquals("D4 (GPIO2)", mcu.getControlPointNodeName(19)); + Assert.assertEquals("3V3_2", mcu.getControlPointNodeName(20)); + Assert.assertEquals("GND3", mcu.getControlPointNodeName(21)); + Assert.assertEquals("D5 (GPIO14)", mcu.getControlPointNodeName(22)); + Assert.assertEquals("D6 (GPIO12)", mcu.getControlPointNodeName(23)); + Assert.assertEquals("D7 (GPIO13)", mcu.getControlPointNodeName(24)); + Assert.assertEquals("D8 (GPIO15)", mcu.getControlPointNodeName(25)); + Assert.assertEquals("RX (GPIO3)", mcu.getControlPointNodeName(26)); + Assert.assertEquals("TX (GPIO1)", mcu.getControlPointNodeName(27)); + Assert.assertEquals("GND4", mcu.getControlPointNodeName(28)); + Assert.assertEquals("3V3_3", mcu.getControlPointNodeName(29)); + } + + @Test + public void testPinGeometryAndSpacing() { + ESP8266NodeMCU mcu = new ESP8266NodeMCU(); + + // Left row pitch (20px per pin = 0.10") + for (int i = 0; i < 14; i++) { + Point2D p1 = mcu.getControlPoint(i); + Point2D p2 = mcu.getControlPoint(i + 1); + Assert.assertEquals(20.0, p1.distance(p2), 0.01); + Assert.assertEquals(p1.getX(), p2.getX(), 0.01); + } + + // Right row pitch (20px per pin = 0.10") + for (int i = 15; i < 29; i++) { + Point2D p1 = mcu.getControlPoint(i); + Point2D p2 = mcu.getControlPoint(i + 1); + Assert.assertEquals(20.0, p1.distance(p2), 0.01); + Assert.assertEquals(p1.getX(), p2.getX(), 0.01); + } + + // Row spacing: 0.90" = 180px between Left row and Right row + Point2D pLeftTop = mcu.getControlPoint(0); + Point2D pRightTop = mcu.getControlPoint(15); + Assert.assertEquals(180.0, pLeftTop.distance(pRightTop), 0.01); + Assert.assertEquals(pLeftTop.getY(), pRightTop.getY(), 0.01); + } + + @Test + public void testBodyShapeDimensions() { + ESP8266NodeMCU mcu = new ESP8266NodeMCU(); + + Shape body = mcu.getBodyShape(); + Assert.assertNotNull(body); + Rectangle2D bounds = body.getBounds2D(); + + double expectedWidth = new Size(25.7d, SizeUnit.mm).convertToPixels(); + double expectedHeight = new Size(48.0d, SizeUnit.mm).convertToPixels(); + double expectedTopMargin = new Size(6.22d, SizeUnit.mm).convertToPixels(); + + Assert.assertEquals(expectedWidth, bounds.getWidth(), 0.1); + Assert.assertEquals(expectedHeight, bounds.getHeight(), 0.1); + + Point2D p0 = mcu.getControlPoint(0); + Assert.assertEquals(expectedTopMargin, p0.getY() - bounds.getY(), 0.1); + } +}