This commit is contained in:
Bane Stojkovic
2026-08-26 13:52:21 +02:00
parent 808528b279
commit 4f0e75f768
2 changed files with 788 additions and 0 deletions
@@ -0,0 +1,435 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.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.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 = "Teensy", category = "Controllers",
author = "Branislav Stojkovic", description = "PJRC Teensy 4.0 / 4.1 Development Board (ARM Cortex-M7, 600 MHz)",
instanceNamePrefix = "MCU", zOrder = IDIYComponent.BOARD,
bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME,
enableCache = true)
public class Teensy extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public enum TeensyVersion {
Teensy_4_0("Teensy 4.0"),
Teensy_4_1("Teensy 4.1");
private final String label;
TeensyVersion(String label) {
this.label = label;
}
@Override
public String toString() {
return label;
}
}
// Board color matches standard green PCB (like Pi Zero)
public static Color TEENSY_GREEN = Color.decode("#1B5E20");
public static Color BUTTON_BODY_COLOR = Color.decode("#383838");
public static Color BUTTON_BORDER_COLOR = Color.decode("#666666");
public static Color BUTTON_ACTUATOR_COLOR = Color.decode("#A0A0A0");
public static Color SILK_COLOR = Color.WHITE;
public static Color PAD_COLOR = GOLD_COLOR;
public static Size HOLE_SIZE = new Size(0.7d, SizeUnit.mm);
// ===== Teensy 4.0 dimensions (PJRC dimensions_teensy40.png) =====
// 35.56 mm x 17.78 mm (1.4" x 0.7")
public static Size BOARD_WIDTH_40 = new Size(17.78d, SizeUnit.mm);
public static Size BOARD_LENGTH_40 = new Size(35.56d, SizeUnit.mm);
// ===== Teensy 4.1 dimensions (PJRC dimensions_teensy41.png) =====
// 60.96 mm x 17.78 mm (2.4" x 0.7")
public static Size BOARD_WIDTH_41 = new Size(17.78d, SizeUnit.mm);
public static Size BOARD_LENGTH_41 = new Size(60.96d, SizeUnit.mm);
// Common pitch and spacing
// Pin 1 offset from top and left edges is 0.05" (1.27 mm)
// Row spacing is 0.60" (15.24 mm)
// Pin spacing along row is 0.10" (2.54 mm)
public static Size PIN1_OFFSET_X = new Size(1.27d, SizeUnit.mm);
public static Size PIN1_OFFSET_Y = new Size(1.27d, SizeUnit.mm);
public static Size ROW_SPACING = new Size(15.24d, SizeUnit.mm);
// ===== Teensy 4.0 Pinout (34 pins total) =====
// Left row (0..13, top to bottom): GND, 0..12
// Right row (14..27, top to bottom): VIN, GND, 3.3V, 23..13
// End cluster (28..32, bottom edge left to right): VBAT, 3.3V, GND, Program, On/Off
// VUSB (33, near VIN): VUSB
public static final String[] PIN_NAMES_40 = new String[] {
// Left row (0..13)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)",
// Right row (14..27)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/CRX1/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "On/Off",
// VUSB (33)
"VUSB"
};
// ===== Teensy 4.1 Pinout (65 pins total) =====
// Left row (0..23, top to bottom): GND, 0..12, 3.3V, 24..32
// Right row (24..47, top to bottom): VIN, GND, 3.3V, 23..13, GND, 41..33
// Middle cluster (48..52, left to right): VBAT, 3.3V, GND, Program, On/Off
// Ethernet header (53..58, 2x3): ETH_Rx+, ETH_LED, ETH_Tx-, ETH_Rx-, ETH_GND, ETH_Tx+
// USB Host header (59..63, 1x5): USB_5V, USB_D-, USB_D+, USB_GND1, USB_GND2
// VUSB (64, near VIN): VUSB
public static final String[] PIN_NAMES_41 = new String[] {
// Left row (0..23)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"26 (A12/MOSI1)", "27 (A13/SCK1)", "28 (RX7)", "29 (TX7)", "30 (CRX3)", "31 (CTX3)",
"32 (OUT1B)",
// Right row (24..47)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL)", "18 (A4/SDA)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND", "41 (A17)",
"40 (A16)", "39 (A15/MISO1)", "38 (A14/CS1)", "37 (CS)", "36 (CS)", "35 (TX8)",
"34 (RX8)", "33 (MCLK2)",
// Middle cluster (48..52, left to right near push button)
"VBAT", "3.3V (Mid)", "GND (Mid)", "Program", "On/Off",
// Ethernet header (53..58, 2x3)
"ETH_Rx+", "ETH_LED", "ETH_Tx-", "ETH_Rx-", "ETH_GND", "ETH_Tx+",
// USB Host header (59..63, 1x5)
"USB_5V", "USB_D-", "USB_D+", "USB_GND1", "USB_GND2",
// VUSB (64)
"VUSB"
};
private TeensyVersion version = TeensyVersion.Teensy_4_0;
public Teensy() {
super();
this.bodyColor = TEENSY_GREEN;
updateControlPoints();
}
@EditableProperty(name = "Version")
public TeensyVersion getVersion() {
return version;
}
public void setVersion(TeensyVersion version) {
this.version = version;
updateControlPoints();
invalidateCache();
}
private String[] getPinNames() {
return (version == TeensyVersion.Teensy_4_0) ? PIN_NAMES_40 : PIN_NAMES_41;
}
@Override
public String getControlPointNodeName(int index) {
String[] names = getPinNames();
if (index >= 0 && index < names.length) {
return names[index];
}
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
double spacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
if (version == TeensyVersion.Teensy_4_0) {
double[][] relativeOffsets = new double[PIN_NAMES_40.length][2];
// Left row (0..13): X=0, Y=0..13*spacing
for (int i = 0; i < 14; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
}
// Right row (14..27): X=rowSpacing, Y=0..13*spacing
for (int i = 0; i < 14; i++) {
relativeOffsets[14 + i][0] = rowSpacing;
relativeOffsets[14 + i][1] = i * spacing;
}
// End row (28..32): 5 pins between pin 12 and pin 13 at Y=13*spacing
for (int k = 1; k <= 5; k++) {
relativeOffsets[28 + k - 1][0] = k * spacing;
relativeOffsets[28 + k - 1][1] = 13 * spacing;
}
// VUSB (33): near VIN at X=rowSpacing - spacing, Y=spacing
relativeOffsets[33][0] = rowSpacing - spacing;
relativeOffsets[33][1] = spacing;
rotatePoints(firstPoint, relativeOffsets);
} else {
double[][] relativeOffsets = new double[PIN_NAMES_41.length][2];
// Left row (0..23): X=0, Y=0..23*spacing
for (int i = 0; i < 24; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
}
// Right row (24..47): X=rowSpacing, Y=0..23*spacing
for (int i = 0; i < 24; i++) {
relativeOffsets[24 + i][0] = rowSpacing;
relativeOffsets[24 + i][1] = i * spacing;
}
// Middle cluster (48..52): 5 pins near push button at Y=17*spacing
for (int k = 1; k <= 5; k++) {
relativeOffsets[48 + k - 1][0] = k * spacing;
relativeOffsets[48 + k - 1][1] = 17 * spacing;
}
// Ethernet Header (53..58): 2x3 header (2.0mm pitch)
double ethPitch = new Size(2.0d, SizeUnit.mm).convertToPixels();
double ethCenterX = rowSpacing / 2.0;
double ethBaseY = 5 * spacing;
// Col 1 (left): Rx+, LED, Tx-
relativeOffsets[53] = new double[] {ethCenterX - ethPitch / 2.0, ethBaseY - ethPitch};
relativeOffsets[54] = new double[] {ethCenterX - ethPitch / 2.0, ethBaseY};
relativeOffsets[55] = new double[] {ethCenterX - ethPitch / 2.0, ethBaseY + ethPitch};
// Col 2 (right): Rx-, GND, Tx+
relativeOffsets[56] = new double[] {ethCenterX + ethPitch / 2.0, ethBaseY - ethPitch};
relativeOffsets[57] = new double[] {ethCenterX + ethPitch / 2.0, ethBaseY};
relativeOffsets[58] = new double[] {ethCenterX + ethPitch / 2.0, ethBaseY + ethPitch};
// USB Host Header (59..63): 1x5 header (2.54mm pitch)
for (int i = 0; i < 5; i++) {
relativeOffsets[59 + i][0] = spacing;
relativeOffsets[59 + i][1] = (4 + i) * spacing;
}
// VUSB (64): near VIN at X=rowSpacing - spacing, Y=spacing
relativeOffsets[64][0] = rowSpacing - spacing;
relativeOffsets[64][1] = spacing;
rotatePoints(firstPoint, relativeOffsets);
}
}
@Override
public Shape getBodyShape() {
Point2D p0 = controlPoints[0];
double x = p0.getX();
double y = p0.getY();
double boardW = (version == TeensyVersion.Teensy_4_0) ? BOARD_WIDTH_40.convertToPixels() : BOARD_WIDTH_41.convertToPixels();
double boardH = (version == TeensyVersion.Teensy_4_0) ? BOARD_LENGTH_40.convertToPixels() : BOARD_LENGTH_41.convertToPixels();
double pin1OffsetX = PIN1_OFFSET_X.convertToPixels();
double pin1OffsetY = PIN1_OFFSET_Y.convertToPixels();
double boardX = x - pin1OffsetX;
double boardY = y - pin1OffsetY;
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);
}
Shape boardShape = getBodyShape();
Composite oldComposite = applyAlpha(g2d, componentState);
drawingObserver.startTracking();
// Fill board body (green PCB)
g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor);
g2d.fill(boardShape);
// Board outline
g2d.setColor(getFinalBorderColor(componentState, outlineMode));
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f));
g2d.draw(boardShape);
if (!outlineMode) {
Rectangle2D boardBounds = boardShape.getBounds2D();
double boardX = boardBounds.getX();
double boardY = boardBounds.getY();
double boardW = boardBounds.getWidth();
double boardH = boardBounds.getHeight();
double centerX = boardX + boardW / 2.0;
// Micro-USB connector on top edge (overhangs edge slightly)
double usbW = new Size(7.5d, SizeUnit.mm).convertToPixels();
double usbH = new Size(5.6d, SizeUnit.mm).convertToPixels();
double usbX = centerX - usbW / 2.0;
double usbY = boardY - new Size(1.0d, SizeUnit.mm).convertToPixels();
drawMetalConnector(g2d, usbX, usbY, usbW, usbH, "USB");
// Main MCU chip (NXP i.MX RT1062 BGA)
double chipW = new Size(7.5d, SizeUnit.mm).convertToPixels();
double chipH = new Size(7.5d, SizeUnit.mm).convertToPixels();
double chipX = centerX - chipW / 2.0;
double chipY = (version == TeensyVersion.Teensy_4_0)
? boardY + new Size(9.5d, SizeUnit.mm).convertToPixels()
: boardY + new Size(19.0d, SizeUnit.mm).convertToPixels();
drawChip(g2d, chipX, chipY, chipW, chipH, "");
// Pushbutton (Program button)
double btnW = new Size(3.5d, SizeUnit.mm).convertToPixels();
double btnH = new Size(3.0d, SizeUnit.mm).convertToPixels();
double btnX = centerX - btnW / 2.0;
double btnY = (version == TeensyVersion.Teensy_4_0)
? boardY + new Size(24.5d, SizeUnit.mm).convertToPixels()
: boardY + new Size(35.5d, SizeUnit.mm).convertToPixels();
g2d.setColor(BUTTON_BODY_COLOR);
g2d.fill(new RoundRectangle2D.Double(btnX, btnY, btnW, btnH, 2, 2));
g2d.setColor(BUTTON_BORDER_COLOR);
g2d.draw(new RoundRectangle2D.Double(btnX, btnY, btnW, btnH, 2, 2));
g2d.setColor(BUTTON_ACTUATOR_COLOR);
g2d.fillOval((int) (btnX + btnW / 2.0 - 2.5), (int) (btnY + btnH / 2.0 - 2.5), 5, 5);
// Teensy 4.1 extras: Ethernet PHY + SD card slot
if (version == TeensyVersion.Teensy_4_1) {
// MicroSD card slot at bottom edge
double sdW = new Size(12.0d, SizeUnit.mm).convertToPixels();
double sdH = new Size(14.0d, SizeUnit.mm).convertToPixels();
double sdX = centerX - sdW / 2.0;
double sdY = boardY + boardH - sdH + new Size(1.0d, SizeUnit.mm).convertToPixels();
drawMetalConnector(g2d, sdX, sdY, sdW, sdH, "SD");
}
// Silkscreen white outline boxes around outer pins
g2d.setColor(Color.WHITE);
g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1));
int outerRowCount = (version == TeensyVersion.Teensy_4_0) ? 14 : 24;
double padBoxMargin = new Size(0.9d, SizeUnit.mm).convertToPixels();
// Left row silkscreen box
Point2D pLeftFirst = controlPoints[0];
Point2D pLeftLast = controlPoints[outerRowCount - 1];
g2d.draw(new Rectangle2D.Double(
pLeftFirst.getX() - padBoxMargin, pLeftFirst.getY() - padBoxMargin,
padBoxMargin * 2, (pLeftLast.getY() - pLeftFirst.getY()) + padBoxMargin * 2));
// Right row silkscreen box
Point2D pRightFirst = controlPoints[outerRowCount];
Point2D pRightLast = controlPoints[outerRowCount * 2 - 1];
g2d.draw(new Rectangle2D.Double(
pRightFirst.getX() - padBoxMargin, pRightFirst.getY() - padBoxMargin,
padBoxMargin * 2, (pRightLast.getY() - pRightFirst.getY()) + padBoxMargin * 2));
// Silkscreen "TEENSY" label
g2d.setColor(SILK_COLOR);
g2d.setFont(SILK_FONT);
String silkText = (version == TeensyVersion.Teensy_4_0) ? "Teensy 4.0" : "Teensy 4.1";
double textY = (version == TeensyVersion.Teensy_4_0)
? chipY + chipH + new Size(2.5d, SizeUnit.mm).convertToPixels()
: chipY + chipH + new Size(3.0d, SizeUnit.mm).convertToPixels();
StringUtils.drawCenteredText(g2d, silkText, centerX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
drawingObserver.stopTracking();
g2d.setTransform(oldTx);
// Render gold solder pads with drill holes (square for Pin 1 / GND)
drawSolderPads(g2d, outlineMode, drawingObserver);
g2d.setComposite(oldComposite);
}
/**
* Helper to draw GPIO solder pads (copper/gold pads with drill holes, square for Pin 1).
*/
protected void drawSolderPads(Graphics2D g2d, boolean outlineMode, IDrawingObserver drawingObserver) {
drawPcbSolderPads(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver);
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(TEENSY_GREEN);
g2d.fill(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3));
g2d.setColor(TEENSY_GREEN.darker());
g2d.draw(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3));
// GPIO Solder Pads in icon
g2d.setColor(PAD_COLOR);
for (int y = 5; y <= height - 6; y += 3) {
g2d.fillRect(6, y, 2, 2);
g2d.fillRect(width - 8, y, 2, 2);
}
// MCU chip
g2d.setColor(IC_BODY_COLOR);
g2d.fillRect(10, 8, 12, 8);
// USB connector at top
g2d.setColor(USB_METAL_COLOR);
g2d.fillRect(12, 2, 8, 3);
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("SansSerif", Font.BOLD, 5));
StringUtils.drawCenteredText(g2d, "TEENSY", width / 2, height / 2 + 8,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
}
}
@@ -0,0 +1,353 @@
package org.diylc.components.micro;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicInteger;
import org.diylc.common.Orientation;
import org.diylc.core.ComponentState;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
import org.junit.Assert;
import org.junit.Test;
public class TeensyTest {
@Test
public void testControlPointCountAndNames40() {
Teensy teensy = new Teensy();
Assert.assertEquals(Teensy.TeensyVersion.Teensy_4_0, teensy.getVersion());
// 14 left + 14 right + 5 end + 1 VUSB = 34 pins
Assert.assertEquals(34, teensy.getControlPointCount());
for (int i = 0; i < teensy.getControlPointCount(); i++) {
String name = teensy.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..13, top to bottom)
Assert.assertEquals("GND", teensy.getControlPointNodeName(0));
Assert.assertEquals("0 (RX1/CS1/CRX2)", teensy.getControlPointNodeName(1));
Assert.assertEquals("1 (TX1/MISO1/CTX2)", teensy.getControlPointNodeName(2));
Assert.assertEquals("12 (MISO/MQSL)", teensy.getControlPointNodeName(13));
// Right row (14..27, top to bottom)
Assert.assertEquals("VIN (3.6-5.5V)", teensy.getControlPointNodeName(14));
Assert.assertEquals("GND", teensy.getControlPointNodeName(15));
Assert.assertEquals("3.3V (250mA)", teensy.getControlPointNodeName(16));
Assert.assertEquals("23 (A9/CRX1)", teensy.getControlPointNodeName(17));
Assert.assertEquals("13 (SCK/CRX1/LED)", teensy.getControlPointNodeName(27));
// End cluster (28..32)
Assert.assertEquals("VBAT", teensy.getControlPointNodeName(28));
Assert.assertEquals("3.3V (End)", teensy.getControlPointNodeName(29));
Assert.assertEquals("GND (End)", teensy.getControlPointNodeName(30));
Assert.assertEquals("Program", teensy.getControlPointNodeName(31));
Assert.assertEquals("On/Off", teensy.getControlPointNodeName(32));
// VUSB (33)
Assert.assertEquals("VUSB", teensy.getControlPointNodeName(33));
}
@Test
public void testControlPointCountAndNames41() {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
Assert.assertEquals(Teensy.TeensyVersion.Teensy_4_1, teensy.getVersion());
// 24 left + 24 right + 5 mid + 6 eth + 5 usb + 1 VUSB = 65 pins
Assert.assertEquals(65, teensy.getControlPointCount());
for (int i = 0; i < teensy.getControlPointCount(); i++) {
String name = teensy.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..23, top to bottom)
Assert.assertEquals("GND", teensy.getControlPointNodeName(0));
Assert.assertEquals("0 (RX1/CS1/CRX2)", teensy.getControlPointNodeName(1));
Assert.assertEquals("3.3V", teensy.getControlPointNodeName(14));
Assert.assertEquals("32 (OUT1B)", teensy.getControlPointNodeName(23));
// Right row (24..47, top to bottom)
Assert.assertEquals("VIN (3.6-5.5V)", teensy.getControlPointNodeName(24));
Assert.assertEquals("GND", teensy.getControlPointNodeName(25));
Assert.assertEquals("13 (SCK/LED)", teensy.getControlPointNodeName(37));
Assert.assertEquals("33 (MCLK2)", teensy.getControlPointNodeName(47));
// Middle cluster (48..52)
Assert.assertEquals("VBAT", teensy.getControlPointNodeName(48));
Assert.assertEquals("3.3V (Mid)", teensy.getControlPointNodeName(49));
Assert.assertEquals("GND (Mid)", teensy.getControlPointNodeName(50));
Assert.assertEquals("Program", teensy.getControlPointNodeName(51));
Assert.assertEquals("On/Off", teensy.getControlPointNodeName(52));
// Ethernet Header (53..58)
Assert.assertEquals("ETH_Rx+", teensy.getControlPointNodeName(53));
Assert.assertEquals("ETH_Tx+", teensy.getControlPointNodeName(58));
// USB Host Header (59..63)
Assert.assertEquals("USB_5V", teensy.getControlPointNodeName(59));
Assert.assertEquals("USB_GND2", teensy.getControlPointNodeName(63));
// VUSB (64)
Assert.assertEquals("VUSB", teensy.getControlPointNodeName(64));
}
@Test
public void testPinGeometryAndSpacing40() {
Teensy teensy = new Teensy();
double spacing = new Size(0.1d, SizeUnit.in).convertToPixels();
// Left row pitch (0.1" per pin, 14 pins)
for (int i = 0; i < 13; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("Left pin " + i + " Y spacing", spacing, p2.getY() - p1.getY(), 0.01);
Assert.assertEquals("Left pin " + i + " X alignment", p1.getX(), p2.getX(), 0.01);
}
// Right row pitch (14 pins)
for (int i = 14; i < 27; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("Right pin " + i + " Y spacing", spacing, p2.getY() - p1.getY(), 0.01);
Assert.assertEquals("Right pin " + i + " X alignment", p1.getX(), p2.getX(), 0.01);
}
// Row spacing: 0.60" (15.24 mm)
double rowSpacing = new Size(15.24d, SizeUnit.mm).convertToPixels();
Point2D pLeft = teensy.getControlPoint(0);
Point2D pRight = teensy.getControlPoint(14);
Assert.assertEquals("Row spacing", rowSpacing, pRight.getX() - pLeft.getX(), 0.01);
Assert.assertEquals("Row Y alignment", pLeft.getY(), pRight.getY(), 0.01);
// End row horizontal spacing: 0.1" pitch at Y = 13 * spacing
for (int i = 28; i < 32; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("End pin " + i + " X spacing", spacing, p2.getX() - p1.getX(), 0.01);
Assert.assertEquals("End pin " + i + " Y alignment", p1.getY(), p2.getY(), 0.01);
}
}
@Test
public void testPinGeometryAndSpacing41() {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
double spacing = new Size(0.1d, SizeUnit.in).convertToPixels();
// Left row pitch (0.1" per pin, 24 pins)
for (int i = 0; i < 23; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("Left pin " + i + " Y spacing", spacing, p2.getY() - p1.getY(), 0.01);
Assert.assertEquals("Left pin " + i + " X alignment", p1.getX(), p2.getX(), 0.01);
}
// Right row pitch (24 pins)
for (int i = 24; i < 47; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("Right pin " + i + " Y spacing", spacing, p2.getY() - p1.getY(), 0.01);
Assert.assertEquals("Right pin " + i + " X alignment", p1.getX(), p2.getX(), 0.01);
}
// Row spacing: 0.60" (15.24 mm)
double rowSpacing = new Size(15.24d, SizeUnit.mm).convertToPixels();
Point2D pLeft = teensy.getControlPoint(0);
Point2D pRight = teensy.getControlPoint(24);
Assert.assertEquals("Row spacing", rowSpacing, pRight.getX() - pLeft.getX(), 0.01);
Assert.assertEquals("Row Y alignment", pLeft.getY(), pRight.getY(), 0.01);
// Middle cluster: horizontal at 0.1" pitch
for (int i = 48; i < 52; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("Middle pin " + i + " X spacing", spacing, p2.getX() - p1.getX(), 0.01);
Assert.assertEquals("Middle pin " + i + " Y alignment", p1.getY(), p2.getY(), 0.01);
}
// USB Host Header: vertical at 0.1" pitch
for (int i = 59; i < 63; i++) {
Point2D p1 = teensy.getControlPoint(i);
Point2D p2 = teensy.getControlPoint(i + 1);
Assert.assertEquals("USB pin " + i + " Y spacing", spacing, p2.getY() - p1.getY(), 0.01);
Assert.assertEquals("USB pin " + i + " X alignment", p1.getX(), p2.getX(), 0.01);
}
}
@Test
public void testBodyShapeDimensions40() {
Teensy teensy = new Teensy();
Shape body = teensy.getBodyShape();
Assert.assertNotNull(body);
Rectangle2D bounds = body.getBounds2D();
double expectedWidth = new Size(17.78d, SizeUnit.mm).convertToPixels();
double expectedHeight = new Size(35.56d, SizeUnit.mm).convertToPixels();
Assert.assertEquals(expectedWidth, bounds.getWidth(), 0.1);
Assert.assertEquals(expectedHeight, bounds.getHeight(), 0.1);
// Pin 1 offset should be 1.27 mm (0.05") from left and top edges
Point2D p0 = teensy.getControlPoint(0);
double expectedOffsetX = new Size(1.27d, SizeUnit.mm).convertToPixels();
double expectedOffsetY = new Size(1.27d, SizeUnit.mm).convertToPixels();
Assert.assertEquals("Pin 1 X offset from left edge", expectedOffsetX, p0.getX() - bounds.getX(), 0.1);
Assert.assertEquals("Pin 1 Y offset from top edge", expectedOffsetY, p0.getY() - bounds.getY(), 0.1);
}
@Test
public void testBodyShapeDimensions41() {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
Shape body = teensy.getBodyShape();
Assert.assertNotNull(body);
Rectangle2D bounds = body.getBounds2D();
double expectedWidth = new Size(17.78d, SizeUnit.mm).convertToPixels();
double expectedHeight = new Size(60.96d, SizeUnit.mm).convertToPixels();
Assert.assertEquals(expectedWidth, bounds.getWidth(), 0.1);
Assert.assertEquals(expectedHeight, bounds.getHeight(), 0.1);
// Pin 1 offset should be 1.27 mm (0.05") from left and top edges
Point2D p0 = teensy.getControlPoint(0);
double expectedOffsetX = new Size(1.27d, SizeUnit.mm).convertToPixels();
double expectedOffsetY = new Size(1.27d, SizeUnit.mm).convertToPixels();
Assert.assertEquals("Pin 1 X offset from left edge", expectedOffsetX, p0.getX() - bounds.getX(), 0.1);
Assert.assertEquals("Pin 1 Y offset from top edge", expectedOffsetY, p0.getY() - bounds.getY(), 0.1);
}
@Test
public void testAllPinsWithinBoardBounds() {
// Teensy 4.0: all 34 pins must be within board bounds
Teensy teensy40 = new Teensy();
Shape body40 = teensy40.getBodyShape();
Rectangle2D bounds40 = body40.getBounds2D();
for (int i = 0; i < teensy40.getControlPointCount(); i++) {
Point2D p = teensy40.getControlPoint(i);
Assert.assertTrue("T4.0 Pin " + i + " (" + teensy40.getControlPointNodeName(i) +
") X=" + p.getX() + " should be within board X bounds [" + bounds40.getMinX() + ", " + bounds40.getMaxX() + "]",
p.getX() >= bounds40.getMinX() - 0.1 && p.getX() <= bounds40.getMaxX() + 0.1);
Assert.assertTrue("T4.0 Pin " + i + " (" + teensy40.getControlPointNodeName(i) +
") Y=" + p.getY() + " should be within board Y bounds [" + bounds40.getMinY() + ", " + bounds40.getMaxY() + "]",
p.getY() >= bounds40.getMinY() - 0.1 && p.getY() <= bounds40.getMaxY() + 0.1);
}
// Teensy 4.1: all 65 pins must be within board bounds
Teensy teensy41 = new Teensy();
teensy41.setVersion(Teensy.TeensyVersion.Teensy_4_1);
Shape body41 = teensy41.getBodyShape();
Rectangle2D bounds41 = body41.getBounds2D();
for (int i = 0; i < teensy41.getControlPointCount(); i++) {
Point2D p = teensy41.getControlPoint(i);
Assert.assertTrue("T4.1 Pin " + i + " (" + teensy41.getControlPointNodeName(i) +
") X=" + p.getX() + " should be within board X bounds [" + bounds41.getMinX() + ", " + bounds41.getMaxX() + "]",
p.getX() >= bounds41.getMinX() - 0.1 && p.getX() <= bounds41.getMaxX() + 0.1);
Assert.assertTrue("T4.1 Pin " + i + " (" + teensy41.getControlPointNodeName(i) +
") Y=" + p.getY() + " should be within board Y bounds [" + bounds41.getMinY() + ", " + bounds41.getMaxY() + "]",
p.getY() >= bounds41.getMinY() - 0.1 && p.getY() <= bounds41.getMaxY() + 0.1);
}
}
@Test
public void testVersionSwitching() {
Teensy teensy = new Teensy();
Assert.assertEquals(34, teensy.getControlPointCount());
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
Assert.assertEquals(65, teensy.getControlPointCount());
Assert.assertEquals("VUSB", teensy.getControlPointNodeName(64));
Assert.assertEquals("GND", teensy.getControlPointNodeName(0));
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_0);
Assert.assertEquals(34, teensy.getControlPointCount());
Assert.assertEquals("VUSB", teensy.getControlPointNodeName(33));
Assert.assertEquals("GND", teensy.getControlPointNodeName(0));
}
@Test
public void testDrawingAndOutline() {
Teensy teensy = new Teensy();
teensy.setControlPoint(new Point2D.Double(200, 200), 0);
BufferedImage img = new BufferedImage(600, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.setClip(new Rectangle(0, 0, 600, 600));
Project project = new Project();
final AtomicInteger trackingStartCount = new AtomicInteger(0);
final AtomicInteger trackingStopCount = new AtomicInteger(0);
IDrawingObserver observer = new IDrawingObserver() {
@Override public void startTracking() {
trackingStartCount.incrementAndGet();
}
@Override public void stopTracking() {
trackingStopCount.incrementAndGet();
}
@Override public void startTrackingContinuityArea(boolean positive) {}
@Override public void stopTrackingContinuityArea() {}
@Override public boolean isTrackingContinuityArea() { return false; }
@Override public void setContinuityMarker(String marker) {}
};
// Test normal draw
teensy.draw(g2d, ComponentState.NORMAL, false, project, observer);
Assert.assertEquals(1, trackingStartCount.get());
Assert.assertEquals(1, trackingStopCount.get());
// Test selected state and outline mode
teensy.draw(g2d, ComponentState.SELECTED, false, project, observer);
teensy.draw(g2d, ComponentState.NORMAL, true, project, observer);
// Test icon drawing
teensy.drawIcon(g2d, 32, 32);
// Test Teensy 4.1 draw
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
teensy.draw(g2d, ComponentState.NORMAL, false, project, observer);
g2d.dispose();
}
@Test
public void testRotation() {
Teensy teensy = new Teensy();
teensy.setControlPoint(new Point2D.Double(200, 200), 0);
Point2D p0Initial = teensy.getControlPoint(0);
Point2D p14Initial = teensy.getControlPoint(14); // First right-row pin
teensy.setOrientation(Orientation._90);
Point2D p0Rot = teensy.getControlPoint(0);
Point2D p14Rot = teensy.getControlPoint(14);
// p0 should stay in place
Assert.assertEquals(p0Initial.getX(), p0Rot.getX(), 0.01);
Assert.assertEquals(p0Initial.getY(), p0Rot.getY(), 0.01);
// After 90 deg clockwise rotation, right row pin should move from (x + rowSpacing, y) to (x, y + rowSpacing)
double rowSpacing = new Size(15.24d, SizeUnit.mm).convertToPixels();
Assert.assertEquals(p0Rot.getX(), p14Rot.getX(), 0.01);
Assert.assertEquals(p0Rot.getY() + rowSpacing, p14Rot.getY(), 0.01);
}
@Test
public void testBoardColor() {
Teensy teensy = new Teensy();
// Board should be green (like Pi Zero)
Assert.assertEquals(Teensy.TEENSY_GREEN, teensy.getBodyColor());
}
}