netlist and node name improvements

This commit is contained in:
Bane Stojkovic
2026-10-08 14:23:36 +02:00
parent 2a08fbe569
commit 02615f5150
16 changed files with 555 additions and 46 deletions
@@ -411,6 +411,23 @@ public class NetlistBuilder {
if (((IContinuity) c).arePointsConnected(i, j))
connections.add(new Connection(c.getControlPoint(i), c.getControlPoint(j), z));
}
// Pins of one component that name the same rail are the same node: a board's eight ground
// pins land on one plane, and grounding one of them must not leave the rest floating.
// Switches are excluded because their connectivity depends on the position, which a name
// cannot express -- a switch states it through ISwitch.arePointsConnected instead.
if (!(c instanceof ISwitch)) {
for (int i = 0; i < c.getControlPointCount() - 1; i++) {
String rail = Node.railName(c.getControlPointNodeName(i));
if (rail == null) {
continue;
}
for (int j = i + 1; j < c.getControlPointCount(); j++) {
if (rail.equals(Node.railName(c.getControlPointNodeName(j)))) {
connections.add(new Connection(c.getControlPoint(i), c.getControlPoint(j), z));
}
}
}
}
// handle switches
if (ISwitch.class.isAssignableFrom(type.getInstanceClass())
&& switchPositions.containsKey(c)) {
@@ -22,7 +22,10 @@
package org.diylc.netlist;
import java.awt.geom.Point2D;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import org.diylc.core.ICommonNode;
import org.diylc.core.IDIYComponent;
@@ -34,6 +37,10 @@ import org.diylc.core.IDIYComponent;
*/
public class Node implements Comparable<Node> {
/** Pin names that mark an absent connection, so repeats of them are not one net. */
private static final Set<String> NON_NET_NAMES =
new HashSet<String>(Arrays.asList("NC", "RSV"));
private IDIYComponent<?> component;
private int pointIndex;
@@ -60,8 +67,9 @@ public class Node implements Comparable<Node> {
* <code>MCU1.D11</code> rather than <code>MCU1.D11 (~, MOSI)</code>. The bare name identifies the
* pin; everything in parentheses documents its alternate functions and only adds noise here.
*
* <p>A trailing disambiguator such as the <code>_1</code> in <code>GND_1</code> is deliberately
* kept, because those mark genuinely different pins that a netlist has to tell apart.
* <p>A trailing disambiguator such as the <code>_1</code> in <code>GND_1</code> is kept, so the
* netlist can still say which pad a connection reached. Whether two such pins are the same
* net is a separate question, answered by {@link #railName(String)}.
*
* @param nodeName raw name from {@link IDIYComponent#getControlPointNodeName(int)}
* @return the sanitized name, or null if the raw name was null
@@ -75,6 +83,28 @@ public class Node implements Comparable<Node> {
// Never let sanitizing leave a pin nameless
return sanitized.isEmpty() ? nodeName.trim() : sanitized;
}
/**
* The rail a pin belongs to: its sanitized name with a trailing <code>_1</code>, <code>_2</code>
* disambiguator removed, so <code>GND_1</code> and <code>GND_2</code> report the same rail.
*
* <p>Only a numeric suffix is dropped. An underscore does three unrelated jobs across the
* pinouts in this library: it disambiguates a repeated rail, it namespaces a connector's signals
* (<code>ETH_TX-</code>, <code>USB_D+</code>), and it qualifies a genuinely separate net
* (<code>MISO_16U2</code>, <code>3V3_EN</code>). Cutting at the first underscore instead would
* report an Ethernet header as one rail.
*
* @param nodeName raw name from {@link IDIYComponent#getControlPointNodeName(int)}
* @return the rail name, or null if the pin has no name or names no net at all
*/
public static String railName(String nodeName) {
String sanitized = sanitizeNodeName(nodeName);
if (sanitized == null || sanitized.isEmpty()) {
return null;
}
String rail = sanitized.replaceAll("_\\d+$", "");
return NON_NET_NAMES.contains(rail) ? null : rail;
}
public Point2D getPoint2D() {
return component.getControlPoint(pointIndex);
@@ -565,6 +565,14 @@
<changeType>IMPROVEMENT</changeType>
<description>Bill of Materials now lists the selected variant of microcontroller boards and displays, so different versions of the same board are no longer merged into a single row</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>BUG_FIX</changeType>
<description>4-pin Jumbo Tube Socket - the two filament pins are now labeled F1 and F2 instead of both being labeled F, so they no longer read as the same node</description>
</org.diylc.appframework.update.Change>
<org.diylc.appframework.update.Change>
<changeType>IMPROVEMENT</changeType>
<description>Netlists now treat the repeated supply pins of a microcontroller or display board as one node, so grounding a single GND pin no longer leaves the others floating</description>
</org.diylc.appframework.update.Change>
</changes>
<url>https://github.com/bancika/diy-layout-creator/releases</url>
</org.diylc.appframework.update.Version>
@@ -48,4 +48,41 @@ public class NodeNameTest {
Assert.assertEquals("(only annotation)", Node.sanitizeNodeName("(only annotation)"));
Assert.assertNull(Node.sanitizeNodeName(null));
}
@Test
public void railNameDropsOnlyANumericDisambiguator() {
Assert.assertEquals("GND", Node.railName("GND_1"));
Assert.assertEquals("GND", Node.railName("GND_12"));
Assert.assertEquals("GND", Node.railName("GND"));
Assert.assertEquals("GND", Node.railName("GND (Pin 6)"));
Assert.assertEquals("+5V", Node.railName("+5V_2"));
// the annotation is stripped before the disambiguator, so an annotated rail still resolves
Assert.assertEquals("3.3V", Node.railName("3.3V_2 (250mA)"));
Assert.assertEquals("GND", Node.railName("GND_4 (Mid)"));
}
@Test
public void railNameKeepsNonNumericQualifiers() {
// namespaced connector signals
Assert.assertEquals("ETH_TX-", Node.railName("ETH_TX-"));
Assert.assertEquals("USB_D+", Node.railName("USB_D+"));
// qualifiers that name a separate net
Assert.assertEquals("MISO_16U2", Node.railName("MISO_16U2"));
Assert.assertEquals("3V3_EN", Node.railName("3V3_EN"));
// a bare trailing digit is part of the name, not a disambiguator
Assert.assertEquals("GP1", Node.railName("GP1"));
Assert.assertEquals("GND1", Node.railName("GND1"));
// digits that are not at the end are part of the name too: the 16U2 bridge's ground and 5V
// are separate nets from the board's own, and USB_5V is a connector pin, not a 5V rail
Assert.assertEquals("GND_16U2", Node.railName("GND_16U2"));
Assert.assertEquals("5V_16U2", Node.railName("5V_16U2"));
Assert.assertEquals("USB_5V", Node.railName("USB_5V"));
}
@Test
public void railNameRejectsPinsThatNameNoNet() {
Assert.assertNull(Node.railName("NC_1"));
Assert.assertNull(Node.railName("RSV_2"));
Assert.assertNull(Node.railName(null));
}
}
@@ -64,7 +64,7 @@ public class ArduinoMega extends AbstractMakerBoard {
// Pin names in sequence (98 pins total)
public static final String[] PIN_NAMES = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Low A0..A7 (8..15)
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7",
// Analog High A8..A15 (16..23)
@@ -72,7 +72,7 @@ public class ArduinoMega extends AbstractMakerBoard {
// Digital Low D0..D7 (24..31)
"D0 (RX0)", "D1 (TX0)", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)",
// Digital High D8..D13, GND, AREF, SDA, SCL (32..41)
"D8 (~)", "D9 (~)", "D10 (~)", "D11 (~)", "D12 (~)", "D13 (~)", "GND3", "AREF", "SDA", "SCL",
"D8 (~)", "D9 (~)", "D10 (~)", "D11 (~)", "D12 (~)", "D13 (~)", "GND_3", "AREF", "SDA", "SCL",
// Communication Header D14..D21 (42..49)
"D14 (TX3)", "D15 (RX3)", "D16 (TX2)", "D17 (RX2)", "D18 (TX1)", "D19 (RX1)", "D20 (SDA)", "D21 (SCL)",
// Double Digital 2x18 Header (50..85: D22..D53, GNDx2, 5Vx2)
@@ -78,9 +78,9 @@ public class ArduinoNano extends AbstractMakerBoard {
public static final String[] PIN_NAMES = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13",
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13",
// ICSP (30..35)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP"
};
@@ -93,26 +93,26 @@ public class ArduinoNano extends AbstractMakerBoard {
// Nano Every (ABX00028): PWM on D3, D5, D6, D9, D10 only, and D13 is SCK.
public static final String[] PIN_NAMES_EVERY = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
};
// Nano 33 IoT (ABX00027): the SAMD21 adds PWM on D2 relative to the Every.
public static final String[] PIN_NAMES_33_IOT = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (SCK)"
};
// Nano 33 BLE and BLE Sense (ABX00030): the nRF52840 routes PWM to every digital pin, D13
// included.
public static final String[] PIN_NAMES_33_BLE = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (~, SCK)"
"VIN", "GND_2", "RST2", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0 (DAC0)", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano RP2040 Connect (ABX00053): every RP2040 GPIO has a PWM slice. Note REC (BOOTSEL) where
@@ -121,17 +121,17 @@ public class ArduinoNano extends AbstractMakerBoard {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2 (~)", "D3 (~)", "D4 (~)", "D5 (~)", "D6 (~)", "D7 (~)", "D8 (~)", "D9 (~)", "D10 (~)", "D11 (~, MOSI)", "D12 (~, MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
"VIN", "GND_2", "REC", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3", "A2", "A1", "A0", "AREF", "3.3V", "D13 (~, SCK)"
};
// Nano R4 (ABX00142): the Renesas RA4M1 board. Same PWM set as the classic, but D4 / D5 double as
// the CAN pins, A1-A3 reach the on-chip OPAMP, A0 is the DAC, and BOOT replaces the second RESET.
public static final String[] PIN_NAMES_R4 = new String[] {
// Left row (0..14, top to bottom)
"D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D1 (TX)", "D0 (RX)", "RST1", "GND_1", "D2", "D3 (~)", "D4 (CAN TX)", "D5 (~, CAN RX)", "D6 (~)",
"D7", "D8", "D9 (~)", "D10 (~, CS)", "D11 (~, MOSI)", "D12 (MISO)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"VIN", "GND_2", "BOOT", "5V", "A7", "A6", "A5 (SCL)", "A4 (SDA)", "A3 (OPAMP OUT)",
"A2 (OPAMP -)", "A1 (OPAMP +)", "A0 (DAC)", "AREF", "3.3V", "D13 (SCK)",
// Qwiic socket (30), a single point at the centre of the connector so a wire can land on it
"QWIIC"
@@ -142,11 +142,11 @@ public class ArduinoNano extends AbstractMakerBoard {
// number, which is what the ESP-IDF documentation refers to.
public static final String[] PIN_NAMES_ESP32 = new String[] {
// Left row (0..14, top to bottom)
"D1 (~, TX/GPIO43)", "D0 (~, RX/GPIO44)", "RST1", "GND1", "D2 (~, GPIO5)", "D3 (~, GPIO6)",
"D1 (~, TX/GPIO43)", "D0 (~, RX/GPIO44)", "RST1", "GND_1", "D2 (~, GPIO5)", "D3 (~, GPIO6)",
"D4 (~, GPIO7)", "D5 (~, GPIO8)", "D6 (~, GPIO9)", "D7 (~, GPIO10)", "D8 (~, GPIO17)",
"D9 (~, GPIO18)", "D10 (~, GPIO21)", "D11 (~, MOSI/GPIO38)", "D12 (~, MISO/GPIO47)",
// Right row (15..29, top to bottom)
"VIN", "GND2", "B1", "VUSB", "A7 (~, GPIO14)", "A6 (~, GPIO13)", "A5 (~, SCL/GPIO12)",
"VIN", "GND_2", "B1", "VUSB", "A7 (~, GPIO14)", "A6 (~, GPIO13)", "A5 (~, SCL/GPIO12)",
"A4 (~, SDA/GPIO11)", "A3 (~, GPIO4)", "A2 (~, GPIO3)", "A1 (~, GPIO2)", "A0 (~, GPIO1)",
"B0", "3.3V", "D13 (~, SCK/GPIO48)"
};
@@ -75,13 +75,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// Pin names in sequence (44 pins total)
public static final String[] PIN_NAMES = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, ATmega328P)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP",
// Top-Left ICSP Header (38..43, ATmega16U2)
@@ -112,13 +112,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// ICSP header, and I2C is shared with D2 / D3 rather than living solely on A4 / A5.
public static final String[] PIN_NAMES_LEONARDO = new String[] {
// Power Header (0..7)
"NC", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"NC", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2 (SDA)", "D3 (~, SCL)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, ATmega32U4)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP"
};
@@ -141,13 +141,13 @@ public class ArduinoUno extends AbstractMakerBoard {
// carries a 10-pin SWD debug connector in the top right corner, modelled as a single node.
public static final String[] PIN_NAMES_R4_MINIMA = new String[] {
// Power Header (0..7)
"BOOT", "IOREF", "RESET", "3.3V", "5V", "GND1", "GND2", "VIN",
"BOOT", "IOREF", "RESET", "3.3V", "5V", "GND_1", "GND_2", "VIN",
// Analog Header (8..13)
"A0", "A1", "A2", "A3", "A4", "A5",
// Digital Low (14..21)
"D0 (RX)", "D1 (TX)", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7",
// Digital High (22..31)
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND3", "AREF", "SDA", "SCL",
"D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", "D13", "GND_3", "AREF", "SDA", "SCL",
// Main ICSP Header (32..37, RA4M1)
"MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP",
// SWD Connector (38)
@@ -89,11 +89,11 @@ public class RaspberryPiPico extends AbstractMakerBoard {
public static final String[] PIN_NAMES = new String[] {
// Left row (pins 0..19)
"GP0", "GP1", "GND1", "GP2", "GP3", "GP4", "GP5", "GND2", "GP6", "GP7",
"GP8", "GP9", "GND3", "GP10", "GP11", "GP12", "GP13", "GND4", "GP14", "GP15",
"GP0", "GP1", "GND_1", "GP2", "GP3", "GP4", "GP5", "GND_2", "GP6", "GP7",
"GP8", "GP9", "GND_3", "GP10", "GP11", "GP12", "GP13", "GND_4", "GP14", "GP15",
// Right row (pins 20..39)
"VBUS", "VSYS", "GND5", "3V3_EN", "3V3 (OUT)", "ADC_VREF", "GP28", "GND6", "GP27", "GP26",
"RUN", "GP22", "GND7", "GP21", "GP20", "GP19", "GP18", "GND8", "GP17", "GP16",
"VBUS", "VSYS", "GND_5", "3V3_EN", "3V3 (OUT)", "ADC_VREF", "GP28", "GND_6", "GP27", "GP26",
"RUN", "GP22", "GND_7", "GP21", "GP20", "GP19", "GP18", "GND_8", "GP17", "GP16",
// SWD debug pins (pins 40..42)
"SWCLK", "GND_SWD", "SWDIO"
};
@@ -89,15 +89,15 @@ public class Teensy extends AbstractMakerBoard {
// VUSB (33, near VIN): VUSB
public static final String[] PIN_NAMES_40 = new String[] {
// Left row (0..13)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"GND_1", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)",
// Right row (14..27)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"VIN (3.6-5.5V)", "GND_2", "3.3V_1 (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/CRX1/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "On/Off",
"VBAT", "3.3V_2 (End)", "GND_3 (End)", "Program", "On/Off",
// VUSB (33)
"VUSB"
};
@@ -125,19 +125,19 @@ public class Teensy extends AbstractMakerBoard {
// VUSB (64, near VIN): VUSB
public static final String[] PIN_NAMES_41 = new String[] {
// Left row (0..23)
"GND", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"GND_1", "0 (RX1/CS1/CRX2)", "1 (TX1/MISO1/CTX2)", "2 (OUT2)", "3 (LRCLK2)", "4 (BCLK2)",
"5 (IN2)", "6 (OUT1D)", "7 (RX2/OUT1A)", "8 (TX2/IN1)", "9 (OUT1C)", "10 (CS/MQSR)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"11 (MOSI/CTX1)", "12 (MISO/MQSL)", "3.3V_1", "24 (A10/TX6/SCL2)", "25 (A11/RX6/SDA2)",
"26 (A12/MOSI1)", "27 (A13/SCK1)", "28 (RX7)", "29 (TX7)", "30 (CRX3)", "31 (CTX3)",
"32 (OUT1B)",
// Right row (24..47)
"VIN (3.6-5.5V)", "GND", "3.3V (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"VIN (3.6-5.5V)", "GND_2", "3.3V_2 (250mA)", "23 (A9/CRX1)", "22 (A8/CTX1)", "21 (A7/RX5)",
"20 (A6/TX5)", "19 (A5/SCL)", "18 (A4/SDA)", "17 (A3/TX4/SDA1)", "16 (A2/RX4/SCL1)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND", "41 (A17)",
"15 (A1/RX3/SPDIF IN)", "14 (A0/TX3/SPDIF OUT)", "13 (SCK/LED)", "GND_3", "41 (A17)",
"40 (A16)", "39 (A15/MISO1)", "38 (A14/CS1)", "37 (CS)", "36 (CS)", "35 (TX8)",
"34 (RX8)", "33 (MCLK2)",
// Middle cluster (48..52, left to right near push button)
"VBAT", "3.3V (Mid)", "GND (Mid)", "Program", "On/Off",
"VBAT", "3.3V_3 (Mid)", "GND_4 (Mid)", "Program", "On/Off",
// Ethernet header (53..58, 3x2)
"ETH_TX-", "ETH_LED", "ETH_RX+", "ETH_TX+", "ETH_GND", "ETH_RX-",
// USB Host header (59..63, 1x5)
@@ -160,14 +160,14 @@ public class Teensy extends AbstractMakerBoard {
// severable VUSB pad next to Vin. Labels follow the PJRC Teensy 3.2 pinout card.
public static final String[] PIN_NAMES_32 = new String[] {
// Left row (0..13)
"GND", "0 (RX1)", "1 (TX1)", "2", "3 (PWM)", "4 (PWM)", "5 (PWM)", "6 (PWM)", "7 (RX3)",
"GND_1", "0 (RX1)", "1 (TX1)", "2", "3 (PWM)", "4 (PWM)", "5 (PWM)", "6 (PWM)", "7 (RX3)",
"8 (TX3)", "9 (RX2/PWM)", "10 (TX2/CS/PWM)", "11 (MOSI)", "12 (MISO)",
// Right row (14..27)
"Vin (3.6-6.0V)", "AGND", "3.3V (100mA)", "23 (A9/PWM)", "22 (A8/PWM)", "21 (A7/PWM)",
"Vin (3.6-6.0V)", "AGND", "3.3V_1 (100mA)", "23 (A9/PWM)", "22 (A8/PWM)", "21 (A7/PWM)",
"20 (A6/PWM)", "19 (A5/SCL0)", "18 (A4/SDA0)", "17 (A3)", "16 (A2)", "15 (A1/CS)",
"14 (A0)", "13 (SCK/LED)",
// End row (28..32, left to right along bottom edge)
"VBAT", "3.3V (End)", "GND (End)", "Program", "A14/DAC",
"VBAT", "3.3V_2 (End)", "GND_2 (End)", "Program", "A14/DAC",
// VUSB (33)
"VUSB"
};
@@ -74,7 +74,7 @@ public class Jumbo4PinTubeSocket extends AbstractAngledComponent<String> {
public static Size PIN_DIAMETER = new Size(1d, SizeUnit.mm);
public static Size HOLE_SIZE = new Size(5d, SizeUnit.mm);
public static Size OCTAL_TICK_SIZE = new Size(2d, SizeUnit.mm);
private static String[] electrodeLabels = new String[] {"G", "F", "P", "F"};
private static String[] electrodeLabels = new String[] {"G", "F1", "P", "F2"};
private String type = "";
@@ -0,0 +1,152 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.maker;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.core.IDIYComponent;
import org.diylc.netlist.Node;
import org.diylc.components.displays.WS2812BStrip;
import org.diylc.components.micro.ArduinoUno;
import org.diylc.components.micro.ESP32DevKit;
import org.diylc.components.micro.ESP8266NodeMCU;
import org.diylc.components.micro.RaspberryPi;
import org.diylc.components.micro.RaspberryPiPico;
import org.diylc.components.micro.Teensy;
/**
* Pins a board joins inside itself, as the netlist decides them through
* {@link Node#railName(String)}.
* <p>
* The negative cases carry the weight here. An underscore does three unrelated jobs across these
* pinouts - it disambiguates a repeated rail, it namespaces a connector's signals, and it qualifies
* a separate net - so a rule keyed on the text before the underscore would short an Ethernet or USB
* header together. Each of these was produced by a sweep of every board and variant.
*
* @author Branislav Stojkovic
*/
public class BoardRailTest {
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
@Test
public void repeatedRailsAreOneNode() throws Exception {
ESP32DevKit esp = new ESP32DevKit();
assertJoined(esp, "GND_1", "GND_2");
RaspberryPi pi = new RaspberryPi();
assertJoined(pi, "GND (Pin 6)", "GND (Pin 9)");
assertJoined(pi, "5V (Pin 2)", "5V (Pin 4)");
assertJoined(pi, "3.3V (Pin 1)", "3.3V (Pin 17)");
RaspberryPiPico pico = new RaspberryPiPico();
assertJoined(pico, "GND_1", "GND_8");
WS2812BStrip strip = new WS2812BStrip();
assertJoined(strip, "GND_1", "GND_2");
assertJoined(strip, "+5V_1", "+5V_2");
}
@Test
public void annotatedRailsAreOneNode() throws Exception {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
assertJoined(teensy, "GND_1", "GND_4 (Mid)");
assertJoined(teensy, "3.3V_1", "3.3V_2 (250mA)");
}
@Test
public void connectorSignalsSharingAPrefixAreNotJoined() throws Exception {
Teensy teensy = new Teensy();
teensy.setVersion(Teensy.TeensyVersion.Teensy_4_1);
assertSeparate(teensy, "ETH_TX-", "ETH_RX+");
assertSeparate(teensy, "ETH_TX-", "ETH_GND");
assertSeparate(teensy, "USB_D-", "USB_D+");
assertSeparate(teensy, "USB_5V", "USB_GND1");
}
@Test
public void qualifiedNamesAreSeparateNets() throws Exception {
ArduinoUno uno = new ArduinoUno();
uno.setVersion(ArduinoUno.ArduinoUnoVersion.REV3);
// the USB bridge has its own ICSP header; it is not the main MCU's SPI
assertSeparate(uno, "MISO", "MISO_16U2");
assertSeparate(uno, "SCK", "SCK_16U2");
RaspberryPiPico pico = new RaspberryPiPico();
// the regulator's enable input is not its output
assertSeparate(pico, "3V3_EN", "3V3 (OUT)");
}
@Test
public void absentConnectionsAreNotANet() throws Exception {
ESP32DevKit esp = new ESP32DevKit();
esp.setVersion(ESP32DevKit.DevKitVersion.ESP32_C6_DevKitC_1);
assertSeparate(esp, "NC_1", "NC_2");
ESP8266NodeMCU nodeMcu = new ESP8266NodeMCU();
assertSeparate(nodeMcu, "RSV_1", "RSV_2");
}
@Test
public void signalPinsAreNeverJoined() throws Exception {
RaspberryPiPico pico = new RaspberryPiPico();
assertSeparate(pico, "GP0", "GP1");
assertSeparate(pico, "GP0", "GP28");
}
private static void assertJoined(IDIYComponent<?> board, String first, String second) {
Assert.assertTrue(board.getClass().getSimpleName() + ": \"" + first + "\" and \"" + second
+ "\" should be one node",
sameRail(board, indexOf(board, first), indexOf(board, second)));
}
private static void assertSeparate(IDIYComponent<?> board, String first, String second) {
Assert.assertFalse(board.getClass().getSimpleName() + ": \"" + first + "\" and \"" + second
+ "\" should stay separate nodes",
sameRail(board, indexOf(board, first), indexOf(board, second)));
}
private static boolean sameRail(IDIYComponent<?> board, int index1, int index2) {
String rail = Node.railName(board.getControlPointNodeName(index1));
return rail != null && rail.equals(Node.railName(board.getControlPointNodeName(index2)));
}
private static int indexOf(IDIYComponent<?> board, String nodeName) {
for (int i = 0; i < board.getControlPointCount(); i++) {
if (nodeName.equals(board.getControlPointNodeName(i))) {
return i;
}
}
throw new IllegalArgumentException(
board.getClass().getSimpleName() + " has no pin named \"" + nodeName + "\"");
}
}
@@ -0,0 +1,262 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.maker;
import java.awt.Graphics2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.diylc.appframework.miscutils.ConfigurationManager;
import org.diylc.common.ComponentType;
import org.diylc.common.PropertyWrapper;
import org.diylc.core.ComponentState;
import org.diylc.core.IDIYComponent;
import org.diylc.core.IDrawingObserver;
import org.diylc.core.Project;
import org.diylc.presenter.ComponentProcessor;
/**
* Sweeps every registered maker component across the cross product of its enum properties.
* <p>
* The per-class tests pin down the figures each variant was built from, and
* {@link MakerComponentsTest} covers discovery, icons and rotation. Both of those exercise the
* default variant only: a component is built with its no-argument constructor and never asked for
* another version. The variants are where the geometry actually differs, and a release makes
* control-point counts and positions permanent, so this walks all of them.
* <p>
* The properties are swept as a cross product rather than one at a time because a variant is
* usually a pair - a panel size and an interface, a controller and a glass - and only the
* combination decides the pin count.
*
* @author Branislav Stojkovic
*/
public class MakerVariantSweepTest {
private static final String[] PACKAGES =
{"org.diylc.components.displays", "org.diylc.components.micro"};
/**
* Orientation is swept inside the drawing check instead of joining the cross product: it moves
* the control points but cannot change how many there are or what they are called, so folding it
* in would only report the same defect once per rotation.
*/
private static final String ORIENTATION = "Orientation";
private static final Project PROJECT = new Project();
private static final IDrawingObserver OBSERVER = new IDrawingObserver() {
@Override
public void stopTracking() {}
@Override
public void startTracking() {}
@Override
public void startTrackingContinuityArea(boolean positive) {}
@Override
public void stopTrackingContinuityArea() {}
@Override
public boolean isTrackingContinuityArea() {
return false;
}
@Override
public void setContinuityMarker(String marker) {}
};
@BeforeClass
public static void setUp() {
try {
ConfigurationManager.getInstance().initialize("diylc");
} catch (Exception ignored) {
}
}
/**
* Problems are collected rather than asserted one at a time, so a run reports every bad variant
* at once instead of stopping at the first.
*/
@Test
public void everyVariantHasSoundControlPointsAndDraws() throws Exception {
List<String> problems = new ArrayList<String>();
int swept = 0;
for (Class<? extends IDIYComponent<?>> clazz : makerComponentClasses()) {
List<PropertyWrapper> enums = new ArrayList<PropertyWrapper>();
PropertyWrapper orientation = null;
for (PropertyWrapper property : ComponentProcessor.getInstance().extractProperties(clazz)) {
if (!property.getType().isEnum()) {
continue;
}
if (ORIENTATION.equals(property.getName())) {
orientation = property;
} else {
enums.add(property);
}
}
int combinations = 1;
for (PropertyWrapper property : enums) {
combinations *= property.getType().getEnumConstants().length;
}
for (int index = 0; index < combinations; index++) {
IDIYComponent<?> component = clazz.getDeclaredConstructor().newInstance();
StringBuilder where = new StringBuilder(clazz.getSimpleName());
int remaining = index;
boolean complete = true;
for (PropertyWrapper property : enums) {
Object[] values = property.getType().getEnumConstants();
Object value = values[remaining % values.length];
remaining /= values.length;
where.append(" / ").append(property.getName()).append("=").append(value);
PropertyWrapper applied = (PropertyWrapper) property.clone();
applied.setValue(value);
try {
applied.writeTo(component);
} catch (Exception e) {
problems.add(where + ": will not apply -- " + e.getClass().getSimpleName());
complete = false;
break;
}
}
if (!complete) {
continue;
}
swept++;
checkNodeNamesAreUnique(component, where.toString(), problems);
checkControlPointsAreDistinct(component, where.toString(), problems);
checkDrawsInEveryOrientation(component, orientation, where.toString(), problems);
}
}
Assert.assertTrue("No maker components were discovered", swept > 0);
Assert.assertTrue(swept + " variants swept, " + problems.size() + " problem(s):\n"
+ String.join("\n", problems), problems.isEmpty());
}
private static void checkNodeNamesAreUnique(IDIYComponent<?> component, String where,
List<String> problems) {
Set<String> seen = new HashSet<String>();
for (int i = 0; i < component.getControlPointCount(); i++) {
String name = component.getControlPointNodeName(i);
// A null name is a point that carries no node, which is not a collision.
if (name != null && !seen.add(name)) {
problems.add(where + ": duplicate node name \"" + name + "\" at point " + i);
}
}
}
private static void checkControlPointsAreDistinct(IDIYComponent<?> component, String where,
List<String> problems) {
int count = component.getControlPointCount();
for (int i = 0; i < count; i++) {
Point2D first = component.getControlPoint(i);
for (int j = i + 1; j < count; j++) {
if (first.distance(component.getControlPoint(j)) <= 0.001) {
problems.add(where + ": control points " + i + " and " + j + " coincide");
}
}
}
}
private static void checkDrawsInEveryOrientation(IDIYComponent<?> component,
PropertyWrapper orientation, String where, List<String> problems)
throws CloneNotSupportedException {
Object[] orientations =
orientation == null ? new Object[] {null} : orientation.getType().getEnumConstants();
for (Object value : orientations) {
String at = where;
if (value != null) {
at = where + " / " + orientation.getName() + "=" + value;
PropertyWrapper applied = (PropertyWrapper) orientation.clone();
applied.setValue(value);
try {
applied.writeTo(component);
} catch (Exception e) {
problems.add(at + ": will not apply -- " + e.getClass().getSimpleName());
continue;
}
}
BufferedImage image = new BufferedImage(600, 600, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = image.createGraphics();
// Translating the canvas rather than moving control point 0 keeps the component's own
// geometry intact: a drag sets every control point, so moving only the first leaves the rest
// stale and the component draws from a mix of old and new positions.
g2d.translate(300, 300);
try {
component.draw(g2d, ComponentState.NORMAL, false, PROJECT, OBSERVER);
component.draw(g2d, ComponentState.SELECTED, false, PROJECT, OBSERVER);
component.draw(g2d, ComponentState.NORMAL, true, PROJECT, OBSERVER);
component.drawIcon(g2d, 32, 32);
} catch (Exception e) {
problems.add(at + ": draw threw " + e.getClass().getSimpleName() + " -- " + e.getMessage());
} finally {
g2d.dispose();
}
}
}
private static List<Class<? extends IDIYComponent<?>>> makerComponentClasses() {
List<Class<? extends IDIYComponent<?>>> classes =
new ArrayList<Class<? extends IDIYComponent<?>>>();
for (List<ComponentType> category : ComponentProcessor.getInstance().getComponentTypes()
.values()) {
for (ComponentType type : category) {
String name = type.getInstanceClass().getName();
for (String prefix : PACKAGES) {
if (name.startsWith(prefix + ".")) {
classes.add(type.getInstanceClass());
}
}
}
}
// Discovery order is not stable, and a failure message naming variants reads better sorted.
Collections.sort(classes, new Comparator<Class<? extends IDIYComponent<?>>>() {
@Override
public int compare(Class<? extends IDIYComponent<?>> a, Class<? extends IDIYComponent<?>> b) {
return a.getName().compareTo(b.getName());
}
});
return classes;
}
}
@@ -47,8 +47,8 @@ public class ArduinoNanoTest {
Assert.assertEquals("MISO", classic.getControlPointNodeName(30));
// the silkscreen prints what is on the board: both grounds and both resets are printed alike
Assert.assertEquals("GND1", classic.getControlPointNodeName(3));
Assert.assertEquals("GND2", classic.getControlPointNodeName(16));
Assert.assertEquals("GND_1", classic.getControlPointNodeName(3));
Assert.assertEquals("GND_2", classic.getControlPointNodeName(16));
Assert.assertEquals("GND", classic.getSilkPinLabel(3));
Assert.assertEquals("GND", classic.getSilkPinLabel(16));
Assert.assertEquals("RST", classic.getSilkPinLabel(2));
@@ -55,8 +55,8 @@ public class ArduinoUnoTest {
// the silkscreen prints what is on the board rather than the node name: both grounds of the
// power header say "GND" and the reserved first pin says nothing at all
Assert.assertEquals("GND1", r3.getControlPointNodeName(5));
Assert.assertEquals("GND2", r3.getControlPointNodeName(6));
Assert.assertEquals("GND_1", r3.getControlPointNodeName(5));
Assert.assertEquals("GND_2", r3.getControlPointNodeName(6));
Assert.assertEquals("GND", r3.getSilkPinLabel(5));
Assert.assertEquals("GND", r3.getSilkPinLabel(6));
Assert.assertEquals("", r3.getSilkPinLabel(0));
@@ -36,7 +36,7 @@ public class TeensyTest {
Teensy teensy40 = new Teensy();
Assert.assertEquals(TeensyVersion.Teensy_4_0, teensy40.getVersion());
Assert.assertEquals(34, teensy40.getControlPointCount());
Assert.assertEquals("GND", teensy40.getControlPointNodeName(0));
Assert.assertEquals("GND_1", teensy40.getControlPointNodeName(0));
Assert.assertEquals("0 (RX1/CS1/CRX2)", teensy40.getControlPointNodeName(1));
Assert.assertEquals("12 (MISO/MQSL)", teensy40.getControlPointNodeName(13));
Assert.assertEquals("VIN (3.6-5.5V)", teensy40.getControlPointNodeName(14));
+5 -2
View File
@@ -127,8 +127,11 @@ What these tests are for, beyond the obvious count-and-dimension checks:
clears the mounting holes; no hole is drilled where a pad is; a lit area that is measured really
does disagree with the rule it replaced. A test that restates the constant passes while the
component is wrong.
- **No duplicate node names,** on every variant. Duplicates reach the netlist as one node and are
invisible on the canvas.
- **No duplicate node names,** on every variant. The cost is legibility, not connectivity: nodes are
identified by component and control point index and the netlist graph is keyed on position, so two
pads named `GND` stay two nodes that merely print the same, and a reader cannot tell which one a
connection reached. They are invisible on the canvas too, since `getDisplayPinLabel` cuts the
suffix off and both silk as `GND`. `Teensy` is the one board that still repeats a bare name.
- **A drawing smoke test** over every variant, state and orientation.
- When a refactor is meant to change nothing, **render before and after and compare pixels.** The
`digitCount` work was verified that way, and it caught a sub-pixel hole shift the unit tests