From 89ce96b6c8fc7690467ceab3266de04825d34250 Mon Sep 17 00:00:00 2001 From: Bane Stojkovic Date: Fri, 11 Sep 2026 11:08:15 +0200 Subject: [PATCH] Controllers (#1068) * initial * more components * fixes * Update TreePanel.java * fix boost converter * Improve wrapping of component names * Update ULN2003Driver.java * Update NRF24L01Transceiver.java * imp * updates * improvements * more components * arduino * uno * icon * cleanup * mega * mega * improveent * nano * esp boards * esp * esp32 * esp32 * Update ESP8266NodeMCU.java * d1 * Update WemosD1Mini.java * pi * consolidate esp32 * pi pico updates * pi zero * Update RaspberryPiZero.java * pi pico * cleanup * pi improvements * consolidate pins * antenna consolidation * improve solder pads * teensy * headers * teensy fix, test cleanup * s3 * Update ESP32DevKit.java * usb standardization * pico w * button standardization * arduino improvements * Update AbstractMakerBoard.java * Update RaspberryPiZero.java * Update RaspberryPi.java * pi 3 * pi 5 improvements * Update RaspberryPiZero.java * fix tests * esp pinout fix * Update ESP32DevKit.java * pin name improvements * Create tier-1-modern-boards.md * new versions * pinouts * nano improvements * sanitize node names * label improvements * minima fixes * arduino uno labels * nano labels * Update ArduinoNano.java * mega * mega * comment out future stuff * release prep * Update RaspberryPiPico.java * Refactor abstract class * labeling improvement * label offset fixes * Update Teensy.java * consolidate tests * micro variants * Update RaspberryPi.java --- diylc/diylc-core/pom.xml | 2 +- .../org/diylc/netlist/NetlistBuilder.java | 1 + .../src/main/java/org/diylc/netlist/Node.java | 23 +- .../resources/import-defaults/variants.xml | 275 ++++++ .../diylc-core/src/main/resources/update.xml | 52 ++ .../java/org/diylc/netlist/NodeNameTest.java | 51 ++ diylc/diylc-library/pom.xml | 2 +- .../diylc/components/AbstractMakerBoard.java | 734 +++++++++++++++ .../org/diylc/components/MakerBoardLogos.java | 374 ++++++++ .../diylc/components/MakerBoardPainter.java | 242 +++++ .../components/displays/CharacterLCD.java | 281 ++++++ .../components/displays/LEDMatrix8x8.java | 235 +++++ .../components/displays/OLEDDisplay.java | 228 +++++ .../displays/SevenSegmentDisplay.java | 511 +++++++++++ .../diylc/components/displays/TFTDisplay.java | 200 ++++ .../components/displays/WS2812BRing.java | 304 ++++++ .../components/displays/WS2812BStick.java | 251 +++++ .../electromechanical/BatteryHolder18650.java | 337 +++++++ .../diylc/components/micro/ArduinoMega.java | 469 ++++++++++ .../diylc/components/micro/ArduinoNano.java | 668 ++++++++++++++ .../diylc/components/micro/ArduinoUno.java | 682 ++++++++++++++ .../diylc/components/micro/ESP32DevKit.java | 862 ++++++++++++++++++ .../components/micro/ESP8266NodeMCU.java | 296 ++++++ .../diylc/components/micro/RaspberryPi.java | 582 ++++++++++++ .../components/micro/RaspberryPiPico.java | 516 +++++++++++ .../components/micro/RaspberryPiZero.java | 405 ++++++++ .../org/diylc/components/micro/Teensy.java | 572 ++++++++++++ .../diylc/components/micro/WemosD1Mini.java | 383 ++++++++ .../modules/A4988StepperDriver.java | 199 ++++ .../modules/ActiveBuzzerModule.java | 225 +++++ .../modules/AnalogJoystickKY023.java | 201 ++++ .../components/modules/GPSModuleNEO6M.java | 246 +++++ .../components/modules/HC05Bluetooth.java | 205 +++++ .../modules/INA219CurrentSensor.java | 318 +++++++ .../components/modules/L298NMotorDriver.java | 240 +++++ .../modules/LM2596BuckConverter.java | 227 +++++ .../modules/LogicLevelConverter.java | 190 ++++ .../components/modules/MB102PowerSupply.java | 224 +++++ .../modules/MOSFETSwitchModule.java | 215 +++++ .../modules/MT3608BoostConverter.java | 233 +++++ .../modules/NRF24L01Transceiver.java | 276 ++++++ .../modules/PCA9685ServoDriver.java | 426 +++++++++ .../diylc/components/modules/RFIDRC522.java | 209 +++++ .../diylc/components/modules/RTCModule.java | 205 +++++ .../diylc/components/modules/RelayModule.java | 271 ++++++ .../modules/RotaryEncoderKY040.java | 195 ++++ .../components/modules/SDCardModule.java | 184 ++++ .../components/modules/TP4056Charger.java | 207 +++++ .../components/modules/ULN2003Driver.java | 337 +++++++ .../components/robotics/AbstractMotor.java | 231 +++++ .../components/robotics/DCHobbyMotor.java | 193 ++++ .../components/robotics/MicroServoSG90.java | 207 +++++ .../components/robotics/NEMA17Stepper.java | 201 ++++ .../robotics/StepperMotor28BYJ48.java | 206 +++++ .../components/sensors/BME280Sensor.java | 185 ++++ .../diylc/components/sensors/DHTSensor.java | 204 +++++ .../components/sensors/IRReceiverModule.java | 214 +++++ .../components/sensors/LDRSensorModule.java | 198 ++++ .../org/diylc/components/sensors/MPU6050.java | 179 ++++ .../components/sensors/PIRMotionSensor.java | 201 ++++ .../sensors/SoilMoistureSensor.java | 207 +++++ .../components/sensors/TCRT5000Sensor.java | 199 ++++ .../components/sensors/UltrasonicSensor.java | 209 +++++ .../components/maker/MakerComponentsTest.java | 259 ++++++ .../diylc/components/maker/UsbPortTest.java | 85 ++ .../components/micro/ArduinoMegaTest.java | 110 +++ .../components/micro/ArduinoNanoTest.java | 131 +++ .../components/micro/ArduinoUnoTest.java | 127 +++ .../components/micro/ESP32DevKitTest.java | 154 ++++ .../components/micro/ESP8266NodeMCUTest.java | 79 ++ .../micro/MakerBoardTestSupport.java | 145 +++ .../components/micro/RaspberryPiPicoTest.java | 125 +++ .../components/micro/RaspberryPiTest.java | 126 +++ .../components/micro/RaspberryPiZeroTest.java | 91 ++ .../diylc/components/micro/TeensyTest.java | 143 +++ .../components/micro/WemosD1MiniTest.java | 70 ++ .../netlist/GuitarDiagramAnalyzerTests.java | 11 +- diylc/diylc-swing/pom.xml | 2 +- .../diylc/swing/plugins/tree/TreePanel.java | 36 +- diylc/docs/plans/maker-board-roadmap.md | 456 +++++++++ diylc/pom.xml | 2 +- 81 files changed, 19542 insertions(+), 15 deletions(-) create mode 100644 diylc/diylc-core/src/test/java/org/diylc/netlist/NodeNameTest.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardLogos.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardPainter.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/CharacterLCD.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/LEDMatrix8x8.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/OLEDDisplay.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/SevenSegmentDisplay.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoMega.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP8266NodeMCU.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/micro/WemosD1Mini.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/A4988StepperDriver.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/AnalogJoystickKY023.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/HC05Bluetooth.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/L298NMotorDriver.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/LM2596BuckConverter.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/LogicLevelConverter.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/MB102PowerSupply.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/MOSFETSwitchModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/MT3608BoostConverter.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/NRF24L01Transceiver.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/PCA9685ServoDriver.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/RFIDRC522.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/RTCModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/RelayModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/RotaryEncoderKY040.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/SDCardModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/TP4056Charger.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/modules/ULN2003Driver.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/robotics/AbstractMotor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/robotics/DCHobbyMotor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/robotics/MicroServoSG90.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/robotics/NEMA17Stepper.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/robotics/StepperMotor28BYJ48.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/BME280Sensor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/DHTSensor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/LDRSensorModule.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/MPU6050.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/PIRMotionSensor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/SoilMoistureSensor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/TCRT5000Sensor.java create mode 100644 diylc/diylc-library/src/main/java/org/diylc/components/sensors/UltrasonicSensor.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/maker/UsbPortTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java create mode 100644 diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java create mode 100644 diylc/docs/plans/maker-board-roadmap.md diff --git a/diylc/diylc-core/pom.xml b/diylc/diylc-core/pom.xml index c759f3533..44438c901 100644 --- a/diylc/diylc-core/pom.xml +++ b/diylc/diylc-core/pom.xml @@ -7,7 +7,7 @@ org.diylc diylc-parent - 6.4.0 + 6.5.0 diylc-core diff --git a/diylc/diylc-core/src/main/java/org/diylc/netlist/NetlistBuilder.java b/diylc/diylc-core/src/main/java/org/diylc/netlist/NetlistBuilder.java index c93388917..bebc62ec2 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/netlist/NetlistBuilder.java +++ b/diylc/diylc-core/src/main/java/org/diylc/netlist/NetlistBuilder.java @@ -72,6 +72,7 @@ public class NetlistBuilder { if (!(c instanceof IContinuity && !(c instanceof ISwitch)) && !(c instanceof ICommonNode)) { // regular components, create a node per control point for (int i = 0; i < c.getControlPointCount(); i++) { + // Node renders the name through Node.sanitizeNodeName; here it only marks a named point String nodeName = c.getControlPointNodeName(i); if (c.isControlPointSticky(i) && nodeName != null && (!includeSwitches || !ISwitch.class.isAssignableFrom(type.getInstanceClass()))) { diff --git a/diylc/diylc-core/src/main/java/org/diylc/netlist/Node.java b/diylc/diylc-core/src/main/java/org/diylc/netlist/Node.java index 2335cc558..902dfa5af 100644 --- a/diylc/diylc-core/src/main/java/org/diylc/netlist/Node.java +++ b/diylc/diylc-core/src/main/java/org/diylc/netlist/Node.java @@ -52,7 +52,28 @@ public class Node implements Comparable { } public String getDisplayName() { - return component.getControlPointNodeName(pointIndex); + return sanitizeNodeName(component.getControlPointNodeName(pointIndex)); + } + + /** + * Strips the parenthesized annotation off a raw control point name, so a netlist reads + * MCU1.D11 rather than MCU1.D11 (~, MOSI). The bare name identifies the + * pin; everything in parentheses documents its alternate functions and only adds noise here. + * + *

A trailing disambiguator such as the _1 in GND_1 is deliberately + * kept, because those mark genuinely different pins that a netlist has to tell apart. + * + * @param nodeName raw name from {@link IDIYComponent#getControlPointNodeName(int)} + * @return the sanitized name, or null if the raw name was null + */ + public static String sanitizeNodeName(String nodeName) { + if (nodeName == null) { + return null; + } + int parenIndex = nodeName.indexOf('('); + String sanitized = (parenIndex == -1 ? nodeName : nodeName.substring(0, parenIndex)).trim(); + // Never let sanitizing leave a pin nameless + return sanitized.isEmpty() ? nodeName.trim() : sanitized; } public Point2D getPoint2D() { diff --git a/diylc/diylc-core/src/main/resources/import-defaults/variants.xml b/diylc/diylc-core/src/main/resources/import-defaults/variants.xml index 96603a315..db9957af4 100644 --- a/diylc/diylc-core/src/main/resources/import-defaults/variants.xml +++ b/diylc/diylc-core/src/main/resources/import-defaults/variants.xml @@ -5080,4 +5080,279 @@ + + org.diylc.components.micro.ArduinoNano + + + Nano (ATmega328) + + VersionCLASSIC + + false + 2026-09-12T00:00:00.0000000 + + + Nano 33 BLE + + VersionNANO_33_BLE + + false + 2026-09-12T00:00:00.0000000 + + + Nano 33 BLE Sense + + VersionNANO_33_BLE_SENSE + + false + 2026-09-12T00:00:00.0000000 + + + Nano 33 IoT + + VersionNANO_33_IOT + + false + 2026-09-12T00:00:00.0000000 + + + Nano ESP32 + + VersionNANO_ESP32 + + false + 2026-09-12T00:00:00.0000000 + + + Nano Every + + VersionEVERY + + false + 2026-09-12T00:00:00.0000000 + + + Nano R4 + + VersionNANO_R4 + + false + 2026-09-12T00:00:00.0000000 + + + Nano RP2040 Connect + + VersionNANO_RP2040_CONNECT + + false + 2026-09-12T00:00:00.0000000 + + + + + org.diylc.components.micro.ArduinoUno + + + Leonardo + + VersionLEONARDO + + false + 2026-09-12T00:00:00.0000000 + + + UNO R3 + + VersionREV3 + + false + 2026-09-12T00:00:00.0000000 + + + UNO R4 Minima + + VersionR4_MINIMA + + false + 2026-09-12T00:00:00.0000000 + + + UNO R4 WiFi + + VersionR4_WIFI + + false + 2026-09-12T00:00:00.0000000 + + + + + org.diylc.components.micro.ESP32DevKit + + + ESP32 DevKit V1 (30-Pin) + + VersionDevKit_V1_30Pin + + false + 2026-09-12T00:00:00.0000000 + + + ESP32 DevKitC V4 (38-Pin) + + VersionDevKitC_V4_38Pin + + false + 2026-09-12T00:00:00.0000000 + + + ESP32-S3 DevKitC-1 (44-Pin) + + VersionESP32_S3_DevKitC_44Pin + + false + 2026-09-12T00:00:00.0000000 + + + ESP32-C3 DevKitM-1 (30-Pin) + + VersionESP32_C3_DevKitM_1 + + false + 2026-09-12T00:00:00.0000000 + + + ESP32-C6 DevKitC-1 (32-Pin) + + VersionESP32_C6_DevKitC_1 + + false + 2026-09-12T00:00:00.0000000 + + + + + org.diylc.components.micro.RaspberryPi + + + Pi 3 (Model B+) + + VersionPI_3_B + + false + 2026-09-12T00:00:00.0000000 + + + Pi 4 (Model B) + + VersionPI_4_B + + false + 2026-09-12T00:00:00.0000000 + + + Pi 5 + + VersionPI_5 + + false + 2026-09-12T00:00:00.0000000 + + + + + org.diylc.components.micro.RaspberryPiPico + + + Pi Pico + + VersionPICO + + false + 2026-09-12T00:00:00.0000000 + + + Pi Pico W + + VersionPICO_W + + false + 2026-09-12T00:00:00.0000000 + + + Pi Pico 2 + + VersionPICO_2 + + false + 2026-09-12T00:00:00.0000000 + + + Pi Pico 2 W + + VersionPICO_2_W + + false + 2026-09-12T00:00:00.0000000 + + + + + org.diylc.components.micro.RaspberryPiZero + + + Pi Zero + + VersionPI_ZERO + + false + 2026-09-12T00:00:00.0000000 + + + Pi Zero W + + VersionPI_ZERO_W + + false + 2026-09-12T00:00:00.0000000 + + + Pi Zero 2 W + + VersionPI_ZERO_2_W + + false + 2026-09-12T00:00:00.0000000 + + + + + org.diylc.components.micro.Teensy + + + Teensy 3.2 + + VersionTeensy_3_2 + + false + 2026-09-12T00:00:00.0000000 + + + Teensy 4.0 + + VersionTeensy_4_0 + + false + 2026-09-12T00:00:00.0000000 + + + Teensy 4.1 + + VersionTeensy_4_1 + + false + 2026-09-12T00:00:00.0000000 + + + diff --git a/diylc/diylc-core/src/main/resources/update.xml b/diylc/diylc-core/src/main/resources/update.xml index fb5ebe5c5..6b42082e5 100644 --- a/diylc/diylc-core/src/main/resources/update.xml +++ b/diylc/diylc-core/src/main/resources/update.xml @@ -412,5 +412,57 @@ https://github.com/bancika/diy-layout-creator/releases + + + 6 + 5 + 0 + + 2026-09-04 00:00:00.000 CET + + + + NEW_FEATURE + Arduino Mega 2560 + + + NEW_FEATURE + Arduino Nano - ATmega328, 33 BLE, 33 BLE Sense, 33 IoT, ESP32, Every, R4, RP3040 Connect + + + NEW_FEATURE + Arduino Uno - R3, R4 Minima, R4 WiFi, Leonardo + + + NEW_FEATURE + ESP32 - DevKit V1 (30-pin), DevKitC V4 (38-pin), S3 DevKitC-1 (44-pin), C3 DevKitM-1 (30-pin), C6 DevKitC-1 (32-pin) + + + NEW_FEATURE + ESP8266 NodeMCU + + + NEW_FEATURE + Raspberry Pi - 3B+, 4B, 5 + + + NEW_FEATURE + Raspberry Pi - Pico, Pico W, Pico 2, Pico 2 W + + + NEW_FEATURE + Raspberry Pi - Zero, Zero 2, Zero 2 W + + + NEW_FEATURE + Teensy - 3.2, 4.0, 4.1 + + + NEW_FEATURE + Wemos D1 Mini + + + https://github.com/bancika/diy-layout-creator/releases + diff --git a/diylc/diylc-core/src/test/java/org/diylc/netlist/NodeNameTest.java b/diylc/diylc-core/src/test/java/org/diylc/netlist/NodeNameTest.java new file mode 100644 index 000000000..99c7978f1 --- /dev/null +++ b/diylc/diylc-core/src/test/java/org/diylc/netlist/NodeNameTest.java @@ -0,0 +1,51 @@ +/* + * + * 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.netlist; + +import org.junit.Assert; +import org.junit.Test; + +public class NodeNameTest { + + @Test + public void testSanitizeNodeName() { + // The annotation in parentheses is stripped, the pin name is kept + Assert.assertEquals("D11", Node.sanitizeNodeName("D11 (~, MOSI)")); + Assert.assertEquals("A5", Node.sanitizeNodeName("A5 (SCL)")); + Assert.assertEquals("D3", Node.sanitizeNodeName("D3 (~)")); + Assert.assertEquals("VIN", Node.sanitizeNodeName("VIN (3.6-5.5V)")); + Assert.assertEquals("GPIO2/SDA", Node.sanitizeNodeName("GPIO2/SDA (Pin 3)")); + + // Names without an annotation are untouched + Assert.assertEquals("VIN", Node.sanitizeNodeName("VIN")); + Assert.assertEquals("Tip", Node.sanitizeNodeName("Tip")); + Assert.assertEquals("3.3V", Node.sanitizeNodeName("3.3V")); + + // Trailing disambiguators stay: these mark genuinely different pins + Assert.assertEquals("GND_1", Node.sanitizeNodeName("GND_1")); + Assert.assertEquals("5V_ICSP", Node.sanitizeNodeName("5V_ICSP")); + + // Degenerate input never leaves a pin nameless + Assert.assertEquals("(only annotation)", Node.sanitizeNodeName("(only annotation)")); + Assert.assertNull(Node.sanitizeNodeName(null)); + } +} diff --git a/diylc/diylc-library/pom.xml b/diylc/diylc-library/pom.xml index 23c1e7231..55d5e12b2 100644 --- a/diylc/diylc-library/pom.xml +++ b/diylc/diylc-library/pom.xml @@ -7,7 +7,7 @@ org.diylc diylc-parent - 6.4.0 + 6.5.0 diylc-library diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java new file mode 100644 index 000000000..8d74e09db --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/AbstractMakerBoard.java @@ -0,0 +1,734 @@ +/* + * + * 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; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.appframework.miscutils.ConfigurationManager; +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.IPlugInPort; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.core.ComponentState; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Theme; +import org.diylc.core.VisibilityPolicy; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.netlist.Node; +import org.diylc.utils.Constants; + +/** + * Base abstract class for Maker-focused boards, microcontrollers, sensors, displays, and breakout modules. + */ +public abstract class AbstractMakerBoard extends AbstractTransparentComponent { + + private static final long serialVersionUID = 1L; + + public static final Color ARDUINO_TEAL = Color.decode("#00878F"); + public static final Color ARDUINO_BLUE = Color.decode("#015687"); + + public static Color PIN_COLOR = Color.decode("#00B2EE"); + public static Color PIN_BORDER_COLOR = PIN_COLOR.darker(); + public static Color HEADER_BODY_COLOR = Color.decode("#222222"); + public static Color HEADER_BORDER_COLOR = Color.decode("#111111"); + public static Color MOUNTING_HOLE_COLOR = Color.decode("#B8860B"); + public static Color MOUNTING_HOLE_BORDER = Color.decode("#8B6508"); + public static Color IC_BODY_COLOR = Color.decode("#1A1A1A"); + public static Color IC_TEXT_COLOR = Color.decode("#CCCCCC"); + public static Color SCREW_TERMINAL_COLOR = Color.decode("#90AB66"); + public static Color SCREW_TERMINAL_BORDER = Color.decode("#90AB66").darker(); + public static Color SCREW_CIRCLE_COLOR = LIGHT_METAL_COLOR; + public static Color METAL_SHIELD_COLOR = Color.decode("#C0C0C0"); + public static Color METAL_SHIELD_BORDER = Color.decode("#909090"); + public static Color USB_METAL_COLOR = Color.decode("#D3D3D3"); + public static Color IC_BORDER_COLOR = Color.decode("#333333"); + public static Color PIN_MARKER_COLOR = Color.decode("#555555"); + public static Color METAL_LABEL_COLOR = Color.decode("#555555"); + public static Color ANTENNA_COLOR = Color.decode("#DAA520"); + public static Color ANTENNA_BG_COLOR = Color.decode("#1E1E1E"); + public static Color PAD_COLOR = Color.decode("#DAA520"); + public static Color CONNECTOR_PLASTIC_COLOR = Color.decode("#F5F5DC"); + public static Color CONNECTOR_PLASTIC_BORDER = Color.decode("#333333"); + 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 Size PIN_SIZE = new Size(0.04d, SizeUnit.in); + public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in); + public static Size PAD_SIZE = new Size(0.065d, SizeUnit.in); + public static Size HOLE_SIZE = new Size(0.035d, SizeUnit.in); + public static Size NOTCH_SIZE = new Size(0.9d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size ANTENNA_LENGTH = new Size(7.0d, SizeUnit.mm); + public static Size BUTTON_WIDTH = new Size(3.5d, SizeUnit.mm); + public static Size BUTTON_LENGTH = new Size(3.0d, SizeUnit.mm); + + // Standard USB Port Dimensions + public static Size USB_MICRO_WIDTH = new Size(7.5d, SizeUnit.mm); + public static Size USB_MICRO_LENGTH = new Size(5.6d, SizeUnit.mm); + public static Size USB_MICRO_OVERHANG = new Size(0.5d, SizeUnit.mm); + + public static Size USB_C_WIDTH = new Size(8.94d, SizeUnit.mm); + public static Size USB_C_LENGTH = new Size(7.5d, SizeUnit.mm); + public static Size USB_C_OVERHANG = new Size(0.5d, SizeUnit.mm); + + public static Size USB_A_WIDTH = new Size(14.5d, SizeUnit.mm); + public static Size USB_A_LENGTH = new Size(14.0d, SizeUnit.mm); + public static Size USB_A_DUAL_LENGTH = new Size(17.5d, SizeUnit.mm); + + public static Size USB_B_WIDTH = new Size(0.45d, SizeUnit.in); + public static Size USB_B_LENGTH = new Size(0.51d, SizeUnit.in); + public static Size USB_B_OVERHANG = new Size(0.14d, SizeUnit.in); + + public static Size USB_MINI_WIDTH = new Size(0.30d, SizeUnit.in); + public static Size USB_MINI_LENGTH = new Size(0.36d, SizeUnit.in); + public static Size USB_MINI_OVERHANG = new Size(0.05d, SizeUnit.in); + + public enum UsbPortType { + MICRO, + TYPE_C, + TYPE_A, + TYPE_B, + MINI + } + + public static Font SILK_FONT_SMALL = new Font("SansSerif", Font.PLAIN, 10); + public static Font SILK_FONT = new Font("SansSerif", Font.BOLD, 11); + public static Font SILK_FONT_LARGE = new Font("SansSerif", Font.BOLD, 13); + public static Size PIN_LABEL_OFFSET = new Size(1.6d, SizeUnit.mm); + // diagonal labels are anchored by both coordinates at once, so they sit closer to the pad than + // the straight ones without touching it + public static Size PIN_LABEL_CORNER_OFFSET = new Size(1.0d, SizeUnit.mm); + public static Size PIN_ROW_LABEL_OFFSET = new Size(2.0d, SizeUnit.mm); + public static Font PIN_FONT = new Font("SansSerif", Font.PLAIN, 8); + // labels along a row run across the board rather than down a column of pins, so they can afford + // a size the tightly stacked column labels cannot + public static Font PIN_ROW_FONT = new Font("SansSerif", Font.PLAIN, 9); + + protected Orientation orientation = Orientation.DEFAULT; + protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)}; + protected Color bodyColor = Color.decode("#006699"); + + public AbstractMakerBoard() { + super(); + } + + @EditableProperty(name = "Orientation") + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Board Color") + public Color getBodyColor() { + return bodyColor; + } + + public void setBodyColor(Color bodyColor) { + this.bodyColor = bodyColor; + invalidateCache(); + } + + @Override + public Point2D getControlPoint(int index) { + return controlPoints[index]; + } + + @Override + public void setControlPoint(Point2D point, int index) { + controlPoints[index].setLocation(point); + invalidateCache(); + } + + @Override + public int getControlPointCount() { + return controlPoints.length; + } + + @Override + public boolean isControlPointSticky(int index) { + return true; + } + + @Override + public VisibilityPolicy getControlPointVisibilityPolicy(int index) { + return VisibilityPolicy.WHEN_SELECTED; + } + + @Override + public boolean canControlPointOverlap(int index) { + return false; + } + + @Override + public boolean canPointMoveFreely(int pointIndex) { + return false; + } + + @Override + public Void getValue() { + return null; + } + + @Override + public void setValue(Void value) {} + + protected abstract void updateControlPoints(); + + protected void invalidateCache() {} + + /** + * Helper to rotate an array of relative point offsets around the origin (first point) by orientation. + */ + protected void rotatePoints(Point2D firstPoint, double[][] relativeOffsets) { + controlPoints = new Point2D[relativeOffsets.length]; + controlPoints[0] = firstPoint; + double theta = orientation.toRadians(); + AffineTransform tx = AffineTransform.getRotateInstance(theta, firstPoint.getX(), firstPoint.getY()); + for (int i = 1; i < relativeOffsets.length; i++) { + Point2D p = new Point2D.Double(firstPoint.getX() + relativeOffsets[i][0], firstPoint.getY() + relativeOffsets[i][1]); + tx.transform(p, p); + controlPoints[i] = p; + } + } + + /** + * Helper to draw standard pin header pins at given control point indices. + */ + protected void drawPinHeader(Graphics2D g2d, int startIndex, int count, boolean outlineMode, + IDrawingObserver drawingObserver) { + if (outlineMode) return; + int pinPx = (int) Math.round(PIN_SIZE.convertToPixels()); + drawingObserver.startTrackingContinuityArea(true); + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + g2d.setColor(HEADER_BODY_COLOR); + g2d.fill(new Rectangle2D.Double(p.getX() - pinPx - 1, p.getY() - pinPx - 1, (pinPx + 1) * 2, (pinPx + 1) * 2)); + g2d.setColor(PIN_COLOR); + g2d.fill(new Rectangle2D.Double(p.getX() - pinPx / 2.0, p.getY() - pinPx / 2.0, pinPx, pinPx)); + } + drawingObserver.stopTrackingContinuityArea(); + + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + g2d.setColor(PIN_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(p.getX() - pinPx / 2.0, p.getY() - pinPx / 2.0, pinPx, pinPx)); + } + } + + /** + * Helper to format a pin name for silkscreen display (e.g. "3V3_1" -> "3V3", "A0 (ADC0)" -> "A0"). + * + *

Stripping the parenthesized annotation is the same operation the netlist performs, so it is + * delegated to {@link Node#sanitizeNodeName(String)} rather than duplicated. The silkscreen goes + * one step further and also drops the trailing disambiguator, because a board prints "GND" on + * every ground pin even though the netlist has to tell "GND_1" from "GND_2". + */ + public static String getDisplayPinLabel(String name) { + String sanitized = Node.sanitizeNodeName(name); + if (sanitized == null) { + return ""; + } + int underscoreIdx = sanitized.indexOf('_'); + return (underscoreIdx == -1 ? sanitized : sanitized.substring(0, underscoreIdx)).trim(); + } + + /** + * Silkscreen text printed next to a pin. Defaults to the node name with its annotation and + * disambiguator stripped, which is right whenever the node name is the silkscreen name plus + * extra detail. A board whose silkscreen genuinely differs from its node names -- the ESP + * DevKits print bare GPIO numbers while their nodes carry the full function list -- overrides + * this to return its own silkscreen label. + */ + protected String getSilkPinLabel(int index) { + return getDisplayPinLabel(getControlPointNodeName(index)); + } + + /** + * Helper to draw rotated control point / pin names next to the pins for dual-row DIP/header boards. + * + * @param g2d Graphics2D context (already transformed for board orientation) + * @param x Unrotated top-left pin X coordinate (P0.getX()) + * @param y Unrotated top-left pin Y coordinate (P0.getY()) + * @param offsets Array of [x, y] relative offsets for all control points + * @param silkColor Silkscreen text color + */ + protected void drawPinLabels(Graphics2D g2d, double x, double y, double[][] offsets, Color silkColor) { + if (offsets == null || offsets.length == 0) return; + drawPinLabels(g2d, x, y, offsets, offsets.length / 2, silkColor); + } + + /** + * Variant for boards whose two pin rows do not take up the whole control point array, such as a + * board that carries an ICSP block or a connector after them. Control points past the two rows + * are left unlabelled. + * + * @param g2d Graphics2D context (already transformed for board orientation) + * @param x Unrotated top-left pin X coordinate (P0.getX()) + * @param y Unrotated top-left pin Y coordinate (P0.getY()) + * @param offsets Array of [x, y] relative offsets for all control points + * @param pinsPerRow Number of pins in each of the two rows + * @param silkColor Silkscreen text color + */ + protected void drawPinLabels(Graphics2D g2d, double x, double y, double[][] offsets, + int pinsPerRow, Color silkColor) { + if (offsets == null || offsets.length == 0) return; + double labelOffset = PIN_LABEL_OFFSET.convertToPixels(); + + g2d.setColor(silkColor); + g2d.setFont(PIN_FONT); + + for (int i = 0; i < 2 * pinsPerRow && i < offsets.length; i++) { + String label = getSilkPinLabel(i); + if (label == null || label.isEmpty()) { + continue; + } + boolean leftRow = i < pinsPerRow; + double pinX = x + offsets[i][0]; + double pinY = y + offsets[i][1]; + double textX = leftRow ? (pinX + labelOffset) : (pinX - labelOffset); + double textY = pinY; + + // the two rows are mirrored so that each label reads away from the pin it belongs to, the + // way a board prints them + AffineTransform oldLabelTx = g2d.getTransform(); + g2d.rotate(leftRow ? Math.PI / 2 : -Math.PI / 2, textX, textY); + StringUtils.drawCenteredText(g2d, label, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setTransform(oldLabelTx); + } + } + + /** + * Variant for boards too crowded for labels that stand square to their row. Each label is turned + * 45 degrees and tucked into the corner of its pad on the side the row reads towards - the top + * right corner for the left row and the top left corner for the right one - which is the trick + * the Teensy 4.1 uses to fit its pad names between two rows a tenth of an inch apart. + * + * @param g2d Graphics2D context (already transformed for board orientation) + * @param x Unrotated top-left pin X coordinate (P0.getX()) + * @param y Unrotated top-left pin Y coordinate (P0.getY()) + * @param offsets Array of [x, y] relative offsets for all control points + * @param pinsPerRow Number of pins in each of the two rows + * @param silkColor Silkscreen text color + */ + protected void drawDiagonalPinLabels(Graphics2D g2d, double x, double y, double[][] offsets, + int pinsPerRow, Color silkColor) { + if (offsets == null || offsets.length == 0) return; + double cornerOffset = PIN_LABEL_CORNER_OFFSET.convertToPixels(); + double straightOffset = PIN_LABEL_OFFSET.convertToPixels(); + + g2d.setColor(silkColor); + g2d.setFont(PIN_FONT); + + for (int i = 0; i < 2 * pinsPerRow && i < offsets.length; i++) { + String label = getSilkPinLabel(i); + if (label == null || label.isEmpty()) { + continue; + } + boolean leftRow = i < pinsPerRow; + // the pad that starts each row has the board edge above it rather than another pad, so it + // keeps the square on label the rest of the row has no room for + boolean square = i == 0 || i == pinsPerRow; + double offset = square ? straightOffset : cornerOffset; + double textX = x + offsets[i][0] + (leftRow ? offset : -offset); + double textY = y + offsets[i][1] - (square ? 0 : cornerOffset); + + // the two rows slant away from each other so that each label runs along the diagonal gap + // between its own pad and the one above it + AffineTransform oldLabelTx = g2d.getTransform(); + g2d.rotate((square ? Math.PI / 2 : Math.PI / 4) * (leftRow ? 1 : -1), textX, textY); + StringUtils.drawCenteredText(g2d, label, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setTransform(oldLabelTx); + } + } + + /** + * Helper to draw rotated pin names next to a horizontal row of pins, the way Arduino-style boards + * print their headers. Labels read top to bottom, are centered on the pin they belong to and + * start {@link #PIN_ROW_LABEL_OFFSET} away from it, growing away from the row. + * + * @param g2d Graphics2D context (already transformed for board orientation) + * @param x Unrotated first control point X coordinate (P0.getX()) + * @param y Unrotated first control point Y coordinate (P0.getY()) + * @param offsets Array of [x, y] relative offsets for all control points + * @param startIndex Index of the first pin of the row + * @param count Number of pins in the row + * @param below True to print the labels below the row, false to print them above it + * @param silkColor Silkscreen text color + */ + protected void drawRowPinLabels(Graphics2D g2d, double x, double y, double[][] offsets, + int startIndex, int count, boolean below, Color silkColor) { + if (offsets == null || offsets.length == 0) return; + double labelOffset = PIN_ROW_LABEL_OFFSET.convertToPixels(); + + g2d.setColor(silkColor); + g2d.setFont(PIN_ROW_FONT); + + for (int i = startIndex; i < startIndex + count && i < offsets.length; i++) { + String label = getSilkPinLabel(i); + if (label == null || label.isEmpty()) { + continue; + } + double pinX = x + offsets[i][0]; + double pinY = y + offsets[i][1]; + + // the quarter turn maps the text advance direction onto -y, so a label above the row is + // anchored by its first character and one below the row by its last, which keeps both of + // them clear of the pin by the same gap + AffineTransform oldLabelTx = g2d.getTransform(); + g2d.rotate(-Math.PI / 2, pinX, pinY); + StringUtils.drawCenteredText(g2d, label, pinX + (below ? -labelOffset : labelOffset), pinY, + below ? HorizontalAlignment.RIGHT : HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + g2d.setTransform(oldLabelTx); + } + } + + /** + * Helper to draw a complete PCB terminal block (green body + inner compartment lines + screw lugs) + * that matches the visuals of the PCBTerminalBlock component. + * + * @param g2d Graphics2D context + * @param startIndex First control point index + * @param count Number of positions in this terminal block + * @param isHorizontal true if terminal block runs horizontally (along X), false if vertically (along Y) + * @param wireEntryOffset Direction/offset from pins to the wire entry edge (positive or negative) + * @param blockDepth Total depth of the green body (e.g. 50.0px for 5.08mm blocks, or 35.0px for compact) + * @param outlineMode Outline mode flag + * @param drawingObserver Observer + */ + protected void drawTerminalBlock(Graphics2D g2d, int startIndex, int count, boolean isHorizontal, + double wireEntryOffset, double blockDepth, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + if (count <= 0 || startIndex < 0 || startIndex >= controlPoints.length) return; + + Point2D p0 = controlPoints[startIndex]; + Point2D pLast = controlPoints[Math.min(startIndex + count - 1, controlPoints.length - 1)]; + + double pitchPx = count > 1 ? + (isHorizontal ? Math.abs(pLast.getX() - p0.getX()) / (count - 1) : Math.abs(pLast.getY() - p0.getY()) / (count - 1)) + : new Size(5.08d, SizeUnit.mm).convertToPixels(); + if (pitchPx <= 0) { + pitchPx = new Size(5.08d, SizeUnit.mm).convertToPixels(); + } + + double minX = Math.min(p0.getX(), pLast.getX()); + double minY = Math.min(p0.getY(), pLast.getY()); + double maxX = Math.max(p0.getX(), pLast.getX()); + double maxY = Math.max(p0.getY(), pLast.getY()); + + Rectangle2D outerBody; + Rectangle2D innerCompartment; + + if (isHorizontal) { + double bx = minX - pitchPx / 2.0; + double bw = (maxX - minX) + pitchPx; + double by = wireEntryOffset >= 0 ? minY - (blockDepth - pitchPx) / 2.0 : minY - (blockDepth - pitchPx / 2.0); + double bh = blockDepth; + outerBody = new Rectangle2D.Double(bx, by, bw, bh); + innerCompartment = new Rectangle2D.Double(bx, minY - pitchPx / 2.0, bw, pitchPx); + } else { + double by = minY - pitchPx / 2.0; + double bh = (maxY - minY) + pitchPx; + double bx = wireEntryOffset >= 0 ? minX - (blockDepth - pitchPx) / 2.0 : minX - (blockDepth - pitchPx / 2.0); + double bw = blockDepth; + outerBody = new Rectangle2D.Double(bx, by, bw, bh); + innerCompartment = new Rectangle2D.Double(minX - pitchPx / 2.0, by, pitchPx, bh); + } + + // Draw green body and compartment + g2d.setColor(SCREW_TERMINAL_COLOR); + g2d.fill(outerBody); + g2d.setColor(SCREW_TERMINAL_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(innerCompartment); + g2d.draw(outerBody); + + // Draw screw contacts + drawScrewTerminals(g2d, startIndex, count, pitchPx, outlineMode, drawingObserver); + } + + /** + * Helper to draw screw terminal block contacts at given control points. + * Visuals match the PCBTerminalBlock component: light metal screw circles with diagonal slot. + */ + protected void drawScrewTerminals(Graphics2D g2d, int startIndex, int count, double pitchPx, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + double effectivePitch = pitchPx > 0 ? pitchPx : PIN_SPACING.convertToPixels(); + int circleDiameter = getClosestOdd((int) (effectivePitch * 3d / 5)); + + drawingObserver.startTrackingContinuityArea(true); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f)); + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + // Light metal screw head circle + g2d.setColor(SCREW_CIRCLE_COLOR); + g2d.fillOval((int) Math.round(p.getX() - circleDiameter / 2.0), (int) Math.round(p.getY() - circleDiameter / 2.0), + circleDiameter, circleDiameter); + // Diagonal screw slot line + g2d.setColor(SCREW_CIRCLE_COLOR.darker()); + g2d.drawLine((int) (p.getX() + Math.cos(Math.PI / 4) * circleDiameter / 2.0), + (int) (p.getY() + Math.sin(Math.PI / 4) * circleDiameter / 2.0), + (int) (p.getX() + Math.cos(5 * Math.PI / 4) * circleDiameter / 2.0), + (int) (p.getY() + Math.sin(5 * Math.PI / 4) * circleDiameter / 2.0)); + } + drawingObserver.stopTrackingContinuityArea(); + } + + /** + * Helper to draw rectangular tinned solder pads with drill holes at given control points. + */ + protected void drawSolderPads(Graphics2D g2d, int startIndex, int count, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + double padW = 22.0; + double padH = 16.0; + double holeD = 7.0; + + drawingObserver.startTrackingContinuityArea(true); + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + RoundRectangle2D pad = new RoundRectangle2D.Double(p.getX() - padW / 2.0, p.getY() - padH / 2.0, padW, padH, 2, 2); + g2d.setColor(LIGHT_METAL_COLOR); + g2d.fill(pad); + g2d.setColor(LIGHT_METAL_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(pad); + + // Central through-hole / drill hole + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new Ellipse2D.Double(p.getX() - holeD / 2.0, p.getY() - holeD / 2.0, holeD, holeD)); + } + drawingObserver.stopTrackingContinuityArea(); + } + + /** + * Helper to draw standard PCB through-hole solder pads (gold/copper pads with drill holes, square for Pin 1). + * + * @param g2d Graphics2D context + * @param startIndex First control point index + * @param count Number of pads to draw + * @param squarePin1 If true, the first pad (index == startIndex) is drawn as a square pad + * @param outlineMode Outline mode flag + * @param drawingObserver Observer + */ + protected void drawPcbSolderPads(Graphics2D g2d, int startIndex, int count, boolean squarePin1, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + int diameter = getClosestOdd((int) Math.round(PAD_SIZE.convertToPixels())); + int holeDiameter = getClosestOdd((int) Math.round(HOLE_SIZE.convertToPixels())); + + drawingObserver.startTrackingContinuityArea(true); + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + if (squarePin1 && i == startIndex) { + // Pin 1 is a square solder pad + g2d.setColor(PAD_COLOR); + g2d.fill(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter)); + g2d.setColor(PAD_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter)); + } else { + // Round solder pads + g2d.setColor(PAD_COLOR); + g2d.fill(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter)); + g2d.setColor(PAD_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Ellipse2D.Double(p.getX() - diameter / 2.0, p.getY() - diameter / 2.0, diameter, diameter)); + } + + // Central drill hole + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(p.getX() - holeDiameter / 2.0, p.getY() - holeDiameter / 2.0, holeDiameter, holeDiameter)); + g2d.setColor(PAD_COLOR.darker()); + g2d.draw(new Ellipse2D.Double(p.getX() - holeDiameter / 2.0, p.getY() - holeDiameter / 2.0, holeDiameter, holeDiameter)); + } + drawingObserver.stopTrackingContinuityArea(); + } + + /** + * Pad, drill-hole and edge-notch diameters used by {@link #drawCastellatedPads} and + * {@link #subtractCastellationNotches}. A board whose castellations differ from the package + * defaults overrides these rather than reimplementing the pad geometry. + */ + protected Size getCastellatedPadSize() { + return PAD_SIZE; + } + + protected Size getCastellatedHoleSize() { + return HOLE_SIZE; + } + + protected Size getCastellatedNotchSize() { + return NOTCH_SIZE; + } + + protected Color getCastellatedPadColor() { + return PAD_COLOR; + } + + /** + * Helper to draw one column of castellated edge pads: a round through-hole pad joined to the + * board edge by a stub, with a semicircular notch bitten out of the edge itself. Pair it with + * {@link #subtractCastellationNotches} so the outline and the pads agree. + * + *

Unlike the control-point based pad helpers this one works in unrotated board coordinates, + * so it has to be called while the board rotation is still applied to {@code g2d}. + * + * @param g2d Graphics2D context, already transformed for board orientation + * @param pinX Unrotated X coordinate of the pad column + * @param pinY Unrotated Y coordinate of the first pad + * @param count Number of pads in the column + * @param spacing Pad pitch in pixels + * @param edgeX Unrotated X coordinate of the board edge the pads reach; the stub is drawn toward + * it, so it may lie on either side of {@code pinX} + */ + protected void drawCastellatedPads(Graphics2D g2d, double pinX, double pinY, int count, double spacing, + double edgeX, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + + int padD = getClosestOdd((int) Math.round(getCastellatedPadSize().convertToPixels())); + int holeD = getClosestOdd((int) Math.round(getCastellatedHoleSize().convertToPixels())); + int notchD = getClosestOdd((int) Math.round(getCastellatedNotchSize().convertToPixels())); + double padR = padD / 2.0; + double holeR = holeD / 2.0; + double notchR = notchD / 2.0; + double stubX = Math.min(pinX, edgeX); + double stubW = Math.abs(edgeX - pinX); + Color padColor = getCastellatedPadColor(); + + drawingObserver.startTrackingContinuityArea(true); + for (int i = 0; i < count; i++) { + double py = pinY + i * spacing; + + Area padArea = new Area(new Rectangle2D.Double(stubX, py - padR, stubW, padD)); + padArea.add(new Area(new Ellipse2D.Double(pinX - padR, py - padR, padD, padD))); + padArea.subtract(new Area(new Ellipse2D.Double(edgeX - notchR, py - notchR, notchD, notchD))); + padArea.subtract(new Area(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD))); + + g2d.setColor(padColor); + g2d.fill(padArea); + g2d.setColor(padColor.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(padArea); + + // Inner through-hole drill hole (white circle matching Zero and perfboard) + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD)); + g2d.setColor(padColor.darker()); + g2d.draw(new Ellipse2D.Double(pinX - holeR, py - holeR, holeD, holeD)); + } + drawingObserver.stopTrackingContinuityArea(); + } + + /** + * Helper to bite the semicircular castellation notches for one column of pads out of a board + * outline, matching what {@link #drawCastellatedPads} draws along the same edge. + */ + protected void subtractCastellationNotches(Area boardArea, double pinY, int count, double spacing, + double edgeX) { + int notchD = getClosestOdd((int) Math.round(getCastellatedNotchSize().convertToPixels())); + double notchR = notchD / 2.0; + for (int i = 0; i < count; i++) { + double py = pinY + i * spacing; + boardArea.subtract(new Area(new Ellipse2D.Double(edgeX - notchR, py - notchR, notchD, notchD))); + } + } + + /** + * Helper to get outline border color. + */ + protected Color getFinalBorderColor(ComponentState componentState, boolean outlineMode) { + if (outlineMode) { + Theme theme = Constants.DEFAULT_THEME; + try { + Theme t = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME); + if (t != null) theme = t; + } catch (Exception ignored) {} + return (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) ? SELECTION_COLOR : theme.getOutlineColor(); + } + return (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) ? SELECTION_COLOR : bodyColor.darker(); + } + + /** + * Returns the unrotated body shape in local coordinate space (relative to unrotated p0). + */ + public abstract Shape getBodyShape(); + + @Override + public Rectangle2D getCachingBounds() { + double minX = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxY = -Double.MAX_VALUE; + + if (controlPoints != null) { + for (Point2D p : controlPoints) { + if (p != null) { + if (p.getX() < minX) minX = p.getX(); + if (p.getX() > maxX) maxX = p.getX(); + if (p.getY() < minY) minY = p.getY(); + if (p.getY() > maxY) maxY = p.getY(); + } + } + } + + Shape bodyShape = getBodyShape(); + if (bodyShape != null && controlPoints != null && controlPoints.length > 0 && controlPoints[0] != null) { + Point2D p0 = controlPoints[0]; + AffineTransform tx = AffineTransform.getRotateInstance(orientation.toRadians(), p0.getX(), p0.getY()); + Shape rotated = tx.createTransformedShape(bodyShape); + Rectangle2D b = rotated.getBounds2D(); + if (b.getMinX() < minX) minX = b.getMinX(); + if (b.getMaxX() > maxX) maxX = b.getMaxX(); + if (b.getMinY() < minY) minY = b.getMinY(); + if (b.getMaxY() > maxY) maxY = b.getMaxY(); + } + + if (minX == Double.MAX_VALUE) { + Point2D p0 = (controlPoints != null && controlPoints.length > 0 && controlPoints[0] != null) + ? controlPoints[0] : new Point2D.Double(0, 0); + return new Rectangle2D.Double(p0.getX() - 50, p0.getY() - 50, 100, 100); + } + + int margin = 50; + return new Rectangle2D.Double(minX - margin, minY - margin, (maxX - minX) + 2 * margin, (maxY - minY) + 2 * margin); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardLogos.java b/diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardLogos.java new file mode 100644 index 000000000..ea0f896d1 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardLogos.java @@ -0,0 +1,374 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Path2D; + +/** + * Vector artwork for the manufacturer logos silkscreened onto maker boards. The paths are traced + * from the official brand marks and are drawn in white, the way the boards themselves print them. + * + * @author Branislav Stojkovic + */ +public class MakerBoardLogos { + + private static final Area RASPBERRY_PI_LOGO_AREA = createRaspberryPiLogoArea(); + + private MakerBoardLogos() {} + + /** + * Helper to draw the official Arduino infinity logo (white infinity symbol with '-' and '+'). + * Default bounding box size is 95.56 x 45.33 px. + * + * @param g2d Graphics2D context + * @param x Top-left X coordinate of the logo bounding box + * @param y Top-left Y coordinate of the logo bounding box + */ + public static void drawArduinoLogo(Graphics2D g2d, double x, double y) { + drawArduinoLogo(g2d, x, y, 1.0); + } + + /** + * Helper to draw the official Arduino infinity logo with custom scaling. + * + * @param g2d Graphics2D context + * @param x Top-left X coordinate of the logo bounding box + * @param y Top-left Y coordinate of the logo bounding box + * @param scale Scale factor (1.0 = 95.56 x 45.33 px) + */ + public static void drawArduinoLogo(Graphics2D g2d, double x, double y, double scale) { + AffineTransform oldTx = g2d.getTransform(); + g2d.translate(x, y); + if (scale != 1.0) { + g2d.scale(scale, scale); + } + + Path2D.Double infinityBody = new Path2D.Double(Path2D.WIND_EVEN_ODD); + // Outer loop + infinityBody.moveTo(47.56, 14.67); + infinityBody.curveTo(50.00, 12.00, 51.78, 9.78, 54.00, 7.78); + infinityBody.curveTo(62.22, 0.44, 71.56, -2.44, 82.22, 2.22); + infinityBody.curveTo(92.22, 6.44, 97.78, 16.89, 95.56, 27.56); + infinityBody.curveTo(93.56, 37.33, 84.00, 44.89, 73.78, 45.33); + infinityBody.curveTo(64.22, 45.56, 56.89, 41.33, 50.89, 34.44); + infinityBody.curveTo(50.00, 33.33, 49.11, 32.22, 48.00, 30.89); + infinityBody.curveTo(47.11, 31.78, 46.44, 32.67, 46.00, 33.33); + infinityBody.curveTo(39.56, 41.33, 31.56, 46.00, 20.89, 45.33); + infinityBody.curveTo(10.44, 44.67, 0.67, 34.89, 0.00, 24.44); + infinityBody.curveTo(-0.89, 11.11, 9.33, 0.22, 23.33, 0.00); + infinityBody.curveTo(32.67, 0.00, 40.00, 4.67, 45.78, 12.00); + infinityBody.curveTo(46.44, 12.89, 47.11, 13.56, 47.78, 14.67); + infinityBody.closePath(); + + // Left hole cutout + infinityBody.moveTo(24.44, 7.56); + infinityBody.curveTo(14.67, 7.56, 7.56, 13.78, 6.89, 21.78); + infinityBody.curveTo(6.22, 28.89, 11.78, 36.00, 19.56, 37.78); + infinityBody.curveTo(27.33, 39.33, 33.33, 36.22, 38.00, 30.67); + infinityBody.curveTo(45.11, 22.22, 44.89, 23.56, 38.00, 15.11); + infinityBody.curveTo(34.22, 10.44, 29.11, 7.78, 24.22, 7.56); + infinityBody.closePath(); + + // Right hole cutout + infinityBody.moveTo(72.67, 7.33); + infinityBody.curveTo(71.56, 7.33, 70.67, 7.33, 69.78, 7.33); + infinityBody.curveTo(61.56, 8.89, 56.44, 14.44, 52.44, 21.56); + infinityBody.curveTo(52.22, 22.00, 52.22, 23.11, 52.44, 23.56); + infinityBody.curveTo(56.00, 29.56, 60.22, 34.89, 67.33, 37.11); + infinityBody.curveTo(74.00, 39.33, 80.00, 37.56, 84.67, 32.22); + infinityBody.curveTo(88.89, 27.56, 89.78, 22.00, 87.11, 16.44); + infinityBody.curveTo(84.22, 10.44, 78.89, 7.78, 72.67, 7.33); + infinityBody.lineTo(72.67, 7.33); + infinityBody.closePath(); + + g2d.setColor(Color.WHITE); + g2d.fill(infinityBody); + + // Minus sign in left loop + Path2D.Double minus = new Path2D.Double(); + minus.moveTo(31.33, 24.44); + minus.curveTo(31.33, 24.44, 31.33, 24.89, 30.89, 24.89); + minus.lineTo(16.44, 24.89); + minus.curveTo(16.44, 24.89, 16.00, 24.89, 16.00, 24.44); + minus.lineTo(16.00, 20.00); + minus.curveTo(16.00, 20.00, 16.00, 19.56, 16.44, 19.56); + minus.lineTo(30.89, 19.56); + minus.curveTo(30.89, 19.56, 31.33, 19.56, 31.33, 20.00); + minus.lineTo(31.33, 24.44); + minus.closePath(); + g2d.fill(minus); + + // Plus sign in right loop + Path2D.Double plus = new Path2D.Double(); + plus.moveTo(78.89, 20.22); + plus.curveTo(78.89, 20.22, 78.89, 19.78, 78.44, 19.78); + plus.lineTo(74.22, 19.78); + plus.curveTo(74.22, 19.78, 73.78, 19.78, 73.78, 19.33); + plus.lineTo(73.78, 15.11); + plus.curveTo(73.78, 15.11, 73.78, 14.67, 73.33, 14.67); + plus.lineTo(68.89, 14.67); + plus.curveTo(68.89, 14.67, 68.44, 14.67, 68.44, 15.11); + plus.lineTo(68.44, 19.33); + plus.curveTo(68.44, 19.33, 68.44, 19.78, 68.00, 19.78); + plus.lineTo(63.78, 19.78); + plus.curveTo(63.78, 19.78, 63.33, 19.78, 63.33, 20.22); + plus.lineTo(63.33, 24.67); + plus.curveTo(63.33, 24.67, 63.33, 25.11, 63.78, 25.11); + plus.lineTo(68.00, 25.11); + plus.curveTo(68.00, 25.11, 68.44, 25.11, 68.44, 25.56); + plus.lineTo(68.44, 29.78); + plus.curveTo(68.44, 29.78, 68.44, 30.22, 68.89, 30.22); + plus.lineTo(73.33, 30.22); + plus.curveTo(73.33, 30.22, 73.78, 30.22, 73.78, 29.78); + plus.lineTo(73.78, 25.56); + plus.curveTo(73.78, 25.56, 73.78, 25.11, 74.22, 25.11); + plus.lineTo(78.44, 25.11); + plus.curveTo(78.44, 25.11, 78.89, 25.11, 78.89, 24.67); + plus.lineTo(78.89, 20.22); + plus.closePath(); + g2d.fill(plus); + + g2d.setTransform(oldTx); + } + + /** + * Helper to draw the Raspberry Pi logo in white using vector paths. + * Native bounding box is 72.51 x 92.60 px. + * + * @param g2d Graphics2D context + * @param x Top-left X coordinate + * @param y Top-left Y coordinate + * @param size Target height in pixels + */ + public static void drawRaspberryPiLogo(Graphics2D g2d, double x, double y, double size) { + AffineTransform oldTx = g2d.getTransform(); + g2d.translate(x, y); + double scale = size / 92.604; + g2d.scale(scale, scale); + + g2d.setColor(Color.WHITE); + g2d.fill(RASPBERRY_PI_LOGO_AREA); + g2d.setTransform(oldTx); + } + + private static Area createRaspberryPiLogoArea() { + Path2D.Double p0 = new Path2D.Double(Path2D.WIND_NON_ZERO); + p0.moveTo(19.6295, 0.0014); + p0.curveTo(19.1612, 0.0159, 18.6569, 0.1889, 18.0849, 0.6402); + p0.curveTo(16.6837, 0.0999, 15.3251, -0.0878, 14.1102, 1.0122); + p0.curveTo(12.2342, 0.7689, 11.6245, 1.2710, 11.1626, 1.8572); + p0.curveTo(10.7507, 1.8487, 8.0807, 1.4339, 6.8564, 3.2603); + p0.curveTo(3.7794, 2.8963, 2.8070, 5.0701, 3.9089, 7.0973); + p0.curveTo(3.2805, 8.0702, 2.6293, 9.0313, 4.0989, 10.8859); + p0.curveTo(3.5790, 11.9189, 3.9013, 13.0394, 5.1259, 14.3954); + p0.curveTo(4.8027, 15.8475, 5.4379, 16.8720, 6.5775, 17.6704); + p0.curveTo(6.3644, 19.6575, 8.3997, 20.8128, 9.0075, 21.2245); + p0.curveTo(9.2408, 22.3823, 9.7272, 23.4749, 12.0520, 24.0790); + p0.curveTo(12.4354, 25.8045, 13.8326, 26.1024, 15.1856, 26.4646); + p0.curveTo(10.7138, 29.0639, 6.8789, 32.4840, 6.9049, 40.8755); + p0.lineTo(6.2500, 42.0440); + p0.curveTo(1.1224, 45.1622, -3.4908, 55.1843, 3.7230, 63.3304); + p0.curveTo(4.1942, 65.8804, 4.9845, 67.7119, 5.6881, 69.7390); + p0.curveTo(6.7405, 77.9072, 13.6088, 81.7320, 15.4203, 82.1843); + p0.curveTo(18.0749, 84.2063, 20.9021, 86.1249, 24.7280, 87.4689); + p0.curveTo(28.3346, 91.1885, 32.2420, 92.6062, 36.1706, 92.6039); + p0.curveTo(36.2282, 92.6039, 36.2868, 92.6045, 36.3445, 92.6039); + p0.curveTo(40.2731, 92.6063, 44.1803, 91.1887, 47.7870, 87.4689); + p0.curveTo(51.6128, 86.1249, 54.4401, 84.2063, 57.0947, 82.1843); + p0.curveTo(58.9062, 81.7319, 65.7745, 77.9072, 66.8269, 69.7390); + p0.curveTo(67.5305, 67.7119, 68.3208, 65.8803, 68.7920, 63.3304); + p0.curveTo(76.0057, 55.1837, 71.3926, 45.1609, 66.2650, 42.0427); + p0.lineTo(65.6090, 40.8744); + p0.curveTo(65.6350, 32.4834, 61.8002, 29.0634, 57.3284, 26.4634); + p0.curveTo(58.6814, 26.1014, 60.0786, 25.8034, 60.4619, 24.0779); + p0.curveTo(62.7867, 23.4737, 63.2732, 22.3811, 63.5065, 21.2234); + p0.curveTo(64.1143, 20.8118, 66.1495, 19.6564, 65.9365, 17.6693); + p0.curveTo(67.0759, 16.8708, 67.7113, 15.8463, 67.3880, 14.3943); + p0.curveTo(68.6127, 13.0383, 68.9350, 11.9177, 68.4150, 10.8847); + p0.curveTo(69.8849, 9.0309, 69.2328, 8.0698, 68.6052, 7.0970); + p0.curveTo(69.7063, 5.0698, 68.7346, 2.8959, 65.6566, 3.2600); + p0.curveTo(64.4326, 1.4335, 61.7634, 1.8484, 61.3506, 1.8569); + p0.curveTo(60.8887, 1.2707, 60.2793, 0.7685, 58.4032, 1.0119); + p0.curveTo(57.1883, -0.0882, 55.8298, 0.0995, 54.4285, 0.6399); + p0.curveTo(52.7646, -0.6731, 51.6635, 0.3794, 50.4059, 0.7773); + p0.curveTo(48.3914, 0.1191, 47.9309, 1.0207, 46.9410, 1.3879); + p0.curveTo(44.7440, 0.9236, 44.0764, 1.9344, 43.0232, 3.0012); + p0.lineTo(41.7982, 2.9769); + p0.curveTo(38.4844, 4.9297, 36.8382, 8.9061, 36.2548, 10.9503); + p0.curveTo(35.6710, 8.9057, 34.0286, 4.9293, 30.7155, 2.9769); + p0.lineTo(29.4904, 3.0012); + p0.curveTo(28.4359, 1.9344, 27.7683, 0.9236, 25.5714, 1.3879); + p0.curveTo(24.5815, 1.0207, 24.1222, 0.1191, 22.1064, 0.7773); + p0.curveTo(21.2810, 0.5162, 20.5218, -0.0267, 19.6279, 0.0010); + p0.closePath(); + + Path2D.Double p1 = new Path2D.Double(Path2D.WIND_NON_ZERO); + p1.moveTo(13.0322, 8.5925); + p1.curveTo(21.8236, 13.1250, 26.9346, 16.7915, 29.7345, 19.9142); + p1.curveTo(28.3006, 25.6612, 20.8204, 25.9235, 18.0852, 25.7623); + p1.curveTo(18.6452, 25.5016, 19.1125, 25.1894, 19.2782, 24.7097); + p1.curveTo(18.5919, 24.2219, 16.1583, 24.6583, 14.4594, 23.7037); + p1.curveTo(15.1120, 23.5686, 15.4173, 23.4369, 15.7225, 22.9552); + p1.curveTo(14.1174, 22.4433, 12.3885, 22.0022, 11.3715, 21.1541); + p1.curveTo(11.9203, 21.1609, 12.4327, 21.2768, 13.1493, 20.7798); + p1.curveTo(11.7116, 20.0050, 10.1775, 19.3910, 8.9856, 18.2068); + p1.curveTo(9.7289, 18.1885, 10.5303, 18.1994, 10.7633, 17.9260); + p1.curveTo(9.4475, 17.1108, 8.3373, 16.2042, 7.4183, 15.2126); + p1.curveTo(8.4585, 15.3381, 8.8977, 15.2300, 9.1493, 15.0488); + p1.curveTo(8.1546, 14.0300, 6.8956, 13.1698, 6.2954, 11.9143); + p1.curveTo(7.0678, 12.1805, 7.7744, 12.2825, 8.2838, 11.8909); + p1.curveTo(7.9458, 11.1284, 6.4976, 10.6787, 5.6638, 8.8967); + p1.curveTo(6.4770, 8.9756, 7.3394, 9.0741, 7.5119, 8.8967); + p1.curveTo(7.1345, 7.3591, 6.4869, 6.4947, 5.8518, 5.5991); + p1.curveTo(7.5920, 5.5732, 10.2286, 5.6058, 10.1092, 5.4587); + p1.lineTo(9.0332, 4.3593); + p1.curveTo(10.7330, 3.9016, 12.4723, 4.4328, 13.7350, 4.8271); + p1.curveTo(14.3020, 4.3797, 13.7250, 3.8141, 13.0332, 3.2365); + p1.curveTo(14.4778, 3.4293, 15.7832, 3.7615, 16.9630, 4.2189); + p1.curveTo(17.5935, 3.6497, 16.5537, 3.0805, 16.0507, 2.5113); + p1.curveTo(18.2825, 2.9347, 19.2281, 3.5297, 20.1677, 4.1253); + p1.curveTo(20.8496, 3.4719, 20.2068, 2.9165, 19.7467, 2.3476); + p1.curveTo(21.4295, 2.9709, 22.2962, 3.7756, 23.2088, 4.5698); + p1.curveTo(23.5182, 4.1523, 23.9948, 3.8463, 23.4193, 2.8388); + p1.curveTo(24.6141, 3.5275, 25.5139, 4.3390, 26.1796, 5.2482); + p1.curveTo(26.9189, 4.7775, 26.6200, 4.1338, 26.6241, 3.5406); + p1.curveTo(27.8657, 4.5506, 28.6537, 5.6255, 29.6183, 6.6751); + p1.curveTo(29.8126, 6.5336, 29.9827, 6.0538, 30.1329, 5.2949); + p1.curveTo(33.0952, 8.1688, 37.2809, 15.4074, 31.2089, 18.2774); + p1.curveTo(26.0413, 14.0154, 19.8697, 10.9176, 13.0304, 8.5937); + p1.closePath(); + p1.moveTo(59.6793, 8.5925); + p1.curveTo(50.8889, 13.1254, 45.7783, 16.7909, 42.9783, 19.9142); + p1.curveTo(44.4122, 25.6612, 51.8924, 25.9235, 54.6276, 25.7623); + p1.curveTo(54.0676, 25.5016, 53.6003, 25.1894, 53.4346, 24.7097); + p1.curveTo(54.1210, 24.2219, 56.5545, 24.6583, 58.2535, 23.7037); + p1.curveTo(57.6008, 23.5686, 57.2955, 23.4369, 56.9903, 22.9552); + p1.curveTo(58.5954, 22.4433, 60.3244, 22.0022, 61.3413, 21.1541); + p1.curveTo(60.7925, 21.1609, 60.2801, 21.2768, 59.5635, 20.7798); + p1.curveTo(61.0012, 20.0050, 62.5353, 19.3910, 63.7272, 18.2068); + p1.curveTo(62.9839, 18.1885, 62.1825, 18.1994, 61.9495, 17.9260); + p1.curveTo(63.2653, 17.1108, 64.3755, 16.2042, 65.2946, 15.2126); + p1.curveTo(64.2543, 15.3381, 63.8151, 15.2300, 63.5636, 15.0488); + p1.curveTo(64.5582, 14.0300, 65.8172, 13.1698, 66.4174, 11.9143); + p1.curveTo(65.6450, 12.1805, 64.9384, 12.2825, 64.4290, 11.8909); + p1.curveTo(64.7670, 11.1284, 66.2152, 10.6787, 67.0490, 8.8967); + p1.curveTo(66.2358, 8.9756, 65.3734, 9.0741, 65.2010, 8.8967); + p1.curveTo(65.5790, 7.3585, 66.2266, 6.4940, 66.8617, 5.5984); + p1.curveTo(65.1215, 5.5726, 62.4849, 5.6052, 62.6043, 5.4581); + p1.lineTo(63.6803, 4.3587); + p1.curveTo(61.9805, 3.9010, 60.2411, 4.4322, 58.9785, 4.8265); + p1.curveTo(58.4115, 4.3791, 58.9885, 3.8135, 59.6802, 3.2358); + p1.curveTo(58.2357, 3.4287, 56.9302, 3.7608, 55.7504, 4.2183); + p1.curveTo(55.1200, 3.6491, 56.1598, 3.0799, 56.6627, 2.5107); + p1.curveTo(54.4310, 2.9341, 53.4854, 3.5290, 52.5457, 4.1247); + p1.curveTo(51.8639, 3.4713, 52.5067, 2.9159, 52.9667, 2.3470); + p1.curveTo(51.2840, 2.9703, 50.4172, 3.7750, 49.5047, 4.5692); + p1.curveTo(49.1953, 4.1517, 48.7187, 3.8456, 49.2941, 2.8382); + p1.curveTo(48.0994, 3.5268, 47.1995, 4.3384, 46.5338, 5.2476); + p1.curveTo(45.7946, 4.7769, 46.0934, 4.1332, 46.0894, 3.5399); + p1.curveTo(44.8477, 4.5499, 44.0597, 5.6249, 43.0952, 6.6744); + p1.curveTo(42.9009, 6.5330, 42.7307, 6.0532, 42.5805, 5.2943); + p1.curveTo(39.6183, 8.1682, 35.4325, 15.4067, 41.5045, 18.2768); + p1.curveTo(46.6695, 14.0139, 52.8407, 10.9163, 59.6805, 8.5924); + p1.closePath(); + + Path2D.Double p2 = new Path2D.Double(Path2D.WIND_NON_ZERO); + p2.moveTo(47.0024, 67.1140); + p2.curveTo(47.0188, 70.5889, 45.0112, 73.8170, 41.7504, 75.5588); + p2.curveTo(38.4896, 77.3006, 34.4542, 77.3006, 31.1934, 75.5588); + p2.curveTo(27.9326, 73.8170, 25.9250, 70.5889, 25.9414, 67.1140); + p2.curveTo(25.9250, 63.6391, 27.9326, 60.4110, 31.1934, 58.6692); + p2.curveTo(34.4542, 56.9274, 38.4896, 56.9274, 41.7504, 58.6692); + p2.curveTo(45.0112, 60.4110, 47.0188, 63.6391, 47.0024, 67.1140); + p2.closePath(); + p2.moveTo(30.3786, 39.3746); + p2.curveTo(33.2055, 41.2176, 34.6522, 44.7077, 34.1649, 48.5090); + p2.curveTo(33.6776, 52.3102, 31.3275, 55.8665, 28.0144, 57.8164); + p2.curveTo(24.7013, 59.7663, 20.9097, 59.8245, 18.0912, 57.9688); + p2.curveTo(15.2643, 56.1258, 13.8176, 52.6357, 14.3049, 48.8344); + p2.curveTo(14.7922, 45.0332, 17.1423, 41.4769, 20.4554, 39.5270); + p2.curveTo(23.7685, 37.5771, 27.5601, 37.5189, 30.3786, 39.3746); + p2.closePath(); + + Path2D.Double p3 = new Path2D.Double(Path2D.WIND_NON_ZERO); + p3.moveTo(42.1557, 38.8570); + p3.curveTo(37.8215, 41.6966, 37.0284, 48.2301, 40.3987, 53.3303); + p3.curveTo(43.7691, 58.4306, 50.1089, 60.2909, 54.4431, 57.4513); + p3.curveTo(58.7773, 54.6117, 59.5704, 48.0782, 56.2001, 42.9780); + p3.curveTo(52.8297, 37.8777, 46.4899, 36.0174, 42.1557, 38.8570); + p3.closePath(); + p3.moveTo(8.5710, 44.0484); + p3.curveTo(13.2819, 42.7856, 10.1614, 63.5377, 6.3284, 61.8350); + p3.curveTo(2.1120, 58.4438, 0.7540, 48.5122, 8.5710, 44.0484); + p3.closePath(); + + Path2D.Double p4 = new Path2D.Double(Path2D.WIND_NON_ZERO); + p4.moveTo(63.0123, 43.7896); + p4.curveTo(58.3009, 42.5272, 61.4219, 63.2802, 65.2549, 61.5775); + p4.curveTo(69.4712, 58.1859, 70.8292, 48.2535, 63.0123, 43.7896); + p4.closePath(); + p4.moveTo(47.0060, 28.3359); + p4.curveTo(55.1360, 26.9631, 61.9009, 31.7933, 61.6279, 40.6091); + p4.curveTo(61.3605, 43.9888, 44.0109, 28.8391, 47.0061, 28.3359); + p4.closePath(); + p4.moveTo(24.5436, 28.0771); + p4.curveTo(16.4130, 26.7043, 9.6488, 31.5356, 9.9218, 40.3506); + p4.curveTo(10.1892, 43.7301, 27.5388, 28.5804, 24.5436, 28.0771); + p4.closePath(); + p4.moveTo(36.2218, 26.0212); + p4.curveTo(31.3695, 25.8950, 26.7127, 29.6225, 26.7014, 31.7845); + p4.curveTo(26.6879, 34.4115, 30.5378, 37.1013, 36.2548, 37.1695); + p4.curveTo(42.0931, 37.2113, 45.8185, 35.0165, 45.8372, 32.3055); + p4.curveTo(45.8585, 29.2338, 40.5274, 25.9737, 36.2217, 26.0211); + p4.closePath(); + p4.moveTo(36.5181, 79.8920); + p4.curveTo(40.7486, 79.7073, 46.4253, 81.2546, 46.4364, 83.3072); + p4.curveTo(46.5066, 85.3003, 41.2882, 89.8037, 36.2375, 89.7166); + p4.curveTo(31.0069, 89.9423, 25.8778, 85.4319, 25.9449, 83.8686); + p4.curveTo(25.8666, 81.5764, 32.3140, 79.7867, 36.5181, 79.8919); + p4.closePath(); + p4.moveTo(20.8923, 67.7271); + p4.curveTo(23.9043, 71.3559, 25.2773, 77.7311, 22.7637, 79.6104); + p4.curveTo(20.3857, 81.0450, 14.6109, 80.4542, 10.5063, 74.5576); + p4.curveTo(7.7381, 69.6096, 8.0949, 64.5746, 10.0385, 63.0956); + p4.curveTo(12.9450, 61.3250, 17.4358, 63.7165, 20.8925, 67.7271); + p4.closePath(); + p4.moveTo(51.5410, 66.5770); + p4.curveTo(48.2822, 70.3940, 46.4676, 77.3560, 48.8449, 79.5982); + p4.curveTo(51.1179, 81.3402, 57.2199, 81.0967, 61.7272, 74.8425); + p4.curveTo(65.0002, 70.6421, 63.9035, 63.6272, 62.0340, 61.7643); + p4.curveTo(59.2569, 59.6163, 55.2701, 62.3653, 51.5410, 66.5759); + p4.closePath(); + + Area area = new Area(p0); + area.subtract(new Area(p1)); + area.subtract(new Area(p2)); + area.subtract(new Area(p3)); + area.subtract(new Area(p4)); + + return area; + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardPainter.java b/diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardPainter.java new file mode 100644 index 000000000..9aaf434cc --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/MakerBoardPainter.java @@ -0,0 +1,242 @@ +/* + * + * 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; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.VerticalAlignment; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +/** + * Painters for the discrete parts populated onto a maker board -- chips, connectors, antennas, + * buttons and mounting holes. They are pure functions of the geometry they are handed, drawn in + * whatever coordinate space {@code g2d} is currently in, so a board calls them while its own + * rotation is still applied. + * + * @author Branislav Stojkovic + */ +public class MakerBoardPainter { + + private MakerBoardPainter() {} + + /** + * Helper to draw a mounting hole. + */ + public static void drawMountingHole(Graphics2D g2d, double cx, double cy, double diameter) { + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter)); + g2d.setColor(Color.DARK_GRAY); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Ellipse2D.Double(cx - diameter / 2.0, cy - diameter / 2.0, diameter, diameter)); + } + + /** + * Helper to draw an IC chip on the board. + */ + public static void drawChip(Graphics2D g2d, double x, double y, double w, double h, String label) { + g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 4, 4)); + g2d.setColor(AbstractMakerBoard.IC_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 4, 4)); + + // pin 1 dot + g2d.setColor(AbstractMakerBoard.PIN_MARKER_COLOR); + g2d.fill(new Ellipse2D.Double(x + 3, y + 3, 3, 3)); + + if (label != null && !label.isEmpty() && w > 20 && h > 10) { + g2d.setColor(AbstractMakerBoard.IC_TEXT_COLOR); + g2d.setFont(AbstractMakerBoard.SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, label, x + w / 2.0, y + h / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + } + + /** + * Helper to draw an FPC / ribbon cable connector (e.g. MIPI CSI/DSI, PCIe FPC). + */ + public static void drawFpcConnector(Graphics2D g2d, double x, double y, double w, double h, boolean vertical, String label) { + g2d.setColor(AbstractMakerBoard.IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 2, 2)); + g2d.setColor(AbstractMakerBoard.IC_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 2, 2)); + + // Inner slot / latch bar + g2d.setColor(AbstractMakerBoard.HEADER_BODY_COLOR.darker()); + if (vertical) { + double slotW = Math.max(1.5, w * 0.35); + double slotH = Math.max(2.0, h - 4); + g2d.fill(new Rectangle2D.Double(x + (w - slotW) / 2.0, y + 2, slotW, slotH)); + } else { + double slotW = Math.max(2.0, w - 4); + double slotH = Math.max(1.5, h * 0.35); + g2d.fill(new Rectangle2D.Double(x + 2, y + (h - slotH) / 2.0, slotW, slotH)); + } + + if (label != null && !label.isEmpty()) { + g2d.setColor(Color.WHITE); + g2d.setFont(AbstractMakerBoard.SILK_FONT_SMALL); + if (w >= 20 && h >= 10) { + StringUtils.drawCenteredText(g2d, label, x + w / 2.0, y + h / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + } + } + + /** + * Helper to draw a standard Micro-USB connector. + */ + public static void drawMicroUsb(Graphics2D g2d, double x, double y, double w, double h, String label) { + drawMetalConnector(g2d, x, y, w, h, label); + } + + /** + * Helper to draw a standard USB Type-C connector. + */ + public static void drawUsbC(Graphics2D g2d, double x, double y, double w, double h, String label) { + drawMetalConnector(g2d, x, y, w, h, label); + } + + /** + * Helper to draw a standard USB Type-A connector. + */ + public static void drawUsbA(Graphics2D g2d, double x, double y, double w, double h, String label) { + drawMetalConnector(g2d, x, y, w, h, label); + } + + /** + * Helper to draw a standard USB Type-B connector. + */ + public static void drawUsbB(Graphics2D g2d, double x, double y, double w, double h, String label) { + drawMetalConnector(g2d, x, y, w, h, label); + } + + /** + * Helper to draw a metal connector / shield (e.g. RF shield cans, SD card slots, HDMI). + */ + public static void drawMetalConnector(Graphics2D g2d, double x, double y, double w, double h, String label) { + g2d.setColor(AbstractMakerBoard.USB_METAL_COLOR); + g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 3, 3)); + g2d.setColor(AbstractMakerBoard.METAL_SHIELD_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 3, 3)); + if (label != null && !label.isEmpty()) { + g2d.setColor(AbstractMakerBoard.METAL_LABEL_COLOR); + g2d.setFont(AbstractMakerBoard.SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, label, x + w / 2.0, y + h / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + } + + /** + * Helper to draw a standard ESP-style PCB meander antenna with a dark (#1E1E1E) substrate rectangle underneath. + * + * @param g2d Graphics2D context + * @param x top-left X coordinate of the antenna substrate rectangle + * @param y top-left Y coordinate of the antenna substrate rectangle + * @param width width of the antenna substrate rectangle (e.g. 15.0mm, matching main chip width) + * @param height height of the antenna substrate rectangle (e.g. 7.0mm) + */ + public static void drawPcbAntenna(Graphics2D g2d, double x, double y, double width, double height) { + // Dark rectangle underneath (#1E1E1E) + g2d.setColor(AbstractMakerBoard.ANTENNA_BG_COLOR); + g2d.fill(new Rectangle2D.Double(x, y, width, height)); + + // Serpentine antenna trace (gold/copper) + g2d.setColor(AbstractMakerBoard.ANTENNA_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + Path2D.Double antPath = new Path2D.Double(); + + if (width >= height) { + double antPadX = x + new Size(1.5d, SizeUnit.mm).convertToPixels(); + double antPadW = width - new Size(3.0d, SizeUnit.mm).convertToPixels(); + double traceH = new Size(4.1d, SizeUnit.mm).convertToPixels(); + double marginY = height > traceH ? (height - traceH) / 2.0 : height * 0.15; + double antTopY = y + marginY; + double antMidY = y + height - marginY; + 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); + } else { + double antPadY = y + new Size(1.5d, SizeUnit.mm).convertToPixels(); + double antPadH = height - new Size(3.0d, SizeUnit.mm).convertToPixels(); + double traceW = new Size(4.1d, SizeUnit.mm).convertToPixels(); + double marginX = width > traceW ? (width - traceW) / 2.0 : width * 0.15; + double antLeftX = x + marginX; + double antRightX = x + width - marginX; + antPath.moveTo(antRightX, antPadY); + antPath.lineTo(antLeftX, antPadY); + antPath.lineTo(antLeftX, antPadY + antPadH * 0.25); + antPath.lineTo(antRightX, antPadY + antPadH * 0.25); + antPath.lineTo(antRightX, antPadY + antPadH * 0.50); + antPath.lineTo(antLeftX, antPadY + antPadH * 0.50); + antPath.lineTo(antLeftX, antPadY + antPadH * 0.75); + antPath.lineTo(antRightX, antPadY + antPadH * 0.75); + antPath.lineTo(antRightX, antPadY + antPadH); + antPath.lineTo(antLeftX, antPadY + antPadH); + } + g2d.draw(antPath); + } + + /** + * Helper to draw a standard ESP-style PCB meander antenna using default 15.0mm x 7.0mm dimensions. + */ + public static void drawPcbAntenna(Graphics2D g2d, double x, double y) { + drawPcbAntenna(g2d, x, y, AbstractMakerBoard.ANTENNA_WIDTH.convertToPixels(), AbstractMakerBoard.ANTENNA_LENGTH.convertToPixels()); + } + + /** + * Helper to draw a standard SMD tactile push button (housing + circular actuator). + * + * @param g2d Graphics2D context + * @param x Top-left X coordinate of the button body + * @param y Top-left Y coordinate of the button body + * @param w Width of the button body + * @param h Height of the button body + */ + public static void drawButton(Graphics2D g2d, double x, double y, double w, double h) { + g2d.setColor(AbstractMakerBoard.BUTTON_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(x, y, w, h, 2, 2)); + g2d.setColor(AbstractMakerBoard.BUTTON_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(x, y, w, h, 2, 2)); + + double actuatorD = Math.min(w, h) * 0.55; + g2d.setColor(AbstractMakerBoard.BUTTON_ACTUATOR_COLOR); + g2d.fill(new Ellipse2D.Double(x + (w - actuatorD) / 2.0, y + (h - actuatorD) / 2.0, actuatorD, actuatorD)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/CharacterLCD.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/CharacterLCD.java new file mode 100644 index 000000000..c1c05ac70 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/CharacterLCD.java @@ -0,0 +1,281 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Character LCD (16x2 / 20x4)", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "HD44780-Compatible Character LCD Display (Parallel / I2C Backpack)", +// instanceNamePrefix = "LCD", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class CharacterLCD extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum LCDSize { + _16x2("16x2 (80x36mm)", 16, 2, 80.0, 36.0), + _20x4("20x4 (98x60mm)", 20, 4, 98.0, 60.0); + + private final String label; + private final int cols; + private final int rows; + private final double widthMm; + private final double heightMm; + + LCDSize(String label, int cols, int rows, double widthMm, double heightMm) { + this.label = label; + this.cols = cols; + this.rows = rows; + this.widthMm = widthMm; + this.heightMm = heightMm; + } + + @Override public String toString() { return label; } + public int getCols() { return cols; } + public int getRows() { return rows; } + public double getWidthMm() { return widthMm; } + public double getHeightMm() { return heightMm; } + } + + public enum LCDInterface { + I2C_Backpack("I2C Backpack (4-Pin)"), + Parallel_16Pin("Parallel HD44780 (16-Pin)"); + + private final String label; + LCDInterface(String label) { this.label = label; } + @Override public String toString() { return label; } + } + + public static Color PCB_GREEN = Color.decode("#1B5E20"); + public static Color SCREEN_BG = Color.decode("#1E88E5"); + public static Color SCREEN_TEXT = Color.decode("#FFFFFF"); + public static Color BEZEL_COLOR = Color.decode("#212121"); + + public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SDA", "SCL"}; + public static final String[] PIN_NAMES_PARALLEL = new String[] { + "VSS (GND)", "VDD (+5V)", "V0 (Contrast)", "RS", "RW", "E", + "D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", + "A (Backlight +)", "K (Backlight -)" + }; + + private LCDSize lcdSize = LCDSize._16x2; + private LCDInterface lcdInterface = LCDInterface.I2C_Backpack; + private Color screenColor = SCREEN_BG; + + public CharacterLCD() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @EditableProperty(name = "Size") + public LCDSize getLcdSize() { + return lcdSize; + } + + public void setLcdSize(LCDSize lcdSize) { + this.lcdSize = lcdSize; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Interface") + public LCDInterface getLcdInterface() { + return lcdInterface; + } + + public void setLcdInterface(LCDInterface lcdInterface) { + this.lcdInterface = lcdInterface; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Backlight Color") + public Color getScreenColor() { + return screenColor; + } + + public void setScreenColor(Color screenColor) { + this.screenColor = screenColor; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (lcdInterface == LCDInterface.I2C_Backpack) { + if (index >= 0 && index < PIN_NAMES_I2C.length) return PIN_NAMES_I2C[index]; + } else { + if (index >= 0 && index < PIN_NAMES_PARALLEL.length) return PIN_NAMES_PARALLEL[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + int pinCount = (lcdInterface == LCDInterface.I2C_Backpack) ? 4 : 16; + double[][] relativeOffsets = new double[pinCount][2]; + + for (int i = 0; i < pinCount; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels(); + double boardH = new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels(); + double boardX = x - 60; + double boardY = y - 20; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels(); + double boardH = new Size(lcdSize.getHeightMm(), SizeUnit.mm).convertToPixels(); + + double boardX = x - 60; + double boardY = y - 20; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes in corners + MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + boardH - 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + boardH - 20, 20); + + // Metal Bezel around screen + double bezelMarginX = (lcdSize == LCDSize._16x2) ? 45 : 40; + double bezelW = boardW - 2 * bezelMarginX; + double bezelX = boardX + bezelMarginX; + double bezelY = boardY + 45; + double bezelH = boardH - 65; + + g2d.setColor(BEZEL_COLOR); + g2d.fill(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 6, 6)); + + // LCD Screen area + double screenMargin = 12; + double screenW = bezelW - 2 * screenMargin; + double screenH = bezelH - 2 * screenMargin; + double screenX = bezelX + screenMargin; + double screenY = bezelY + screenMargin; + + g2d.setColor(screenColor); + g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH)); + + // Render character boxes + g2d.setColor(SCREEN_TEXT); + g2d.setFont(new Font("Monospaced", Font.BOLD, (lcdSize == LCDSize._16x2) ? 14 : 11)); + String line1 = (lcdSize == LCDSize._16x2) ? "HELLO WORLD! 16x2" : "DIYLC LCD MODULE"; + String line2 = (lcdSize == LCDSize._16x2) ? "DIY LAYOUT CREATOR" : "20x4 CHARACTER LCD"; + StringUtils.drawCenteredText(g2d, line1, screenX + screenW / 2.0, screenY + screenH / 3.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, line2, screenX + screenW / 2.0, screenY + screenH * 2.0 / 3.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + + // Screen + g2d.setColor(SCREEN_BG); + g2d.fillRect(6, 10, width - 12, height - 20); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 6)); + StringUtils.drawCenteredText(g2d, "LCD", width / 2, height / 2 + 1, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/LEDMatrix8x8.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/LEDMatrix8x8.java new file mode 100644 index 000000000..b1bb2e2f1 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/LEDMatrix8x8.java @@ -0,0 +1,235 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "8x8 LED Matrix (MAX7219)", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "MAX7219 Dot LED Matrix Display Module (Cascadable SPI)", +// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class LEDMatrix8x8 extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_BLUE = Color.decode("#0055A5"); + public static Color MATRIX_BODY = Color.decode("#1A1A1A"); + public static Color LED_RED = Color.decode("#E53935"); + public static Color LED_OFF = Color.decode("#333333"); + + public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + // Input Header (0..4) + "VCC_IN", "GND_IN", "DIN", "CS_IN", "CLK_IN", + // Output Header (5..9) + "VCC_OUT", "GND_OUT", "DOUT", "CS_OUT", "CLK_OUT" + }; + + private Color dotColor = LED_RED; + + public LEDMatrix8x8() { + super(); + this.bodyColor = PCB_BLUE; + updateControlPoints(); + } + + @EditableProperty(name = "Dot Color") + public Color getDotColor() { + return dotColor; + } + + public void setDotColor(Color dotColor) { + this.dotColor = dotColor; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + double[][] relativeOffsets = new double[10][2]; + + // Input header (0..4) at bottom + for (int i = 0; i < 5; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + // Output header (5..9) at top + double topY = -new Size(44.0d, SizeUnit.mm).convertToPixels(); + for (int i = 0; i < 5; i++) { + relativeOffsets[5 + i][0] = i * spacing; + relativeOffsets[5 + i][1] = topY; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 20; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 20; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 8x8 LED Matrix block (square 32mm) + double matrixSize = boardW - 10; + double matrixX = boardX + 5; + double matrixY = boardY + 30; + + g2d.setColor(MATRIX_BODY); + g2d.fill(new RoundRectangle2D.Double(matrixX, matrixY, matrixSize, matrixSize, 6, 6)); + + // Draw 64 circular LED dots + double dotPitch = matrixSize / 8.0; + double dotR = dotPitch * 0.7; + for (int row = 0; row < 8; row++) { + for (int col = 0; col < 8; col++) { + double dx = matrixX + col * dotPitch + (dotPitch - dotR) / 2.0; + double dy = matrixY + row * dotPitch + (dotPitch - dotR) / 2.0; + // create a demo heart or smile pattern + boolean on = (row == 0 && (col == 2 || col == 5)) || + (row == 1 && (col == 1 || col == 3 || col == 4 || col == 6)) || + (row == 2 && col >= 1 && col <= 6) || + (row == 3 && col >= 1 && col <= 6) || + (row == 4 && col >= 2 && col <= 5) || + (row == 5 && col >= 3 && col <= 4); + g2d.setColor(on ? dotColor : LED_OFF); + g2d.fill(new Ellipse2D.Double(dx, dy, dotR, dotR)); + } + } + + // MAX7219 IC + MakerBoardPainter.drawChip(g2d, boardX + 20, matrixY + matrixSize + 15, boardW - 40, 45, "MAX7219"); + + // Silkscreen + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "IN", x + 2 * PIN_SPACING.convertToPixels(), y - 16, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT", x + 2 * PIN_SPACING.convertToPixels(), y - boardH + 34, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(PCB_BLUE); + g2d.fill(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3)); + g2d.setColor(PCB_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3)); + + // Matrix block + g2d.setColor(MATRIX_BODY); + g2d.fillRect(6, 4, width - 12, height - 12); + + // 4x4 sample dots + g2d.setColor(LED_RED); + for (int r = 0; r < 4; r++) { + for (int c = 0; c < 4; c++) { + g2d.fillOval(9 + c * 4, 7 + r * 4, 2, 2); + } + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/OLEDDisplay.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/OLEDDisplay.java new file mode 100644 index 000000000..5b6428918 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/OLEDDisplay.java @@ -0,0 +1,228 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "0.96\" OLED Display (SSD1306)", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "0.96\" Monochrome 128x64 OLED Display Module (I2C / SPI)", +// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class OLEDDisplay extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum OLEDInterface { + I2C_4Pin("I2C (4-Pin: GND, VCC, SCL, SDA)"), + SPI_7Pin("SPI (7-Pin: GND, VCC, D0, D1, RES, DC, CS)"); + + private final String label; + OLEDInterface(String label) { this.label = label; } + @Override public String toString() { return label; } + } + + public static Color OLED_BLUE = Color.decode("#004488"); + public static Color GLASS_COLOR = Color.decode("#0D1B2A"); + public static Color PIXEL_BLUE = Color.decode("#00D4FF"); + public static Color PIXEL_YELLOW = Color.decode("#FFD700"); + + public static Size BOARD_SIZE = new Size(27.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SCL", "SDA"}; + public static final String[] PIN_NAMES_SPI = new String[] {"GND", "VCC", "D0 (CLK)", "D1 (MOSI)", "RES", "DC", "CS"}; + + private OLEDInterface oledInterface = OLEDInterface.I2C_4Pin; + + public OLEDDisplay() { + super(); + this.bodyColor = OLED_BLUE; + updateControlPoints(); + } + + @EditableProperty(name = "Interface") + public OLEDInterface getOledInterface() { + return oledInterface; + } + + public void setOledInterface(OLEDInterface oledInterface) { + this.oledInterface = oledInterface; + updateControlPoints(); + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (oledInterface == OLEDInterface.I2C_4Pin) { + if (index >= 0 && index < PIN_NAMES_I2C.length) return PIN_NAMES_I2C[index]; + } else { + if (index >= 0 && index < PIN_NAMES_SPI.length) return PIN_NAMES_SPI[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7; + double[][] relativeOffsets = new double[count][2]; + + for (int i = 0; i < count; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardSizePx = BOARD_SIZE.convertToPixels(); + int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7; + double boardX = x - (boardSizePx - (count - 1) * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - 14; + return new RoundRectangle2D.Double(boardX, boardY, boardSizePx, boardSizePx, 8, 8); + } + + @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 boardSizePx = BOARD_SIZE.convertToPixels(); + int count = (oledInterface == OLEDInterface.I2C_4Pin) ? 4 : 7; + double boardX = x - (boardSizePx - (count - 1) * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - 14; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Corner mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardSizePx - 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 16, boardY + boardSizePx - 16, 16); + + // Glass OLED Panel + double glassMarginX = 18; + double glassW = boardSizePx - 2 * glassMarginX; + double glassH = boardSizePx - 65; + double glassX = boardX + glassMarginX; + double glassY = boardY + 45; + + g2d.setColor(GLASS_COLOR); + g2d.fill(new RoundRectangle2D.Double(glassX, glassY, glassW, glassH, 4, 4)); + g2d.setColor(Color.decode("#334E68")); + g2d.draw(new RoundRectangle2D.Double(glassX, glassY, glassW, glassH, 4, 4)); + + // Display demo graphics (yellow top banner + blue body) + g2d.setColor(PIXEL_YELLOW); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "SSD1306 128x64", glassX + glassW / 2.0, glassY + 15, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + g2d.setColor(PIXEL_BLUE); + StringUtils.drawCenteredText(g2d, "OLED DISPLAY", glassX + glassW / 2.0, glassY + glassH / 2.0 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(OLED_BLUE); + g2d.fill(new RoundRectangle2D.Double(4, 4, width - 8, height - 8, 3, 3)); + g2d.setColor(OLED_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(4, 4, width - 8, height - 8, 3, 3)); + + // Screen + g2d.setColor(GLASS_COLOR); + g2d.fillRect(7, 12, width - 14, height - 18); + + g2d.setColor(PIXEL_BLUE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "OLED", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Pins + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 4; i++) { + g2d.fillRect(10 + i * 3, 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/SevenSegmentDisplay.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/SevenSegmentDisplay.java new file mode 100644 index 000000000..330a208a7 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/SevenSegmentDisplay.java @@ -0,0 +1,511 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "7-Segment Display", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "7-Segment LED Display (1-Digit, 4-Digit, or TM1637 I2C Driver Module)", +// instanceNamePrefix = "DISP", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class SevenSegmentDisplay extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum DisplayType { + SingleDigit_10Pin("1-Digit 0.56\" (10-Pin DIP)"), + FourDigit_12Pin("4-Digit Bare (12-Pin DIP)"), + TM1637_Module_4Pin("4-Digit TM1637 Module (4-Pin)"); + + private final String label; + DisplayType(String label) { this.label = label; } + @Override public String toString() { return label; } + } + + public static Color BODY_BLACK = Color.decode("#1C1C1C"); + public static Color FACE_BLACK = Color.decode("#111111"); + public static Color FACE_BORDER = Color.decode("#333333"); + public static Color LED_RED = Color.decode("#E53935"); + public static Color LED_OFF = Color.decode("#2E2E2E"); + public static Color LED_OFF_BORDER = Color.decode("#222222"); + + // Exact physical proportions from datasheet + public static Size DIGIT_HEIGHT = new Size(14.2d, SizeUnit.mm); // 0.56" + public static Size DIGIT_WIDTH = new Size(8.1d, SizeUnit.mm); + public static Size DIGIT_PITCH = new Size(12.7d, SizeUnit.mm); + public static Size SEGMENT_THICKNESS = new Size(1.4d, SizeUnit.mm); + public static Size SEGMENT_GAP = new Size(0.35d, SizeUnit.mm); + public static double SLANT_DEGREES = 8.0d; + + public static final String[] PIN_NAMES_1DIGIT = new String[] { + "e", "d", "COM1", "c", "DP", "b", "a", "COM2", "f", "g" + }; + + public static final String[] PIN_NAMES_4DIGIT = new String[] { + "D1", "a", "f", "D2", "D3", "b", "e", "d", "DP", "c", "g", "D4" + }; + + public static final String[] PIN_NAMES_TM1637 = new String[] {"CLK", "DIO", "VCC", "GND"}; + + private DisplayType displayType = DisplayType.SingleDigit_10Pin; + private Color ledColor = LED_RED; + + public SevenSegmentDisplay() { + super(); + this.bodyColor = BODY_BLACK; + updateControlPoints(); + } + + @EditableProperty(name = "Type") + public DisplayType getDisplayType() { + return displayType; + } + + public void setDisplayType(DisplayType displayType) { + this.displayType = displayType; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "LED Color") + public Color getLedColor() { + return ledColor; + } + + public void setLedColor(Color ledColor) { + this.ledColor = ledColor; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + switch (displayType) { + case SingleDigit_10Pin: + if (index >= 0 && index < PIN_NAMES_1DIGIT.length) return PIN_NAMES_1DIGIT[index]; + break; + case FourDigit_12Pin: + if (index >= 0 && index < PIN_NAMES_4DIGIT.length) return PIN_NAMES_4DIGIT[index]; + break; + case TM1637_Module_4Pin: + if (index >= 0 && index < PIN_NAMES_TM1637.length) return PIN_NAMES_TM1637[index]; + break; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + if (displayType == DisplayType.SingleDigit_10Pin) { + double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); // 120px + double[][] relativeOffsets = new double[10][2]; + for (int i = 0; i < 5; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + for (int i = 0; i < 5; i++) { + relativeOffsets[5 + i][0] = (4 - i) * spacing; + relativeOffsets[5 + i][1] = -rowSpacing; + } + rotatePoints(firstPoint, relativeOffsets); + } else if (displayType == DisplayType.FourDigit_12Pin) { + double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); + double[][] relativeOffsets = new double[12][2]; + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + for (int i = 0; i < 6; i++) { + relativeOffsets[6 + i][0] = (5 - i) * spacing; + relativeOffsets[6 + i][1] = -rowSpacing; + } + rotatePoints(firstPoint, relativeOffsets); + } else { + // TM1637 Module 4-pin + double[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + rotatePoints(firstPoint, relativeOffsets); + } + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW; + double boardH; + double boardX; + double boardY; + + if (displayType == DisplayType.SingleDigit_10Pin) { + boardW = new Size(12.6d, SizeUnit.mm).convertToPixels(); + boardH = new Size(19.0d, SizeUnit.mm).convertToPixels(); + double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); + boardX = x - (boardW - 4 * PIN_SPACING.convertToPixels()) / 2.0; + boardY = y - rowSpacing - (boardH - rowSpacing) / 2.0; + } else if (displayType == DisplayType.FourDigit_12Pin) { + boardW = new Size(50.3d, SizeUnit.mm).convertToPixels(); + boardH = new Size(19.0d, SizeUnit.mm).convertToPixels(); + double rowSpacing = new Size(0.6d, SizeUnit.in).convertToPixels(); + boardX = x - (boardW - 5 * PIN_SPACING.convertToPixels()) / 2.0; + boardY = y - rowSpacing - (boardH - rowSpacing) / 2.0; + } else { + boardW = new Size(42.0d, SizeUnit.mm).convertToPixels(); + boardH = new Size(24.0d, SizeUnit.mm).convertToPixels(); + boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0; + boardY = y - (boardH - PIN_SPACING.convertToPixels()) / 2.0; + } + + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + Shape boardShape = getBodyShape(); + Rectangle2D bounds = boardShape.getBounds2D(); + double boardX = bounds.getX(); + double boardY = bounds.getY(); + double boardW = bounds.getWidth(); + double boardH = bounds.getHeight(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + if (displayType == DisplayType.SingleDigit_10Pin) { + // Dark display face + g2d.setColor(FACE_BLACK); + g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); + g2d.setColor(FACE_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); + + // Single 0.56" 7-segment digit with exact datasheet dimensions + double dw = DIGIT_WIDTH.convertToPixels(); + double dh = DIGIT_HEIGHT.convertToPixels(); + double dx = boardX + (boardW - dw) / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels(); + double dy = boardY + (boardH - dh) / 2.0; + drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor); + + } else if (displayType == DisplayType.FourDigit_12Pin) { + // Dark display face + g2d.setColor(FACE_BLACK); + g2d.fill(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); + g2d.setColor(FACE_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(boardX + 3, boardY + 3, boardW - 6, boardH - 6, 3, 3)); + + // 4 digits across with 12.70mm center-to-center pitch + double dw = DIGIT_WIDTH.convertToPixels(); + double dh = DIGIT_HEIGHT.convertToPixels(); + double pitch = DIGIT_PITCH.convertToPixels(); + double firstCenterX = boardX + new Size(6.1d, SizeUnit.mm).convertToPixels(); + double dy = boardY + (boardH - dh) / 2.0; + + for (int d = 0; d < 4; d++) { + double centerX = firstCenterX + d * pitch; + double dx = centerX - dw / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels(); + drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor); + } + + // Center colon dots between digits 1 & 2 + double colonX = boardX + boardW / 2.0; + double colonY1 = dy + dh * 0.35; + double colonY2 = dy + dh * 0.65; + g2d.setColor(ledColor); + g2d.fill(new Ellipse2D.Double(colonX - 2.0, colonY1 - 2.0, 4.0, 4.0)); + g2d.fill(new Ellipse2D.Double(colonX - 2.0, colonY2 - 2.0, 4.0, 4.0)); + + } else { + // TM1637 4-Digit Display Module + MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + 14, 12); + MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + boardH - 14, 12); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 14, boardY + 14, 12); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 14, boardY + boardH - 14, 12); + + // Display Bezel (Center) + double bezelW = new Size(30.0d, SizeUnit.mm).convertToPixels(); + double bezelH = new Size(14.0d, SizeUnit.mm).convertToPixels(); + double bezelX = boardX + (boardW - bezelW) / 2.0 - 6.0; + double bezelY = boardY + (boardH - bezelH) / 2.0; + + g2d.setColor(FACE_BLACK); + g2d.fill(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 4, 4)); + g2d.setColor(FACE_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(bezelX, bezelY, bezelW, bezelH, 4, 4)); + + // 4 Digits inside 0.36" bezel + double dw = new Size(5.5d, SizeUnit.mm).convertToPixels(); + double dh = new Size(9.2d, SizeUnit.mm).convertToPixels(); + double pitch = new Size(7.62d, SizeUnit.mm).convertToPixels(); + double firstCenterX = bezelX + (bezelW - 3 * pitch) / 2.0; + double dy = bezelY + (bezelH - dh) / 2.0; + + for (int d = 0; d < 4; d++) { + double centerX = firstCenterX + d * pitch; + double dx = centerX - dw / 2.0 - 0.5 * SEGMENT_THICKNESS.convertToPixels() * 0.65; + drawSevenSegmentDigit(g2d, dx, dy, dw, dh, "8.", ledColor); + } + + // Center colon dots + double colonX = bezelX + bezelW / 2.0; + double colonY1 = bezelY + bezelH / 2.0 - 8.0; + double colonY2 = bezelY + bezelH / 2.0 + 6.0; + g2d.setColor(ledColor); + g2d.fill(new Ellipse2D.Double(colonX - 1.5, colonY1 - 1.5, 3.0, 3.0)); + g2d.fill(new Ellipse2D.Double(colonX - 1.5, colonY2 - 1.5, 3.0, 3.0)); + } + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + private static final int SEG_A = 1 << 0; + private static final int SEG_B = 1 << 1; + private static final int SEG_C = 1 << 2; + private static final int SEG_D = 1 << 3; + private static final int SEG_E = 1 << 4; + private static final int SEG_F = 1 << 5; + private static final int SEG_G = 1 << 6; + private static final int SEG_DP = 1 << 7; + + private static int getSegmentMask(char c) { + switch (c) { + case '0': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F; + case '1': return SEG_B | SEG_C; + case '2': return SEG_A | SEG_B | SEG_G | SEG_E | SEG_D; + case '3': return SEG_A | SEG_B | SEG_G | SEG_C | SEG_D; + case '4': return SEG_F | SEG_G | SEG_B | SEG_C; + case '5': return SEG_A | SEG_F | SEG_G | SEG_C | SEG_D; + case '6': return SEG_A | SEG_F | SEG_E | SEG_D | SEG_C | SEG_G; + case '7': return SEG_A | SEG_B | SEG_C; + case '8': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G; + case '9': return SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G; + case 'A': case 'a': return SEG_A | SEG_B | SEG_C | SEG_E | SEG_F | SEG_G; + case 'B': case 'b': return SEG_C | SEG_D | SEG_E | SEG_F | SEG_G; + case 'C': case 'c': return SEG_A | SEG_D | SEG_E | SEG_F; + case 'D': case 'd': return SEG_B | SEG_C | SEG_D | SEG_E | SEG_G; + case 'E': case 'e': return SEG_A | SEG_D | SEG_E | SEG_F | SEG_G; + case 'F': case 'f': return SEG_A | SEG_E | SEG_F | SEG_G; + case '-': return SEG_G; + case '.': return SEG_DP; + default: return SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G; + } + } + + /** + * Draws an authentic 7-segment digit matching exact engineering drawing specifications: + * - 8.0° italic forward slant + * - Realistic polygon segment geometry (trapezoids/hexagons with clean junction bevels) + * - 1.40mm segment thickness, 0.35mm inter-segment gaps without overlap + * - Unlit segments rendered in dim silhouette and active segments lit in onColor + * - 1.40mm Decimal point circle at bottom-right + */ + private void drawSevenSegmentDigit(Graphics2D g2d, double x, double y, double w, double h, String charToDisplay, Color onColor) { + AffineTransform orig = g2d.getTransform(); + + // Exact 8.0° forward italic slant from engineering drawing + g2d.translate(x + w / 2.0, y + h / 2.0); + g2d.shear(-Math.tan(Math.toRadians(SLANT_DEGREES)), 0); + g2d.translate(-w / 2.0, -h / 2.0); + + double t = Math.max(3.0, SEGMENT_THICKNESS.convertToPixels() * (h / DIGIT_HEIGHT.convertToPixels())); + double g = Math.max(1.0, SEGMENT_GAP.convertToPixels() * (h / DIGIT_HEIGHT.convertToPixels())); + double segW = w - t * 0.8; + + double x0 = 0; + double x1 = segW; + double y0 = 0; + double ym = h / 2.0; + double y1 = h; + + // Segment A (Top horizontal) + Path2D pathA = new Path2D.Double(); + pathA.moveTo(x0 + t * 0.6 + g, y0); + pathA.lineTo(x1 - t * 0.6 - g, y0); + pathA.lineTo(x1 - g, y0 + t * 0.45); + pathA.lineTo(x1 - t - g, y0 + t); + pathA.lineTo(x0 + t + g, y0 + t); + pathA.lineTo(x0 + g, y0 + t * 0.45); + pathA.closePath(); + + // Segment B (Top-Right vertical) + Path2D pathB = new Path2D.Double(); + pathB.moveTo(x1, y0 + t * 0.5 + g); + pathB.lineTo(x1 - t * 0.35, y0 + g); + pathB.lineTo(x1 - t, y0 + t + 1.4 * g); + pathB.lineTo(x1 - t, ym - t * 0.5 - 0.8 * g); + pathB.lineTo(x1, ym - 0.8 * g); + pathB.closePath(); + + // Segment C (Bottom-Right vertical) + Path2D pathC = new Path2D.Double(); + pathC.moveTo(x1, ym + 0.8 * g); + pathC.lineTo(x1 - t, ym + t * 0.5 + 0.8 * g); + pathC.lineTo(x1 - t, y1 - t - 1.4 * g); + pathC.lineTo(x1 - t * 0.35, y1 - g); + pathC.lineTo(x1, y1 - t * 0.5 - g); + pathC.closePath(); + + // Segment D (Bottom horizontal) + Path2D pathD = new Path2D.Double(); + pathD.moveTo(x0 + t + g, y1 - t); + pathD.lineTo(x1 - t - g, y1 - t); + pathD.lineTo(x1 - g, y1 - t * 0.45); + pathD.lineTo(x1 - t * 0.6 - g, y1); + pathD.lineTo(x0 + t * 0.6 + g, y1); + pathD.lineTo(x0 + g, y1 - t * 0.45); + pathD.closePath(); + + // Segment E (Bottom-Left vertical) + Path2D pathE = new Path2D.Double(); + pathE.moveTo(x0, ym + 0.8 * g); + pathE.lineTo(x0 + t, ym + t * 0.5 + 0.8 * g); + pathE.lineTo(x0 + t, y1 - t - 1.4 * g); + pathE.lineTo(x0 + t * 0.35, y1 - g); + pathE.lineTo(x0, y1 - t * 0.5 - g); + pathE.closePath(); + + // Segment F (Top-Left vertical) + Path2D pathF = new Path2D.Double(); + pathF.moveTo(x0, y0 + t * 0.5 + g); + pathF.lineTo(x0 + t * 0.35, y0 + g); + pathF.lineTo(x0 + t, y0 + t + 1.4 * g); + pathF.lineTo(x0 + t, ym - t * 0.5 - 0.8 * g); + pathF.lineTo(x0, ym - 0.8 * g); + pathF.closePath(); + + // Segment G (Middle horizontal hexagon) + Path2D pathG = new Path2D.Double(); + pathG.moveTo(x0 + 1.4 * g, ym); + pathG.lineTo(x0 + t * 0.6 + 1.4 * g, ym - t * 0.48); + pathG.lineTo(x1 - t * 0.6 - 1.4 * g, ym - t * 0.48); + pathG.lineTo(x1 - 1.4 * g, ym); + pathG.lineTo(x1 - t * 0.6 - 1.4 * g, ym + t * 0.48); + pathG.lineTo(x0 + t * 0.6 + 1.4 * g, ym + t * 0.48); + pathG.closePath(); + + // Decimal Point (DP) + double dpD = t * 0.95; + Shape dpShape = new Ellipse2D.Double(x1 + t * 0.25, y1 - dpD, dpD, dpD); + + Shape[] segments = new Shape[] { pathA, pathB, pathC, pathD, pathE, pathF, pathG }; + int mask = getSegmentMask(charToDisplay.isEmpty() ? '8' : charToDisplay.charAt(0)); + + // 1. Draw unlit segment silhouettes (dim background) + g2d.setColor(LED_OFF); + for (int i = 0; i < 7; i++) { + if ((mask & (1 << i)) == 0) { + g2d.fill(segments[i]); + } + } + if ((mask & SEG_DP) == 0 && !charToDisplay.contains(".")) { + g2d.fill(dpShape); + } + + // 2. Draw lit active segments + g2d.setColor(onColor); + for (int i = 0; i < 7; i++) { + if ((mask & (1 << i)) != 0) { + g2d.fill(segments[i]); + } + } + if ((mask & SEG_DP) != 0 || charToDisplay.contains(".")) { + g2d.fill(dpShape); + } + + g2d.setTransform(orig); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(BODY_BLACK); + g2d.fill(new RoundRectangle2D.Double(6, 3, width - 12, height - 6, 3, 3)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new RoundRectangle2D.Double(6, 3, width - 12, height - 6, 3, 3)); + + // Draw single stylized '8' + drawSevenSegmentDigit(g2d, 9, 5, width - 18, height - 10, "8.", LED_RED); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java new file mode 100644 index 000000000..43e32036b --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/TFTDisplay.java @@ -0,0 +1,200 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "TFT Touch Screen (ILI9341)", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "2.8\" 240x320 Color TFT LCD Display with SPI Interface, Touch, and SD Slot", +// instanceNamePrefix = "DISP", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class TFTDisplay extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color TFT_RED = Color.decode("#C0392B"); + public static Color SCREEN_BG = Color.decode("#111111"); + + public static Size BOARD_WIDTH = new Size(86.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(50.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "VCC", "GND", "CS", "RESET", "DC", "MOSI (SDI)", "SCK", "LED", "MISO (SDO)", + "T_CLK", "T_CS", "T_DIN", "T_DO", "T_IRQ" + }; + + public TFTDisplay() { + super(); + this.bodyColor = TFT_RED; + 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[][] relativeOffsets = new double[14][2]; + for (int i = 0; i < 14; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 70; + double boardY = y - 20; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 70; + double boardY = y - 20; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes in corners + MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + boardH - 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + boardH - 20, 20); + + // Color Screen Area + double screenMarginX = 35; + double screenW = boardW - 2 * screenMarginX - 25; + double screenH = boardH - 65; + double screenX = boardX + screenMarginX; + double screenY = boardY + 45; + + g2d.setColor(SCREEN_BG); + g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new Rectangle2D.Double(screenX, screenY, screenW, screenH)); + + // Display Demo UI + g2d.setColor(Color.decode("#2980B9")); + g2d.fillRect((int) screenX, (int) screenY, (int) screenW, 25); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "TFT 2.8\" 320x240 SPI", screenX + screenW / 2.0, screenY + 12, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // SD Card holder on right edge + MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - 28, boardY + 100, 36, 120, "SD"); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(TFT_RED); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(TFT_RED.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + + // Screen + g2d.setColor(SCREEN_BG); + g2d.fillRect(6, 9, width - 12, height - 16); + g2d.setColor(Color.decode("#2980B9")); + g2d.fillRect(6, 9, width - 12, 5); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "TFT", width / 2, height / 2 + 3, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java new file mode 100644 index 000000000..c360abc2d --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BRing.java @@ -0,0 +1,304 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "NeoPixel Ring (WS2812B)", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "Addressable RGB WS2812B NeoPixel Ring (12/16/24 LEDs)", +// instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class WS2812BRing extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum RingSize { + _12_LED("12 LEDs (37mm OD)", 12, 37.0, 27.0), + _16_LED("16 LEDs (44.5mm OD)", 16, 44.5, 32.0), + _24_LED("24 LEDs (66mm OD)", 24, 66.0, 52.0); + + private final String label; + private final int ledCount; + private final double outerDiameterMm; + private final double innerDiameterMm; + + RingSize(String label, int ledCount, double outerDiameterMm, double innerDiameterMm) { + this.label = label; + this.ledCount = ledCount; + this.outerDiameterMm = outerDiameterMm; + this.innerDiameterMm = innerDiameterMm; + } + + @Override public String toString() { return label; } + public int getLedCount() { return ledCount; } + public double getOuterDiameterMm() { return outerDiameterMm; } + public double getInnerDiameterMm() { return innerDiameterMm; } + } + + // Colors shared with WS2812BStick + public static Color NEO_BLACK = Color.decode("#111111"); + public static Color LED_PACKAGE = Color.decode("#FDFEFE"); + public static Color LED_BORDER = Color.decode("#D0D3D4"); + public static Color LED_DIFFUSER = Color.decode("#EAECEE"); + public static Color DIFFUSER_BORDER = Color.decode("#BDC3C7"); + public static Color CHIP_DOT_COLOR = Color.decode("#333333"); + public static Color SOLDER_PAD_COLOR = Color.decode("#D4AC0D"); + public static Color SOLDER_PAD_BORDER = Color.decode("#9A7D0A"); + + private static final String[] PIN_NAMES = {"DIN", "+5V", "GND", "DOUT"}; + + private RingSize ringSize = RingSize._16_LED; + + public WS2812BRing() { + super(); + this.bodyColor = NEO_BLACK; + updateControlPoints(); + } + + @EditableProperty(name = "Ring Size") + public RingSize getRingSize() { + return ringSize; + } + + public void setRingSize(RingSize ringSize) { + this.ringSize = ringSize; + updateControlPoints(); + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + private double getMidRadius() { + double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels(); + double innerR = new Size(ringSize.getInnerDiameterMm() / 2.0, SizeUnit.mm).convertToPixels(); + return (outerR + innerR) / 2.0; + } + + private double getPadAngle(int index) { + double spacing = PIN_SPACING.convertToPixels(); + double midR = getMidRadius(); + double deltaTheta = spacing / midR; + // 4 pads placed symmetrically at the bottom of the circular ring (near angle PI / 2) + return Math.PI / 2.0 + (index - 1.5) * deltaTheta; + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double midR = getMidRadius(); + double theta0 = getPadAngle(0); + + double[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; i++) { + double theta = getPadAngle(i); + relativeOffsets[i][0] = midR * (Math.cos(theta) - Math.cos(theta0)); + relativeOffsets[i][1] = midR * (Math.sin(theta) - Math.sin(theta0)); + } + + rotatePoints(firstPoint, relativeOffsets); + } + + private Point2D getCenter() { + Point2D p0 = controlPoints[0]; + double midR = getMidRadius(); + double theta0 = getPadAngle(0); + // When unrotated, center is at p0 - (midR*cos(theta0), midR*sin(theta0)) + return new Point2D.Double(p0.getX() - midR * Math.cos(theta0), p0.getY() - midR * Math.sin(theta0)); + } + + @Override + public Shape getBodyShape() { + Point2D center = getCenter(); + double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels(); + return new Ellipse2D.Double(center.getX() - outerR, center.getY() - outerR, outerR * 2, outerR * 2); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + Point2D center = getCenter(); + double cx = center.getX(); + double cy = center.getY(); + + double outerR = new Size(ringSize.getOuterDiameterMm() / 2.0, SizeUnit.mm).convertToPixels(); + double innerR = new Size(ringSize.getInnerDiameterMm() / 2.0, SizeUnit.mm).convertToPixels(); + double midR = (outerR + innerR) / 2.0; + + // Annular ring shape: outer circle minus inner circle + Area outerArea = new Area(new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2)); + Area innerArea = new Area(new Ellipse2D.Double(cx - innerR, cy - innerR, innerR * 2, innerR * 2)); + outerArea.subtract(innerArea); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(outerArea); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(outerArea); + + if (!outlineMode) { + int ledCount = ringSize.getLedCount(); + double ledSize = Math.max(8.0, Math.min(15.0, (2 * Math.PI * midR / ledCount) * 0.60)); + double halfLed = ledSize / 2.0; + + // Draw all LEDs around the circular ring + for (int i = 0; i < ledCount; i++) { + double angle = 2 * Math.PI * i / ledCount - Math.PI / 2.0; + double lx = cx + midR * Math.cos(angle); + double ly = cy + midR * Math.sin(angle); + + // 5050 White Package + g2d.setColor(LED_PACKAGE); + g2d.fill(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2)); + g2d.setColor(LED_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(0.5f)); + g2d.draw(new RoundRectangle2D.Double(lx - halfLed, ly - halfLed, ledSize, ledSize, 2, 2)); + + // Milky phosphor lens + double diffR = halfLed * 0.7; + g2d.setColor(LED_DIFFUSER); + g2d.fill(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2)); + g2d.setColor(DIFFUSER_BORDER); + g2d.draw(new Ellipse2D.Double(lx - diffR, ly - diffR, diffR * 2, diffR * 2)); + + // Tiny IC dot + g2d.setColor(CHIP_DOT_COLOR); + double dotS = Math.max(2.0, diffR * 0.4); + g2d.fill(new Rectangle2D.Double(lx - dotS / 2.0, ly - dotS / 2.0, dotS, dotS)); + } + + // Center silkscreen text + g2d.setColor(Color.decode("#666666")); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "NeoPixel", cx, cy - 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, ringSize.getLedCount() + "x LED", cx, cy + 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen labels next to the 4 solder pads on the ring + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + String[] labels = new String[] {"DIN", "5V", "GND", "DOUT"}; + for (int i = 0; i < 4; i++) { + double theta = getPadAngle(i); + double labelR = innerR + 5; + double px = cx + labelR * Math.cos(theta); + double py = cy + labelR * Math.sin(theta); + StringUtils.drawCenteredText(g2d, labels[i], px, py, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw the 4 solder pads directly on the circular ring PCB + drawSolderPads(g2d, 0, 4, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + int cx = width / 2; + int cy = height / 2; + int outerR = width / 2 - 2; + int innerR = outerR / 2; + + Area outer = new Area(new Ellipse2D.Double(cx - outerR, cy - outerR, outerR * 2, outerR * 2)); + Area inner = new Area(new Ellipse2D.Double(cx - innerR, cy - innerR, innerR * 2, innerR * 2)); + outer.subtract(inner); + g2d.setColor(NEO_BLACK); + g2d.fill(outer); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(outer); + + Color[] rainbow = new Color[] { + Color.decode("#FF3333"), + Color.decode("#FFD700"), + Color.decode("#00E676"), + Color.decode("#00E5FF"), + Color.decode("#E040FB"), + Color.decode("#FF6B35") + }; + + double midR = (outerR + innerR) / 2.0; + int dotR = Math.max(2, (int)(midR * 0.35)); + int dotCount = 6; + + for (int i = 0; i < dotCount; i++) { + double angle = 2 * Math.PI * i / dotCount - Math.PI / 2.0; + double lx = cx + midR * Math.cos(angle); + double ly = cy + midR * Math.sin(angle); + g2d.setColor(rainbow[i % rainbow.length]); + g2d.fill(new Ellipse2D.Double(lx - dotR, ly - dotR, dotR * 2, dotR * 2)); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java new file mode 100644 index 000000000..2c0d4f25f --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/displays/WS2812BStick.java @@ -0,0 +1,251 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.displays; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "NeoPixel Stick (8x WS2812B)", category = "Displays & Outputs", +// author = "Branislav Stojkovic", description = "8-LED Addressable RGB WS2812B NeoPixel Stick", +// instanceNamePrefix = "LED", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class WS2812BStick extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color NEO_BLACK = Color.decode("#111111"); + public static Color LED_PACKAGE = Color.decode("#FDFEFE"); + public static Color LED_BORDER = Color.decode("#D0D3D4"); + public static Color LED_DIFFUSER = Color.decode("#EAECEE"); + public static Color DIFFUSER_BORDER = Color.decode("#BDC3C7"); + public static Color CHIP_DOT_COLOR = Color.decode("#333333"); + + public static Font PIN_LABEL_FONT = new Font("SansSerif", Font.BOLD, 9); + + public static Size BOARD_WIDTH = new Size(53.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(10.2d, SizeUnit.mm); + + private static final String[] PIN_NAMES = { + "DIN", "+5V (In)", "GND (In)", "GND (In)", + "DOUT", "+5V (Out)", "GND (Out)", "GND (Out)" + }; + + public WS2812BStick() { + super(); + this.bodyColor = NEO_BLACK; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double w = BOARD_WIDTH.convertToPixels(); + + // 8 pins total: + // Left input header (4 pins): 0 = DIN, 1 = +5V, 2 = GND, 3 = GND + // Right output header (4 pins): 4 = DOUT, 5 = +5V, 6 = GND, 7 = GND + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { 0, spacing }, + { 0, spacing * 2 }, + { 0, spacing * 3 }, + { w - 20, 0 }, + { w - 20, spacing }, + { w - 20, spacing * 2 }, + { w - 20, spacing * 3 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + return new RoundRectangle2D.Double(x - 10, y - 10, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 10; + double boardY = y - 10; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 8x 5050 SMD WS2812B RGB LEDs (centered between left and right pin areas) + double spacing = PIN_SPACING.convertToPixels(); + double ledAreaStartX = boardX + 68; + double ledAreaEndX = boardX + boardW - 68; + double ledPitch = (ledAreaEndX - ledAreaStartX) / 7.0; + + for (int i = 0; i < 8; i++) { + double lx = ledAreaStartX + i * ledPitch; + double ly = boardY + boardH / 2.0; + + // 5050 White Package (28x28) + g2d.setColor(LED_PACKAGE); + g2d.fill(new RoundRectangle2D.Double(lx - 14, ly - 14, 28, 28, 3, 3)); + g2d.setColor(LED_BORDER); + g2d.draw(new RoundRectangle2D.Double(lx - 14, ly - 14, 28, 28, 3, 3)); + + // Circular milky phosphor/lens + g2d.setColor(LED_DIFFUSER); + g2d.fill(new Ellipse2D.Double(lx - 10, ly - 10, 20, 20)); + g2d.setColor(DIFFUSER_BORDER); + g2d.draw(new Ellipse2D.Double(lx - 10, ly - 10, 20, 20)); + + // Internal tiny IC dot + g2d.setColor(CHIP_DOT_COLOR); + g2d.fill(new Rectangle2D.Double(lx - 2.5, ly - 2.5, 5, 5)); + } + + // Silk Screen Pin Labels — all 4 pins labeled on both sides with clean spacing + g2d.setColor(Color.WHITE); + g2d.setFont(PIN_LABEL_FONT); + + // Left Input Header Labels (DIN, 5V, GND, GND) + String[] inLabels = {"DIN", "5V", "GND", "GND"}; + for (int i = 0; i < 4; i++) { + StringUtils.drawCenteredText(g2d, inLabels[i], boardX + 23, y + i * spacing, + HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + } + + // Right Output Header Labels (DOUT, 5V, GND, GND) + String[] outLabels = {"DOUT", "5V", "GND", "GND"}; + for (int i = 0; i < 4; i++) { + StringUtils.drawCenteredText(g2d, outLabels[i], boardX + boardW - 23, y + i * spacing, + HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw input and output header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + double rectX = 2; + double rectY = height / 2.0 - 5; + double rectW = width - 4; + double rectH = 10; + + g2d.setColor(NEO_BLACK); + g2d.fill(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 2, 2)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new RoundRectangle2D.Double(rectX, rectY, rectW, rectH, 2, 2)); + + Color[] rainbow = new Color[] { + Color.decode("#FF3333"), // Red + Color.decode("#FFD700"), // Yellow + Color.decode("#00E676"), // Green + Color.decode("#00E5FF"), // Cyan + Color.decode("#E040FB") // Magenta + }; + + double dotSize = Math.max(3.0, Math.min(4.0, rectH - 4.0)); + double margin = 3.0; + double step = (rectW - 2 * margin - dotSize) / (rainbow.length - 1); + + for (int i = 0; i < rainbow.length; i++) { + double dotX = rectX + margin + i * step; + double dotY = height / 2.0 - dotSize / 2.0; + + // 5050 package mini white backing + g2d.setColor(Color.WHITE); + g2d.fill(new Rectangle2D.Double(dotX - 0.5, dotY - 0.5, dotSize + 1, dotSize + 1)); + + // RGB LED dot + g2d.setColor(rainbow[i]); + g2d.fill(new Ellipse2D.Double(dotX, dotY, dotSize, dotSize)); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java new file mode 100644 index 000000000..b520b5abe --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/electromechanical/BatteryHolder18650.java @@ -0,0 +1,337 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.electromechanical; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.appframework.miscutils.ConfigurationManager; +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.IPlugInPort; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractTransparentComponent; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.Theme; +import org.diylc.core.VisibilityPolicy; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "18650 Battery Holder", category = "Electro-Mechanical", + author = "Branislav Stojkovic", description = "PCB-mount 18650 Li-ion Battery Holder (1S or 2S)", + instanceNamePrefix = "BTR", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class BatteryHolder18650 extends AbstractTransparentComponent { + + private static final long serialVersionUID = 1L; + + public enum CellCount { + _1S("1S (3.7V) — Single Cell", 1, 77.0, 21.0), + _2S("2S (7.4V) — Two Cells", 2, 77.0, 42.0); + + private final String label; + private final int cells; + private final double lengthMm; + private final double widthMm; + + CellCount(String label, int cells, double lengthMm, double widthMm) { + this.label = label; + this.cells = cells; + this.lengthMm = lengthMm; + this.widthMm = widthMm; + } + + @Override public String toString() { return label; } + public int getCells() { return cells; } + public double getLengthMm() { return lengthMm; } + public double getWidthMm() { return widthMm; } + } + + public static Color HOLDER_BODY = Color.decode("#2C2C2C"); + public static Color HOLDER_BODY_LIGHT = Color.decode("#3C3C3C"); + public static Color SPRING_COLOR = Color.decode("#C0C0C0"); + public static Color SPRING_BORDER = Color.decode("#909090"); + public static Color PLATE_COLOR = Color.decode("#D0D0D0"); + public static Color CELL_CAVITY = Color.decode("#1A1A1A"); + + private static final Font LABEL_FONT = new Font("SansSerif", Font.BOLD, 12); + + private String value = ""; + private CellCount cellCount = CellCount._1S; + private Orientation orientation = Orientation.DEFAULT; + + // Two control points: Point 0 = + (left), Point 1 = - (right) + private Point2D[] controlPoints = new Point2D[] { + new Point2D.Double(0, 0), + new Point2D.Double(0, 0) + }; + + public BatteryHolder18650() { + super(); + updateControlPoints(); + } + + @EditableProperty(name = "Cell Count") + public CellCount getCellCount() { + return cellCount; + } + + public void setCellCount(CellCount cellCount) { + this.cellCount = cellCount; + updateControlPoints(); + } + + @EditableProperty(name = "Orientation") + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + updateControlPoints(); + } + + private void updateControlPoints() { + double length = new Size(cellCount.getLengthMm(), SizeUnit.mm).convertToPixels(); + Point2D p0 = controlPoints[0]; + + double theta = orientation.toRadians(); + // + at p0 (left), - at p0 + (length, 0) rotated (right) + double dx = Math.cos(theta) * length; + double dy = Math.sin(theta) * length; + controlPoints[1] = new Point2D.Double(p0.getX() + dx, p0.getY() + dy); + } + + @Override + public int getControlPointCount() { return 2; } + + @Override + public Point2D getControlPoint(int index) { return controlPoints[index]; } + + @Override + public void setControlPoint(Point2D point, int index) { + controlPoints[index].setLocation(point); + if (index == 0) { + updateControlPoints(); + } + } + + @Override + public boolean isControlPointSticky(int index) { return true; } + + @Override + public VisibilityPolicy getControlPointVisibilityPolicy(int index) { + return VisibilityPolicy.WHEN_SELECTED; + } + + @Override + public boolean canPointMoveFreely(int pointIndex) { return false; } + + @Override + public boolean canControlPointOverlap(int index) { return false; } + + @Override + public String getControlPointNodeName(int index) { + return index == 0 ? "+ (VCC)" : "- (GND)"; + } + + @Override + public String getValue() { return value; } + + @Override + public void setValue(String value) { this.value = value; } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double length = new Size(cellCount.getLengthMm(), SizeUnit.mm).convertToPixels(); + double width = new Size(cellCount.getWidthMm(), SizeUnit.mm).convertToPixels(); + double cellD = new Size(18.5d, SizeUnit.mm).convertToPixels(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + Composite oldComposite = applyAlpha(g2d, componentState); + + // Border color + Color borderColor; + if (outlineMode) { + Theme theme = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME); + borderColor = (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) + ? SELECTION_COLOR : (theme != null ? theme.getOutlineColor() : Color.BLACK); + } else { + borderColor = (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) + ? SELECTION_COLOR : HOLDER_BODY.darker(); + } + + double bodyY = y - width / 2.0; + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : HOLDER_BODY); + g2d.fill(new RoundRectangle2D.Double(x, bodyY, length, width, 10, 10)); + drawingObserver.stopTracking(); + + g2d.setColor(borderColor); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new RoundRectangle2D.Double(x, bodyY, length, width, 10, 10)); + + if (!outlineMode) { + int cells = cellCount.getCells(); + double cellWidth = (width - 8.0) / cells; + + for (int i = 0; i < cells; i++) { + double cavityX = x + 6; + double cavityY = bodyY + 4 + i * cellWidth; + double cavityW = length - 12; + double cavityH = cellWidth - 4; + + // Cell cavity (dark inset) + g2d.setColor(CELL_CAVITY); + g2d.fill(new RoundRectangle2D.Double(cavityX, cavityY, cavityW, cavityH, 6, 6)); + g2d.setColor(HOLDER_BODY_LIGHT); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(cavityX, cavityY, cavityW, cavityH, 6, 6)); + + // Positive flat plate button contact on left (x) + double plateX = x + 6; + double plateW = 6; + double plateH = Math.min(14, cellH(cellD)); + double contactY = cavityY + cavityH / 2.0; + g2d.setColor(PLATE_COLOR); + g2d.fill(new Rectangle2D.Double(plateX, contactY - plateH / 2.0, plateW, plateH)); + g2d.setColor(SPRING_BORDER); + g2d.draw(new Rectangle2D.Double(plateX, contactY - plateH / 2.0, plateW, plateH)); + + // Negative spring contact on right (x + length) + double springX = x + length - 16; + double springW = 10; + double springH = Math.min(14, cellH(cellD)); + g2d.setColor(SPRING_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Rectangle2D.Double(springX, contactY - springH / 2.0, springW, springH)); + g2d.draw(new Rectangle2D.Double(springX + 3, contactY - springH / 2.0 + 2, springW - 6, springH - 4)); + + // Polarity markings (+ on left, - on right) + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT); + StringUtils.drawCenteredText(g2d, "+", x + 20, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "-", x + length - 20, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // 18650 text in cavity + g2d.setColor(Color.decode("#555555")); + StringUtils.drawCenteredText(g2d, "18650", x + length / 2.0, contactY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // Series connector bar between cells for 2S + if (cells > 1) { + g2d.setColor(SPRING_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f)); + double midY = bodyY + width / 2.0; + g2d.draw(new Rectangle2D.Double(x + length - 10, midY - 6, 6, 12)); + } + } + + g2d.setTransform(oldTx); + + // Draw control point solder tabs (Point 0 = + left, Point 1 = - right) + if (!outlineMode) { + for (Point2D cp : controlPoints) { + g2d.setColor(PLATE_COLOR); + g2d.fill(new Ellipse2D.Double(cp.getX() - 5, cp.getY() - 5, 10, 10)); + g2d.setColor(SPRING_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new Ellipse2D.Double(cp.getX() - 5, cp.getY() - 5, 10, 10)); + // Inner hole + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(cp.getX() - 2, cp.getY() - 2, 4, 4)); + } + } + + g2d.setComposite(oldComposite); + } + + private double cellH(double cellD) { + return cellD * 0.45; + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Dark holder body + g2d.setColor(HOLDER_BODY); + g2d.fill(new RoundRectangle2D.Double(2, height / 2.0 - 8, width - 4, 16, 4, 4)); + g2d.setColor(HOLDER_BODY.brighter()); + g2d.draw(new RoundRectangle2D.Double(2, height / 2.0 - 8, width - 4, 16, 4, 4)); + + // Cell cavity + g2d.setColor(CELL_CAVITY); + g2d.fill(new RoundRectangle2D.Double(5, height / 2.0 - 6, width - 10, 12, 3, 3)); + + // Plate on left (+), spring on right (-) + g2d.setColor(PLATE_COLOR); + g2d.fillRect(5, height / 2 - 3, 3, 6); + g2d.setColor(SPRING_COLOR); + g2d.fillRect(width - 9, height / 2 - 3, 4, 6); + + // Polarity + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + StringUtils.drawCenteredText(g2d, "+", 12, height / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "-", width - 13, height / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + @Override + public Rectangle2D getCachingBounds() { + double minX = Math.min(controlPoints[0].getX(), controlPoints[1].getX()); + double minY = Math.min(controlPoints[0].getY(), controlPoints[1].getY()); + double maxX = Math.max(controlPoints[0].getX(), controlPoints[1].getX()); + double maxY = Math.max(controlPoints[0].getY(), controlPoints[1].getY()); + double width = new Size(cellCount.getWidthMm(), SizeUnit.mm).convertToPixels(); + int margin = 50; + return new Rectangle2D.Double(minX - margin, minY - width / 2.0 - margin, + (maxX - minX) + 2 * margin, (maxY - minY) + width + 2 * margin); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoMega.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoMega.java new file mode 100644 index 000000000..395d84324 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoMega.java @@ -0,0 +1,469 @@ +/* + * + * 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 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.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardLogos; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Arduino Mega 2560", category = "Controllers", + author = "Branislav Stojkovic", description = "Arduino Mega 2560 R3 High-I/O Microcontroller Board", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.BOARD, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ArduinoMega extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SILK_COLOR = Color.WHITE; + + public static Size BOARD_WIDTH = new Size(101.6d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(53.4d, SizeUnit.mm); + + // 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", + // Analog Low A0..A7 (8..15) + "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", + // Analog High A8..A15 (16..23) + "A8", "A9", "A10", "A11", "A12", "A13", "A14", "A15", + // 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", + // 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) + "D22", "D23", "D24", "D25", "D26", "D27", "D28", "D29", "D30", "D31", "D32", "D33", + "D34", "D35", "D36", "D37", "D38", "D39", "D40", "D41", "D42", "D43", "D44", "D45", + "D46", "D47", "D48", "D49", "D50 (MISO)", "D51 (MOSI)", "D52 (SCK)", "D53 (SS)", + "GND_EXT1", "GND_EXT2", "5V_EXT1", "5V_EXT2", + // Main ICSP Header (86..91, ATmega2560) + "MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP", + // Top-Left ICSP Header (92..97, ATmega16U2) + "MISO_16U2", "5V_16U2", "SCK_16U2", "MOSI_16U2", "RST_16U2", "GND_16U2" + }; + + // What the board prints next to the headers, which is not the node name: every ground says + // "GND", the supply says "3V3", the digital pins carry bare numbers with a "~" on the PWM-capable + // ones, and the reserved first power pin is left blank. The ICSP pins are printed nowhere on the + // board, so their entries only keep this array parallel to PIN_NAMES. + public static final String[] SILK_NAMES = new String[] { + // Power Header (0..7) + "", "IOREF", "RESET", "3V3", "5V", "GND", "GND", "VIN", + // Analog Low A0..A7 (8..15) + "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", + // Analog High A8..A15 (16..23) + "A8", "A9", "A10", "A11", "A12", "A13", "A14", "A15", + // Digital Low D0..D7 (24..31) + "RX0", "TX0", "~2", "~3", "~4", "~5", "~6", "~7", + // Digital High D8..D13, GND, AREF, SDA, SCL (32..41) + "~8", "~9", "~10", "~11", "~12", "~13", "GND", "AREF", "SDA", "SCL", + // Communication Header D14..D21 (42..49) + "14", "15", "16", "17", "18", "19", "20", "21", + // Double Digital 2x18 Header (50..85) + "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", + "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", + "46", "47", "48", "49", "50", "51", "52", "53", + // the block's power and ground rows are printed once for the pair, on the outer column, + // which is the only side with room for them next to the analog header and the mounting hole + "", "GND", "", "5V", + // Main ICSP Header (86..91) + "MISO", "5V", "SCK", "MOSI", "RST", "GND", + // Top-Left ICSP Header (92..97) + "MISO", "5V", "SCK", "MOSI", "RST", "GND" + }; + + public ArduinoMega() { + super(); + this.bodyColor = ARDUINO_TEAL; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected String getSilkPinLabel(int index) { + if (index >= 0 && index < SILK_NAMES.length) { + return SILK_NAMES[index]; + } + return super.getSilkPinLabel(index); + } + + private double[][] getRelativeOffsets() { + double spacing = PIN_SPACING.convertToPixels(); // 20px for 0.1" (100 mils) + double gap02 = new Size(0.2d, SizeUnit.in).convertToPixels(); // 40px for 0.2" + + // Reference: Pin 0 (NC) is at (boardX + 220.0, boardY + 400.0) [1100 mils, 100 mils from bottom-left] + // Board is 800px wide (4000 mils), 420px high (2100 mils) + double[][] relativeOffsets = new double[PIN_NAMES.length][2]; + + // Power header pins 0..7 (bottom row, 1100..1800 mils) + for (int i = 0; i < 8; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + // Analog Low A0..A7 pins 8..15 (bottom row, 2000..2700 mils, separated from VIN by 0.2" = 40px) + double a0X = 7 * spacing + gap02; // 180px + for (int i = 0; i < 8; i++) { + relativeOffsets[8 + i][0] = a0X + i * spacing; + relativeOffsets[8 + i][1] = 0; + } + // Analog High A8..A15 pins 16..23 (bottom row, 2900..3600 mils, separated from A7 by 0.2" = 40px) + double a8X = a0X + 7 * spacing + gap02; // 360px + for (int i = 0; i < 8; i++) { + relativeOffsets[16 + i][0] = a8X + i * spacing; + relativeOffsets[16 + i][1] = 0; + } + // Digital Low D0..D7 pins 24..31 (top row, 2500..1800 mils, Y=2000 mils) + double topRowY = -new Size(1.9d, SizeUnit.in).convertToPixels(); // -380px + double d0X = new Size(1.4d, SizeUnit.in).convertToPixels(); // aligns with A5 at 2500 mils (280px) + for (int i = 0; i < 8; i++) { + relativeOffsets[24 + i][0] = d0X - i * spacing; + relativeOffsets[24 + i][1] = topRowY; + } + // Digital High D8..D13/GND/AREF/SDA/SCL pins 32..41 (top row, 1640..740 mils, gap between D7 and D8 is 0.16" = 32px) + double d7X = 7 * spacing; // 140px + double d7d8Gap = new Size(0.16d, SizeUnit.in).convertToPixels(); // 32px + double d8X = d7X - d7d8Gap; // 108px + for (int i = 0; i < 10; i++) { + relativeOffsets[32 + i][0] = d8X - i * spacing; + relativeOffsets[32 + i][1] = topRowY; + } + // Communication header D14..D21 pins 42..49 (top row, 2700..3400 mils, gap from D0 is 0.2" = 40px) + double d14X = d0X + gap02; // 320px, aligns with A7 at 2700 mils + for (int i = 0; i < 8; i++) { + relativeOffsets[42 + i][0] = d14X + i * spacing; + relativeOffsets[42 + i][1] = topRowY; + } + // Double digital header 2x18 at far right pins 50..85 (D22..D53, GNDx2, 5Vx2; X=3600, 3700 mils, + // Y=2000..300 mils). The block is powered from its top row and grounded at the bottom one, so + // the two 5V pins sit alongside the digital header and D22..D53 fill the sixteen rows below. + double d22InnerX = new Size(2.6d, SizeUnit.in).convertToPixels(); // 520px: (3700 - 1100) * 0.2 + double d22OuterX = new Size(2.7d, SizeUnit.in).convertToPixels(); // 540px: (3800 - 1100) * 0.2 + relativeOffsets[84] = new double[] {d22InnerX, topRowY}; + relativeOffsets[85] = new double[] {d22OuterX, topRowY}; + for (int row = 0; row < 16; row++) { + double py = topRowY + (row + 1) * spacing; + relativeOffsets[50 + row * 2][0] = d22InnerX; + relativeOffsets[50 + row * 2][1] = py; + relativeOffsets[50 + row * 2 + 1][0] = d22OuterX; + relativeOffsets[50 + row * 2 + 1][1] = py; + } + relativeOffsets[82] = new double[] {d22InnerX, topRowY + 17 * spacing}; + relativeOffsets[83] = new double[] {d22OuterX, topRowY + 17 * spacing}; + // Main ICSP header (2x3 pins, 86..91) for ATmega2560 at (2505, 1200) mils + double icspX = new Size(1.405d, SizeUnit.in).convertToPixels(); // 281.0 px: (2505 - 1100) * 0.2 + double icspY = -new Size(1.1d, SizeUnit.in).convertToPixels(); // -220.0 px: (100 - 1200) * 0.2 + relativeOffsets[86] = new double[] {icspX, icspY}; + relativeOffsets[87] = new double[] {icspX + spacing, icspY}; + relativeOffsets[88] = new double[] {icspX, icspY + spacing}; + relativeOffsets[89] = new double[] {icspX + spacing, icspY + spacing}; + relativeOffsets[90] = new double[] {icspX, icspY + 2 * spacing}; + relativeOffsets[91] = new double[] {icspX + spacing, icspY + 2 * spacing}; + + // Top-Left ICSP header (2x3 pins, 92..97) for ATmega16U2 near USB jack at (820, 1870) mils + double icsp2X = -new Size(0.28d, SizeUnit.in).convertToPixels(); // -56.0 px: (820 - 1100) * 0.2 + // far enough below the digital header for its rotated pin names to fit above the connector + double icsp2Y = -new Size(1.77d, SizeUnit.in).convertToPixels() + + new Size(3.5d, SizeUnit.mm).convertToPixels(); + relativeOffsets[92] = new double[] {icsp2X, icsp2Y}; + relativeOffsets[93] = new double[] {icsp2X, icsp2Y + spacing}; + relativeOffsets[94] = new double[] {icsp2X - spacing, icsp2Y}; + relativeOffsets[95] = new double[] {icsp2X - spacing, icsp2Y + spacing}; + relativeOffsets[96] = new double[] {icsp2X - 2 * spacing, icsp2Y}; + relativeOffsets[97] = new double[] {icsp2X - 2 * spacing, icsp2Y + spacing}; + + return relativeOffsets; + } + + @Override + protected void updateControlPoints() { + rotatePoints(controlPoints[0], getRelativeOffsets()); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardX = x - new Size(1.1d, SizeUnit.in).convertToPixels(); + double boardY = y - new Size(2.0d, SizeUnit.in).convertToPixels(); + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + + double notchInset = new Size(0.1d, SizeUnit.in).convertToPixels(); + double topCutoff = new Size(0.16d, SizeUnit.in).convertToPixels(); + double y12 = new Size(0.06d, SizeUnit.in).convertToPixels(); + double y102 = new Size(0.51d, SizeUnit.in).convertToPixels(); + double y122 = new Size(0.61d, SizeUnit.in).convertToPixels(); + double y380 = new Size(1.9d, SizeUnit.in).convertToPixels(); + double y400 = new Size(2.0d, SizeUnit.in).convertToPixels(); + double cornerRadius = new Size(1.0d, SizeUnit.mm).convertToPixels(); + double c1 = cornerRadius * (1 - 0.5523); + double c2 = cornerRadius * 0.5523; + double y412 = boardH - cornerRadius; + + Path2D.Double path = new Path2D.Double(); + path.moveTo(boardX + boardW - topCutoff, boardY); + path.lineTo(boardX + boardW - notchInset, boardY + y12); + path.lineTo(boardX + boardW - notchInset, boardY + y102); + path.lineTo(boardX + boardW, boardY + y122); + path.lineTo(boardX + boardW, boardY + y380); + path.lineTo(boardX + boardW - notchInset, boardY + y400); + path.lineTo(boardX + boardW - notchInset, boardY + y412); + path.curveTo(boardX + boardW - notchInset, boardY + boardH - c1, boardX + boardW - notchInset - c2, boardY + boardH, boardX + boardW - notchInset - cornerRadius, boardY + boardH); + path.lineTo(boardX + cornerRadius, boardY + boardH); + path.curveTo(boardX + c1, boardY + boardH, boardX, boardY + boardH - c1, boardX, boardY + boardH - cornerRadius); + path.lineTo(boardX, boardY + cornerRadius); + path.curveTo(boardX, boardY + c1, boardX + c1, boardY, boardX + cornerRadius, boardY); + path.lineTo(boardX + boardW - topCutoff, boardY); + path.closePath(); + return path; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardX = x - new Size(1.1d, SizeUnit.in).convertToPixels(); + double boardY = y - new Size(2.0d, SizeUnit.in).convertToPixels(); + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + // Draw PCB body + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Mounting holes (6 mounting holes from technical design) + double holeDiameter = new Size(0.12d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.55d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter); // (550, 100 mils) + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter); // (600, 2000 mils) + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(2.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.7d, SizeUnit.in).convertToPixels(), holeDiameter); // (2600, 1400 mils) + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(2.6d, SizeUnit.in).convertToPixels(), boardY + new Size(1.8d, SizeUnit.in).convertToPixels(), holeDiameter); // (2600, 300 mils) + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(3.55d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter); // (3550, 2000 mils) + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(3.8d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter); // (3800, 100 mils) + + // USB Type-B Jack & DC Power Jack + MakerBoardPainter.drawUsbB(g2d, boardX - USB_B_OVERHANG.convertToPixels(), + boardY + new Size(0.375d, SizeUnit.in).convertToPixels(), + USB_B_LENGTH.convertToPixels(), + USB_B_WIDTH.convertToPixels(), "USB"); + MakerBoardPainter.drawChip(g2d, boardX - new Size(0.07d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.625d, SizeUnit.in).convertToPixels(), + new Size(0.52d, SizeUnit.in).convertToPixels(), + new Size(0.35d, SizeUnit.in).convertToPixels(), "DC IN"); + + // ATmega2560 square QFP chip + MakerBoardPainter.drawChip(g2d, boardX + new Size(1.825d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.825d, SizeUnit.in).convertToPixels(), + new Size(0.4d, SizeUnit.in).convertToPixels(), + new Size(0.4d, SizeUnit.in).convertToPixels(), "ATmega2560"); + + // Reset Button, which the Mega carries beside the ICSP header rather than by the USB jack + // the way the Uno does; it sits level with the middle row of the header + double btnW = BUTTON_WIDTH.convertToPixels(); + double btnH = BUTTON_LENGTH.convertToPixels(); + double btnX = boardX + new Size(2.72d, SizeUnit.in).convertToPixels(); + double btnY = boardY + new Size(1.0d, SizeUnit.in).convertToPixels() - btnH / 2.0; + MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH); + + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "RST", btnX + btnW / 2.0, + btnY + btnH + new Size(1.0d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Arduino Infinity Logo + MakerBoardLogos.drawArduinoLogo(g2d, boardX + new Size(1.2d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.4d, SizeUnit.in).convertToPixels()); + + // Silkscreen text & branding + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_LARGE); + StringUtils.drawCenteredText(g2d, "ARDUINO", boardX + new Size(1.44d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.725d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "MEGA 2560", boardX + new Size(1.44d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.825d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Header silkscreen labels; the captions sit beyond the rotated pin names, which take up the + // space right next to the headers themselves + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "POWER", boardX + new Size(1.45d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.75d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "ANALOG IN", boardX + new Size(3.15d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.75d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "DIGITAL (PWM ~)", boardX + new Size(1.85d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.34d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "COMMUNICATION", boardX + new Size(3.05d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.34d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Text below the 3x2 ICSP header rather than above it, where the Uno prints it: on the Mega + // that space belongs to a mounting hole. Clears the bottom edge of the MCU package as well. + double icspLabelX = boardX + new Size(2.505d, SizeUnit.in).convertToPixels() + new Size(0.05d, SizeUnit.in).convertToPixels(); + double icspLabelY = boardY + new Size(1.1d, SizeUnit.in).convertToPixels() + new Size(2.5d, SizeUnit.mm).convertToPixels(); + StringUtils.drawCenteredText(g2d, "ICSP", icspLabelX, icspLabelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Header pin names, printed above the power / analog row and below the digital row the way + // they are silkscreened on the board; the 2x18 block at the right edge is not a row + double[][] relativeOffsets = getRelativeOffsets(); + drawRowPinLabels(g2d, x, y, relativeOffsets, 0, 24, false, SILK_COLOR); + drawRowPinLabels(g2d, x, y, relativeOffsets, 24, 26, true, SILK_COLOR); + + // The 2x18 block prints its numbers upright rather than rotated like the header names, each + // on the far side of the column it belongs to + g2d.setColor(SILK_COLOR); + g2d.setFont(PIN_FONT); + double innerLabelOffset = PIN_LABEL_OFFSET.convertToPixels(); + // the outer column has only the strip between it and the notched board edge to print in + double outerLabelOffset = new Size(1.27d, SizeUnit.mm).convertToPixels(); + for (int i = 50; i < PIN_NAMES.length - 12; i++) { + String label = getSilkPinLabel(i); + if (label.isEmpty()) { + continue; + } + boolean outerColumn = (i - 50) % 2 == 1; + StringUtils.drawCenteredText(g2d, label, + outerColumn ? x + relativeOffsets[i][0] + outerLabelOffset + : x + relativeOffsets[i][0] - innerLabelOffset, + y + relativeOffsets[i][1], + outerColumn ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT, + VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw header pins with continuity tracking + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + double boardX = 5; + double boardY = 3; + double boardW = width - boardX - 2; + double boardH = height - 6; + + // USB Type-B Jack (metallic silver, top left protruding) + double usbW = 7; + double usbH = 8; + double usbX = 1; + double usbY = boardY + 2; + + // DC Power Jack (dark body, bottom left protruding) + double dcW = 7; + double dcH = 6; + double dcX = 1; + double dcY = boardY + boardH - dcH - 3; + + // Board PCB + g2d.setColor(ARDUINO_TEAL); + g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4)); + g2d.setColor(ARDUINO_TEAL.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4)); + + // Draw USB Jack + g2d.setColor(USB_METAL_COLOR); + g2d.fill(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2)); + g2d.setColor(METAL_SHIELD_BORDER); + g2d.draw(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2)); + + // Draw DC Jack + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2)); + g2d.setColor(Color.BLACK); + g2d.draw(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2)); + + // Arduino Infinity logo + double scale = 14.0 / 95.56; + double logoW = 95.56 * scale; + double logoH = 45.33 * scale; + double logoX = boardX + (boardW - logoW) / 2.0 + 1.0; + double logoY = boardY + 3.5; + MakerBoardLogos.drawArduinoLogo(g2d, logoX, logoY, scale); + + // MEGA text below logo + g2d.setColor(SILK_COLOR); + int fontSize = Math.max(6, (int) Math.round(boardH * 0.26)); + g2d.setFont(new Font("SansSerif", Font.BOLD, fontSize)); + double textY = logoY + logoH + (boardY + boardH - (logoY + logoH)) / 2.0; + double textX = boardX + (boardW / 2.0) + 0.5; + StringUtils.drawCenteredText(g2d, "MEGA", textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java new file mode 100644 index 000000000..6f3ce96f8 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoNano.java @@ -0,0 +1,668 @@ +/* + * + * 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 java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Arduino Nano", category = "Controllers", + author = "Branislav Stojkovic", + description = "Arduino Nano Breadboard-Friendly Microcontroller Board (classic, Every, 33 IoT, " + + "33 BLE, RP2040 Connect, Nano ESP32, Nano R4)", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ArduinoNano extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SILK_COLOR = Color.WHITE; + + public static Size BOARD_WIDTH = new Size(0.73d, SizeUnit.in); + public static Size BOARD_LENGTH = new Size(1.70d, SizeUnit.in); + + public static Size ROW_SPACING = new Size(0.60d, SizeUnit.in); + public static Size BOARD_MARGIN_Y = new Size(0.15d, SizeUnit.in); + + // Where the part at the end of the board opposite the USB jack starts, and the footprint of the + // Nano R4's Qwiic socket. The R4's extra control point is derived from these, so the point and + // the drawn connector stay together. + public static Size END_PART_OFFSET_Y = new Size(1.5d, SizeUnit.mm); + public static Size QWIIC_WIDTH = new Size(6.5d, SizeUnit.mm); + public static Size QWIIC_LENGTH = new Size(4.5d, SizeUnit.mm); + + public static final int PINS_PER_ROW = 15; + + // Ordered by label so the Version drop-down reads alphabetically. XStream serializes enum + // constants by name, so the order here is free to change without affecting existing .diy files. + public enum NanoVersion { + CLASSIC("Nano (ATmega328)", "NANO", "m328P", null), + NANO_33_BLE("Nano 33 BLE", "33 BLE", null, "NINA-B306"), + NANO_33_BLE_SENSE("Nano 33 BLE Sense", "33 BLE SENSE", null, "NINA-B306"), + NANO_33_IOT("Nano 33 IoT", "33 IOT", "SAMD21", "NINA-W102"), + NANO_ESP32("Nano ESP32", "NANO ESP32", null, "NORA-W106"), + EVERY("Nano Every", "EVERY", "m4809", "SAMD11"), + NANO_R4("Nano R4", "NANO R4", "RA4M1", "QWIIC"), + // Silkscreened "CONNECT" rather than the full name, which does not fit between the two rows of + // pad names; the RP2040 package right below it carries the part number + NANO_RP2040_CONNECT("Nano RP2040 Connect", "CONNECT", "RP2040", "NINA-W102"); + + private final String label; + private final String silkLabel; + private final String mcuLabel; + private final String moduleLabel; + + NanoVersion(String label, String silkLabel, String mcuLabel, String moduleLabel) { + this.label = label; + this.silkLabel = silkLabel; + this.mcuLabel = mcuLabel; + this.moduleLabel = moduleLabel; + } + + public String getSilkLabel() { + return silkLabel; + } + + /** + * Marking of the separately visible MCU package, or null when the MCU is a die inside the + * module rather than its own part on the board. + */ + public String getMcuLabel() { + return mcuLabel; + } + + /** + * Marking of the part occupying the end of the board opposite the USB jack, where the classic + * Nano has its ICSP header. Null on the classic Nano, which has the header instead. + */ + public String getModuleLabel() { + return moduleLabel; + } + + @Override + public String toString() { + return label; + } + } + + public static final String[] PIN_NAMES = new String[] { + // Left row (0..14, top to bottom) + "D1 (TX)", "D0 (RX)", "RST1", "GND1", "D2", "D3 (~)", "D4", "D5 (~)", "D6 (~)", "D7", "D8", "D9 (~)", "D10 (~)", "D11 (~)", "D12", + // Right row (15..29, top to bottom) + "VIN", "GND2", "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" + }; + + // Every board after the classic keeps the same 2x15 pin positions but has no 2x3 ICSP block -- + // that end of the PCB carries the radio module or USB bridge -- so they are 30 control points. + // Each one's PWM set and power pins are taken from its official Arduino pinout diagram; they + // differ more than the shared outline suggests. + + // 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)", + // 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)" + }; + + // 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)", + // 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)" + }; + + // 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)", + // 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)" + }; + + // Nano RP2040 Connect (ABX00053): every RP2040 GPIO has a PWM slice. Note REC (BOOTSEL) where + // the other boards repeat RESET. + public static final String[] PIN_NAMES_RP2040 = new String[] { + // 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)" + }; + + // 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 (~)", + "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)", + "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" + }; + + // Nano ESP32 (ABX00083): PWM on every digital and analog pin, and the pins the other boards use + // for RESET and AREF are the B1 / B0 boot-mode pads. Dual-labelled with the Espressif GPIO + // 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)", + "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)", + "A4 (~, SDA/GPIO11)", "A3 (~, GPIO4)", "A2 (~, GPIO3)", "A1 (~, GPIO2)", "A0 (~, GPIO1)", + "B0", "3.3V", "D13 (~, SCK/GPIO48)" + }; + + // What the boards print next to the pads, which is shorter than the node name: the two grounds + // and the two resets are printed alike, the supply is "3V3", and the function annotations the + // node names carry are not on the silkscreen at all. The classic Nano's ICSP block and the R4's + // Qwiic socket are printed nowhere, so these arrays cover the two pin rows only. + public static final String[] SILK_NAMES = new String[] { + // Left row (0..14, top to bottom) + "TX1", "RX0", "RST", "GND", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", + // Right row (15..29, top to bottom) + "VIN", "GND", "RST", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3V3", "D13" + }; + + // The RP2040 Connect puts the BOOTSEL pad where the other boards repeat RESET + public static final String[] SILK_NAMES_RP2040 = new String[] { + // Left row (0..14, top to bottom) + "TX1", "RX0", "RST", "GND", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", + // Right row (15..29, top to bottom) + "VIN", "GND", "REC", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3V3", "D13" + }; + + // The R4 uses that same pad for boot mode selection + public static final String[] SILK_NAMES_R4 = new String[] { + // Left row (0..14, top to bottom) + "TX1", "RX0", "RST", "GND", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", + // Right row (15..29, top to bottom) + "VIN", "GND", "BOOT", "5V", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "AREF", "3V3", "D13" + }; + + // The Nano ESP32 has the B1 / B0 boot-mode pads where the others carry the second RESET and + // AREF, and its 5V pad is fed straight from the USB bus + public static final String[] SILK_NAMES_ESP32 = new String[] { + // Left row (0..14, top to bottom) + "TX1", "RX0", "RST", "GND", "D2", "D3", "D4", "D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", + // Right row (15..29, top to bottom) + "VIN", "GND", "B1", "VUSB", "A7", "A6", "A5", "A4", "A3", "A2", "A1", "A0", "B0", "3V3", "D13" + }; + + protected NanoVersion version = NanoVersion.CLASSIC; + protected boolean headers = false; + + public ArduinoNano() { + super(); + this.bodyColor = ARDUINO_TEAL; + updateControlPoints(); + } + + @EditableProperty(name = "Version") + public NanoVersion getVersion() { + if (version == null) { + version = NanoVersion.CLASSIC; + } + return version; + } + + public void setVersion(NanoVersion version) { + this.version = version; + updateControlPoints(); + invalidateCache(); + } + + private String[] getPinNames() { + switch (getVersion()) { + case EVERY: + return PIN_NAMES_EVERY; + case NANO_33_IOT: + return PIN_NAMES_33_IOT; + case NANO_33_BLE: + case NANO_33_BLE_SENSE: + return PIN_NAMES_33_BLE; + case NANO_RP2040_CONNECT: + return PIN_NAMES_RP2040; + case NANO_ESP32: + return PIN_NAMES_ESP32; + case NANO_R4: + return PIN_NAMES_R4; + default: + return PIN_NAMES; + } + } + + private String[] getSilkNames() { + switch (getVersion()) { + case NANO_RP2040_CONNECT: + return SILK_NAMES_RP2040; + case NANO_ESP32: + return SILK_NAMES_ESP32; + case NANO_R4: + return SILK_NAMES_R4; + default: + return SILK_NAMES; + } + } + + @Override + protected String getSilkPinLabel(int index) { + String[] silkNames = getSilkNames(); + if (index >= 0 && index < silkNames.length) { + return silkNames[index]; + } + return super.getSilkPinLabel(index); + } + + private UsbPortType getUsbPortType() { + switch (getVersion()) { + case CLASSIC: + return UsbPortType.MINI; + case NANO_ESP32: + case NANO_R4: + return UsbPortType.TYPE_C; + default: + return UsbPortType.MICRO; + } + } + + /** Connector footprint {width, length, overhang} for a USB port type. */ + private Size[] getUsbPortSizes(UsbPortType type) { + switch (type) { + case MINI: + return new Size[] {USB_MINI_WIDTH, USB_MINI_LENGTH, USB_MINI_OVERHANG}; + case TYPE_C: + return new Size[] {USB_C_WIDTH, USB_C_LENGTH, USB_C_OVERHANG}; + default: + return new Size[] {USB_MICRO_WIDTH, USB_MICRO_LENGTH, USB_MICRO_OVERHANG}; + } + } + + /** + * Number of control points that are actual header pins. The Nano R4's last point is the centre + * of its Qwiic socket, which takes a wire but is not a pin to be populated with a header. + */ + private int getHeaderPinCount() { + return getVersion() == NanoVersion.NANO_R4 ? controlPoints.length - 1 : controlPoints.length; + } + + /** + * True for the boards carrying their MCU as a package rotated 45 degrees, the way the classic + * Nano mounts its ATmega328P and the Every its ATmega4809. + */ + private boolean isChipRotated() { + return getVersion() == NanoVersion.CLASSIC || getVersion() == NanoVersion.EVERY; + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + String[] pinNames = getPinNames(); + if (index >= 0 && index < pinNames.length) { + return pinNames[index]; + } + return "Pin " + (index + 1); + } + + private double[][] getRelativeOffsets() { + double spacing = PIN_SPACING.convertToPixels(); // 20px (0.10") + double rowSpacing = ROW_SPACING.convertToPixels(); // 120px (0.60") + + double[][] relativeOffsets = new double[getPinNames().length][2]; + + // Left row (pins 0..14, top to bottom) + for (int i = 0; i < 15; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + // Right row (pins 15..29, top to bottom) + for (int i = 0; i < 15; i++) { + relativeOffsets[15 + i][0] = rowSpacing; + relativeOffsets[15 + i][1] = i * spacing; + } + // Only the classic Nano populates the 2x3 ICSP block; on every later board that end of the + // PCB carries the radio module, USB bridge or Qwiic socket instead. + if (getVersion() != NanoVersion.CLASSIC) { + if (getVersion() == NanoVersion.NANO_R4) { + // Centre of the Qwiic socket, matching where draw() places it + relativeOffsets[30][0] = rowSpacing / 2.0; + relativeOffsets[30][1] = -BOARD_MARGIN_Y.convertToPixels() + END_PART_OFFSET_Y.convertToPixels() + + QWIIC_LENGTH.convertToPixels() / 2.0; + } + return relativeOffsets; + } + + // ICSP header (2x3 pins, 30..35) flush with top edge: + // Outer row is 0.05" (10px) from top edge (-20px / -0.10" relative to Pin 0) + // Inner row is 0.15" (30px) from top edge (0px / 0.00" relative to Pin 0, aligned with Pin 0 & Pin 15) + double icspOuterY = -spacing; // -20px (-0.10") + double icspInnerY = 0; // 0px (0.00") + + double col0X = rowSpacing / 2.0 - spacing; // 40px (0.20") + double col1X = rowSpacing / 2.0; // 60px (0.30") + double col2X = rowSpacing / 2.0 + spacing; // 80px (0.40") + + // Pin 1 (MISO at col2X, outer row) + relativeOffsets[30] = new double[] {col2X, icspOuterY}; + // Pin 2 (5V_ICSP at col2X, inner row) + relativeOffsets[31] = new double[] {col2X, icspInnerY}; + // Pin 3 (SCK at col1X, outer row) + relativeOffsets[32] = new double[] {col1X, icspOuterY}; + // Pin 4 (MOSI at col1X, inner row) + relativeOffsets[33] = new double[] {col1X, icspInnerY}; + // Pin 5 (RST_ICSP at col0X, outer row) + relativeOffsets[34] = new double[] {col0X, icspOuterY}; + // Pin 6 (GND_ICSP at col0X, inner row) + relativeOffsets[35] = new double[] {col0X, icspInnerY}; + + return relativeOffsets; + } + + @Override + protected void updateControlPoints() { + rotatePoints(controlPoints[0], getRelativeOffsets()); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + double boardW = BOARD_WIDTH.convertToPixels(); // 146px (0.73") + double boardH = BOARD_LENGTH.convertToPixels(); // 340px (1.70") + double rowSpacing = new Size(0.60d, SizeUnit.in).convertToPixels(); // 120px (0.60") + double boardMarginX = (boardW - rowSpacing) / 2.0; // 13px (0.065") + double boardMarginY = new Size(0.15d, SizeUnit.in).convertToPixels(); // 30px (0.15") + double cornerRadius = new Size(1.0d, SizeUnit.mm).convertToPixels(); + + double boardX = x - boardMarginX; + double boardY = y - boardMarginY; + RoundRectangle2D outline = + new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, cornerRadius * 2, cornerRadius * 2); + if (getVersion() == NanoVersion.CLASSIC) { + return outline; + } + + // Every later Nano has castellated edges, so the pad columns bite notches out of both sides + double spacing = PIN_SPACING.convertToPixels(); + Area boardArea = new Area(outline); + subtractCastellationNotches(boardArea, y, PINS_PER_ROW, spacing, boardX); + subtractCastellationNotches(boardArea, y, PINS_PER_ROW, spacing, boardX + boardW); + return boardArea; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); // 146px (0.73") + double boardH = BOARD_LENGTH.convertToPixels(); // 340px (1.70") + double rowSpacing = new Size(0.60d, SizeUnit.in).convertToPixels(); + double boardMarginX = (boardW - rowSpacing) / 2.0; // 13px (0.065") + double boardMarginY = new Size(0.15d, SizeUnit.in).convertToPixels(); // 30px (0.15") + + double boardX = x - boardMarginX; + double boardY = y - boardMarginY; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + // Draw PCB body + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Corner Mounting Holes (non-plated drill holes matching Uno/Mega) + double holeDiameter = new Size(0.07d, SizeUnit.in).convertToPixels(); + double topHoleY = boardY + new Size(0.05d, SizeUnit.in).convertToPixels(); + double bottomHoleY = boardY + new Size(1.65d, SizeUnit.in).convertToPixels(); + double leftHoleX = boardX + boardMarginX; + double rightHoleX = boardX + boardW - boardMarginX; + + MakerBoardPainter.drawMountingHole(g2d, leftHoleX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, rightHoleX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, leftHoleX, bottomHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, rightHoleX, bottomHoleY, holeDiameter); + + // USB jack at bottom (plain metal connector) + UsbPortType usbType = getUsbPortType(); + Size[] usbSizes = getUsbPortSizes(usbType); + double usbW = usbSizes[0].convertToPixels(); + double usbH = usbSizes[1].convertToPixels(); + double usbOverhang = usbSizes[2].convertToPixels(); + MakerBoardPainter.drawMetalConnector(g2d, boardX + (boardW - usbW) / 2.0, + boardY + boardH - usbH + usbOverhang, usbW, usbH, "USB"); + + double chipSize = new Size(0.28d, SizeUnit.in).convertToPixels(); + double chipCenterX = boardX + boardW / 2.0; + double chipCenterY = boardY + new Size(1.06d, SizeUnit.in).convertToPixels(); + + if (isChipRotated()) { + // The ATmega328P on the classic Nano and the ATmega4809 on the Every are both mounted at + // 45 degrees + AffineTransform oldChipTx = g2d.getTransform(); + g2d.translate(chipCenterX, chipCenterY); + g2d.rotate(Math.PI / 4.0); + + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(-chipSize / 2.0, -chipSize / 2.0, chipSize, chipSize, 4, 4)); + g2d.setColor(IC_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(-chipSize / 2.0, -chipSize / 2.0, chipSize, chipSize, 4, 4)); + + // Pin 1 dot + g2d.setColor(PIN_MARKER_COLOR); + g2d.fill(new Ellipse2D.Double(-chipSize / 2.0 + 3, -chipSize / 2.0 + 3, 3, 3)); + + g2d.setColor(IC_TEXT_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, getVersion().getMcuLabel(), 0, 0, HorizontalAlignment.CENTER, + VerticalAlignment.CENTER); + + g2d.setTransform(oldChipTx); + } else if (getVersion().getMcuLabel() != null) { + // Later Nanos carry an axis-aligned QFN in the same spot, unless the MCU is a die inside + // the module at the far end of the board + MakerBoardPainter.drawChip(g2d, chipCenterX - chipSize / 2.0, chipCenterY - chipSize / 2.0, chipSize, chipSize, + getVersion().getMcuLabel()); + } + + // The end of the board opposite the USB jack: the classic Nano's ICSP header, and on every + // later board the part that identifies it at a glance + if (getVersion().getModuleLabel() != null) { + double moduleY = boardY + END_PART_OFFSET_Y.convertToPixels(); + switch (getVersion()) { + case EVERY: { + // A bare QFN USB bridge rather than a shielded module + double bridgeSize = new Size(5.0d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawChip(g2d, boardX + (boardW - bridgeSize) / 2.0, moduleY, bridgeSize, bridgeSize, + getVersion().getModuleLabel()); + break; + } + case NANO_R4: { + // The Qwiic I2C socket is a 4-way JST-SH in beige plastic, not a shielded module + double qwiicW = QWIIC_WIDTH.convertToPixels(); + double qwiicH = QWIIC_LENGTH.convertToPixels(); + double qwiicX = boardX + (boardW - qwiicW) / 2.0; + g2d.setColor(CONNECTOR_PLASTIC_COLOR); + g2d.fill(new Rectangle2D.Double(qwiicX, moduleY, qwiicW, qwiicH)); + g2d.setColor(CONNECTOR_PLASTIC_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(qwiicX, moduleY, qwiicW, qwiicH)); + + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, getVersion().getModuleLabel(), qwiicX + qwiicW / 2.0, + moduleY + qwiicH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + break; + } + default: { + double moduleW = new Size(10.0d, SizeUnit.mm).convertToPixels(); + double moduleH = new Size(11.0d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMetalConnector(g2d, boardX + (boardW - moduleW) / 2.0, moduleY, moduleW, moduleH, + getVersion().getModuleLabel()); + } + } + } + + // Reset Button + double btnW = BUTTON_WIDTH.convertToPixels(); + double btnH = BUTTON_LENGTH.convertToPixels(); + double btnX = boardX + (boardW - btnW) / 2.0; + double btnY = boardY + new Size(0.68d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH); + + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "RST", boardX + boardW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen, dropped 1mm below the nominal 0.50" so it clears the chips above it + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, getVersion().getSilkLabel(), boardX + boardW / 2.0, + boardY + new Size(0.50d, SizeUnit.in).convertToPixels() + new Size(1.0d, SizeUnit.mm).convertToPixels(), + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Pad names, printed between the two rows the way they are on the board + drawPinLabels(g2d, x, y, getRelativeOffsets(), PINS_PER_ROW, SILK_COLOR); + } + + // Castellated pads bite into the board edge, so they are drawn while the board rotation is + // still applied rather than from the already-rotated control points + if (!headers && getVersion() != NanoVersion.CLASSIC) { + double spacing = PIN_SPACING.convertToPixels(); + drawCastellatedPads(g2d, x, y, PINS_PER_ROW, spacing, boardX, outlineMode, drawingObserver); + drawCastellatedPads(g2d, x + rowSpacing, y, PINS_PER_ROW, spacing, boardX + boardW, outlineMode, + drawingObserver); + } + + g2d.setTransform(oldTx); + + // Draw pins or solder pads + if (headers) { + drawPinHeader(g2d, 0, getHeaderPinCount(), outlineMode, drawingObserver); + } else if (getVersion() == NanoVersion.CLASSIC) { + drawPcbSolderPads(g2d, 0, getHeaderPinCount(), true, outlineMode, drawingObserver); + } + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + double boardX = 6; + double boardY = 2; + double boardW = width - 12; + double boardH = height - 4; + + g2d.setColor(ARDUINO_TEAL); + g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4)); + g2d.setColor(ARDUINO_TEAL.darker()); + g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4)); + + // ICSP header at top + g2d.setColor(HEADER_BODY_COLOR); + g2d.fill(new Rectangle2D.Double(width / 2.0 - 4, boardY, 8, 4)); + + // Mini USB at bottom + g2d.setColor(USB_METAL_COLOR); + g2d.fillRect((int) (width / 2.0 - 4), (int) (boardY + boardH - 4), 8, 4); + + // 45-degree rotated diamond IC chip in center + AffineTransform oldTx = g2d.getTransform(); + g2d.translate(width / 2.0, boardY + boardH / 2.0 + 1); + g2d.rotate(Math.PI / 4.0); + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(-4, -4, 8, 8, 1, 1)); + g2d.setColor(IC_BORDER_COLOR); + g2d.draw(new RoundRectangle2D.Double(-4, -4, 8, 8, 1, 1)); + g2d.setTransform(oldTx); + + // Pin strips on sides + g2d.setColor(PIN_COLOR); + for (int y = 5; y < height - 5; y += 3) { + g2d.fillRect(7, y, 2, 2); + g2d.fillRect(width - 9, y, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java new file mode 100644 index 000000000..dc419ce75 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ArduinoUno.java @@ -0,0 +1,682 @@ +/* + * + * 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 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.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; +import java.util.Locale; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardLogos; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Arduino Uno", category = "Controllers", + author = "Branislav Stojkovic", + description = "Arduino Uno R3 / R4 WiFi / R4 Minima / Leonardo Microcontroller Board", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.BOARD, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ArduinoUno extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum ArduinoUnoVersion { + LEONARDO("Leonardo"), + REV3("UNO R3"), + R4_MINIMA("UNO R4 Minima"), + R4_WIFI("UNO R4 WiFi"); + + private String label; + + private ArduinoUnoVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + + private ArduinoUnoVersion version = ArduinoUnoVersion.REV3; + + public static Color SILK_COLOR = Color.WHITE; + + public static Size BOARD_WIDTH = new Size(68.6d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(53.4d, SizeUnit.mm); + + // Top left corner of the Minima's 10-pin SWD connector, measured from the board's top left corner + public static Size SWD_X = new Size(61.0d, SizeUnit.mm); + public static Size SWD_Y = new Size(9.5d, SizeUnit.mm); + public static Size SWD_WIDTH = new Size(2.5d, SizeUnit.mm); + public static Size SWD_HEIGHT = new Size(6.0d, SizeUnit.mm); + + // 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", + // 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", + // Main ICSP Header (32..37, ATmega328P) + "MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP", + // Top-Left ICSP Header (38..43, ATmega16U2) + "MISO_16U2", "5V_16U2", "SCK_16U2", "MOSI_16U2", "RST_16U2", "GND_16U2" + }; + + // What the board actually prints next to the headers, which is not the node name: both grounds + // of the power header are silkscreened "GND", the supply is "3V3" rather than "3.3V", the digital + // pins carry bare numbers with a "~" on the PWM-capable ones, and the reserved first power pin is + // left blank. The ICSP pins are printed nowhere on the board, so their entries only keep this + // array parallel to PIN_NAMES. + public static final String[] SILK_NAMES = new String[] { + // Power Header (0..7) + "", "IOREF", "RESET", "3V3", "5V", "GND", "GND", "VIN", + // Analog Header (8..13) + "A0", "A1", "A2", "A3", "A4", "A5", + // Digital Low (14..21) + "RX0", "TX1", "2", "~3", "4", "~5", "~6", "7", + // Digital High (22..31) + "8", "~9", "~10", "~11", "12", "13", "GND", "AREF", "SDA", "SCL", + // Main ICSP Header (32..37) + "MISO", "5V", "SCK", "MOSI", "RST", "GND", + // Top-Left ICSP Header (38..43) + "MISO", "5V", "SCK", "MOSI", "RST", "GND" + }; + + // The Leonardo has no ATmega16U2 - the 32U4 speaks USB natively - so it carries only the main + // 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", + // 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", + // Main ICSP Header (32..37, ATmega32U4) + "MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP" + }; + + public static final String[] SILK_NAMES_LEONARDO = new String[] { + // Power Header (0..7) + "", "IOREF", "RESET", "3V3", "5V", "GND", "GND", "VIN", + // Analog Header (8..13) + "A0", "A1", "A2", "A3", "A4", "A5", + // Digital Low (14..21) + "RX0", "TX1", "2", "~3", "4", "~5", "~6", "7", + // Digital High (22..31) + "8", "~9", "~10", "~11", "12", "13", "GND", "AREF", "SDA", "SCL", + // Main ICSP Header (32..37) + "MISO", "5V", "SCK", "MOSI", "RST", "GND" + }; + + // The Minima has no USB bridge chip either - the RA4M1 has native USB - so it drops the second + // ICSP header, and without the WiFi module it has no OFF / GND / VRTC header. In their place it + // 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", + // 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", + // Main ICSP Header (32..37, RA4M1) + "MISO", "5V_ICSP", "SCK", "MOSI", "RST_ICSP", "GND_ICSP", + // SWD Connector (38) + "SWD" + }; + + // Both R4 boards use the power header pin the R3 leaves reserved to select boot mode, and print + // "BOOT" next to it + public static final String[] SILK_NAMES_R4_MINIMA = new String[] { + // Power Header (0..7) + "BOOT", "IOREF", "RESET", "3V3", "5V", "GND", "GND", "VIN", + // Analog Header (8..13) + "A0", "A1", "A2", "A3", "A4", "A5", + // Digital Low (14..21) + "RX0", "TX1", "2", "~3", "4", "~5", "~6", "7", + // Digital High (22..31) + "8", "~9", "~10", "~11", "12", "13", "GND", "AREF", "SDA", "SCL", + // Main ICSP Header (32..37) + "MISO", "5V", "SCK", "MOSI", "RST", "GND", + // SWD Connector (38) + "SWD" + }; + + public static final String[] SILK_NAMES_R4_WIFI = new String[] { + // Power Header (0..7) + "BOOT", "IOREF", "RESET", "3V3", "5V", "GND", "GND", "VIN", + // Analog Header (8..13) + "A0", "A1", "A2", "A3", "A4", "A5", + // Digital Low (14..21) + "RX0", "TX1", "2", "~3", "4", "~5", "~6", "7", + // Digital High (22..31) + "8", "~9", "~10", "~11", "12", "13", "GND", "AREF", "SDA", "SCL", + // Main ICSP Header (32..37) + "MISO", "5V", "SCK", "MOSI", "RST", "GND", + // Top-Left ICSP Header (38..43) + "MISO", "5V", "SCK", "MOSI", "RST", "GND", + // RTC Header (44..46) + "OFF", "GND", "VRTC" + }; + + public ArduinoUno() { + super(); + this.bodyColor = ARDUINO_TEAL; + updateControlPoints(); + } + + @EditableProperty(name = "Version") + public ArduinoUnoVersion getVersion() { + if (version == null) { + version = ArduinoUnoVersion.REV3; + } + return version; + } + + public void setVersion(ArduinoUnoVersion version) { + this.version = version; + if (isR4()) { + this.bodyColor = ARDUINO_BLUE; + } else { + this.bodyColor = ARDUINO_TEAL; + } + updateControlPoints(); + invalidateCache(); + } + + /** + * True for the Renesas RA4M1 generation, which shares a USB-C connector and the RA4M1 in place of + * the ATmega328P. + */ + private boolean isR4() { + return getVersion() == ArduinoUnoVersion.R4_WIFI || getVersion() == ArduinoUnoVersion.R4_MINIMA; + } + + /** + * The second 2x3 ICSP header belongs to the USB bridge chip. The Leonardo's 32U4 and the Minima's + * RA4M1 speak USB natively, so neither board carries it. + */ + private boolean hasBridgeIcspHeader() { + return getVersion() == ArduinoUnoVersion.REV3 || getVersion() == ArduinoUnoVersion.R4_WIFI; + } + + private String[] getPinNames() { + switch (getVersion()) { + case LEONARDO: + return PIN_NAMES_LEONARDO; + case R4_MINIMA: + return PIN_NAMES_R4_MINIMA; + default: + return PIN_NAMES; + } + } + + private String[] getSilkNames() { + switch (getVersion()) { + case LEONARDO: + return SILK_NAMES_LEONARDO; + case R4_MINIMA: + return SILK_NAMES_R4_MINIMA; + case R4_WIFI: + return SILK_NAMES_R4_WIFI; + default: + return SILK_NAMES; + } + } + + @Override + protected String getSilkPinLabel(int index) { + String[] silkNames = getSilkNames(); + if (index >= 0 && index < silkNames.length) { + return silkNames[index]; + } + return super.getSilkPinLabel(index); + } + + @Override + public String getControlPointNodeName(int index) { + // The WiFi shares the R3 pin table except for the reserved power header pin, which both R4 + // boards use for boot mode selection - the Minima says so in its own table + if (index == 0 && getVersion() == ArduinoUnoVersion.R4_WIFI) { + return "BOOT"; + } + String[] pinNames = getPinNames(); + if (index >= 0 && index < pinNames.length) { + return pinNames[index]; + } + if (getVersion() == ArduinoUnoVersion.R4_WIFI) { + if (index == 44) return "OFF"; + if (index == 45) return "GND"; + if (index == 46) return "VRTC"; + } + return "Pin " + (index + 1); + } + + private double[][] getRelativeOffsets() { + double spacing = PIN_SPACING.convertToPixels(); // 20px for 0.1" (100 mils) + double gap02 = new Size(0.2d, SizeUnit.in).convertToPixels(); // 40px for 0.2" + + // Reference: Pin 0 (NC) is at (boardX + 220.0, boardY + 400.0) [1100 mils, 100 mils from bottom-left] + // Board is 539.78px wide, 419.78px high + // Only the WiFi adds the OFF / GND / VRTC header on top of the names listed for the version + int numPins = + getVersion() == ArduinoUnoVersion.R4_WIFI ? PIN_NAMES.length + 3 : getPinNames().length; + double[][] relativeOffsets = new double[numPins][2]; + + // Power header pins 0..7 (bottom left row) + for (int i = 0; i < 8; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + // Analog pins 8..13 (bottom right row, separated from VIN by 0.2" = 40px) + double analogStartX = 7 * spacing + gap02; // 180px + for (int i = 0; i < 6; i++) { + relativeOffsets[8 + i][0] = analogStartX + i * spacing; + relativeOffsets[8 + i][1] = 0; + } + // Digital Low pins 14..21 (top right row: D0..D7) + double topRowY = -new Size(1.9d, SizeUnit.in).convertToPixels(); // -380px + double d0X = new Size(1.4d, SizeUnit.in).convertToPixels(); // 280px (aligns with A5) + for (int i = 0; i < 8; i++) { + relativeOffsets[14 + i][0] = d0X - i * spacing; + relativeOffsets[14 + i][1] = topRowY; + } + // Digital High pins 22..31 (top left row: D8..D13, GND, AREF, SDA, SCL) + // Gap between D7 (140px) and D8 is 0.16" = 32px + double d7X = 7 * spacing; // 140px + double d7d8Gap = new Size(0.16d, SizeUnit.in).convertToPixels(); // 32px + double d8X = d7X - d7d8Gap; // 108px + for (int i = 0; i < 10; i++) { + relativeOffsets[22 + i][0] = d8X - i * spacing; + relativeOffsets[22 + i][1] = topRowY; + } + // Main ICSP header (2x3 pins, 32..37) located on right side of board + double icspX = new Size(1.405d, SizeUnit.in).convertToPixels(); // 281.0 px + double icspY = -new Size(1.1d, SizeUnit.in).convertToPixels(); // -220.0 px + relativeOffsets[32] = new double[] {icspX, icspY}; + relativeOffsets[33] = new double[] {icspX + spacing, icspY}; + relativeOffsets[34] = new double[] {icspX, icspY + spacing}; + relativeOffsets[35] = new double[] {icspX + spacing, icspY + spacing}; + relativeOffsets[36] = new double[] {icspX, icspY + 2 * spacing}; + relativeOffsets[37] = new double[] {icspX + spacing, icspY + 2 * spacing}; + + // Top-Left ICSP header (2x3 pins, 38..43) for ATmega16U2 + if (hasBridgeIcspHeader()) { + double icsp2X = -new Size(0.28d, SizeUnit.in).convertToPixels(); // -56.0 px + // far enough below the digital header for its rotated pin names to fit above the connector + double icsp2Y = -new Size(1.77d, SizeUnit.in).convertToPixels() + + new Size(3.5d, SizeUnit.mm).convertToPixels(); + relativeOffsets[38] = new double[] {icsp2X, icsp2Y}; + relativeOffsets[39] = new double[] {icsp2X, icsp2Y + spacing}; + relativeOffsets[40] = new double[] {icsp2X - spacing, icsp2Y}; + relativeOffsets[41] = new double[] {icsp2X - spacing, icsp2Y + spacing}; + relativeOffsets[42] = new double[] {icsp2X - 2 * spacing, icsp2Y}; + relativeOffsets[43] = new double[] {icsp2X - 2 * spacing, icsp2Y + spacing}; + } + + if (getVersion() == ArduinoUnoVersion.R4_WIFI) { + relativeOffsets[44] = new double[] {-4 * spacing, 0}; // OFF + relativeOffsets[45] = new double[] {-3 * spacing, 0}; // GND + relativeOffsets[46] = new double[] {-2 * spacing, 0}; // VRTC + } + + // The SWD connector (38) gets a single point in the middle of its body, the way the Pi models + // its flat-flex connectors, rather than ten points 1.27mm apart that nothing could reach + if (getVersion() == ArduinoUnoVersion.R4_MINIMA) { + relativeOffsets[38] = new double[] { + SWD_X.convertToPixels() + SWD_WIDTH.convertToPixels() / 2.0 + - new Size(1.1d, SizeUnit.in).convertToPixels(), + SWD_Y.convertToPixels() + SWD_HEIGHT.convertToPixels() / 2.0 + - new Size(2.0d, SizeUnit.in).convertToPixels()}; + } + + return relativeOffsets; + } + + @Override + protected void updateControlPoints() { + rotatePoints(controlPoints[0], getRelativeOffsets()); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardX = x - new Size(1.1d, SizeUnit.in).convertToPixels(); + double boardY = y - new Size(2.0d, SizeUnit.in).convertToPixels(); + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + + double notchInset = new Size(0.1d, SizeUnit.in).convertToPixels(); + double topCutoff = new Size(0.16d, SizeUnit.in).convertToPixels(); + double y12 = new Size(0.06d, SizeUnit.in).convertToPixels(); + double y102 = new Size(0.51d, SizeUnit.in).convertToPixels(); + double y122 = new Size(0.61d, SizeUnit.in).convertToPixels(); + double y380 = new Size(1.9d, SizeUnit.in).convertToPixels(); + double y400 = new Size(2.0d, SizeUnit.in).convertToPixels(); + double cornerRadius = new Size(1.0d, SizeUnit.mm).convertToPixels(); + double c1 = cornerRadius * (1 - 0.5523); + double c2 = cornerRadius * 0.5523; + double y412 = boardH - cornerRadius; + + Path2D.Double path = new Path2D.Double(); + path.moveTo(boardX + boardW - topCutoff, boardY); + path.lineTo(boardX + boardW - notchInset, boardY + y12); + path.lineTo(boardX + boardW - notchInset, boardY + y102); + path.lineTo(boardX + boardW, boardY + y122); + path.lineTo(boardX + boardW, boardY + y380); + path.lineTo(boardX + boardW - notchInset, boardY + y400); + path.lineTo(boardX + boardW - notchInset, boardY + y412); + path.curveTo(boardX + boardW - notchInset, boardY + boardH - c1, boardX + boardW - notchInset - c2, boardY + boardH, boardX + boardW - notchInset - cornerRadius, boardY + boardH); + path.lineTo(boardX + cornerRadius, boardY + boardH); + path.curveTo(boardX + c1, boardY + boardH, boardX, boardY + boardH - c1, boardX, boardY + boardH - cornerRadius); + path.lineTo(boardX, boardY + cornerRadius); + path.curveTo(boardX, boardY + c1, boardX + c1, boardY, boardX + cornerRadius, boardY); + path.lineTo(boardX + boardW - topCutoff, boardY); + path.closePath(); + return path; + } + + @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); + } + + // Board bounding origin + double boardX = x - new Size(1.1d, SizeUnit.in).convertToPixels(); + double boardY = y - new Size(2.0d, SizeUnit.in).convertToPixels(); + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + // Draw PCB body + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Mounting holes + double holeDiameter = new Size(0.12d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.1d, SizeUnit.in).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(0.55d, SizeUnit.in).convertToPixels(), boardY + new Size(2.0d, SizeUnit.in).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(2.6d, SizeUnit.in).convertToPixels(), boardY + new Size(0.7d, SizeUnit.in).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(2.6d, SizeUnit.in).convertToPixels(), boardY + new Size(1.8d, SizeUnit.in).convertToPixels(), holeDiameter); + + if (isR4()) { + double usbCW = USB_C_WIDTH.convertToPixels(); + double usbCL = USB_C_LENGTH.convertToPixels(); + double usbCY = boardY + new Size(15.5d, SizeUnit.mm).convertToPixels() - usbCW / 2.0; + MakerBoardPainter.drawUsbC(g2d, boardX - USB_C_OVERHANG.convertToPixels(), usbCY, usbCL, usbCW, "USB-C"); + } else if (getVersion() == ArduinoUnoVersion.LEONARDO) { + double microW = USB_MICRO_LENGTH.convertToPixels(); + double microH = USB_MICRO_WIDTH.convertToPixels(); + double microY = boardY + new Size(0.6d, SizeUnit.in).convertToPixels() - microH / 2.0; + MakerBoardPainter.drawMicroUsb(g2d, boardX - USB_MICRO_OVERHANG.convertToPixels(), microY, microW, microH, "USB"); + } else { + MakerBoardPainter.drawUsbB(g2d, boardX - USB_B_OVERHANG.convertToPixels(), + boardY + new Size(0.375d, SizeUnit.in).convertToPixels(), + USB_B_LENGTH.convertToPixels(), + USB_B_WIDTH.convertToPixels(), "USB"); + } + + MakerBoardPainter.drawChip(g2d, boardX - new Size(0.07d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.615d, SizeUnit.in).convertToPixels(), + new Size(0.52d, SizeUnit.in).convertToPixels(), + new Size(0.355d, SizeUnit.in).convertToPixels(), "DC IN"); + + if (isR4()) { + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + + // The Minima is the same board without the radio: no antenna and no ESP32-S3 module + if (getVersion() == ArduinoUnoVersion.R4_WIFI) { + double antW = new Size(5d, SizeUnit.mm).convertToPixels(); + double antH = new Size(15d, SizeUnit.mm).convertToPixels(); + double antX = boardX; + double antY = boardY + boardH - new Size(13.5d, SizeUnit.mm).convertToPixels() - antH; + MakerBoardPainter.drawPcbAntenna(g2d, antX, antY, antW, antH); + + double s3W = new Size(14d, SizeUnit.mm).convertToPixels(); + double s3H = new Size(14d, SizeUnit.mm).convertToPixels(); + double s3X = boardX + new Size(5.5d, SizeUnit.mm).convertToPixels(); + double s3Y = boardY + boardH - new Size(14.0d, SizeUnit.mm).convertToPixels() - s3H; + MakerBoardPainter.drawMetalConnector(g2d, s3X, s3Y, s3W, s3H, "ESP32-S3"); + } + + double raW = new Size(10d, SizeUnit.mm).convertToPixels(); + double raH = new Size(10d, SizeUnit.mm).convertToPixels(); + // The Minima carries the RA4M1 further from the top right corner than the WiFi does + boolean minima = getVersion() == ArduinoUnoVersion.R4_MINIMA; + double raShiftX = minima ? new Size(8d, SizeUnit.mm).convertToPixels() : 0; + double raShiftY = minima ? new Size(4.5d, SizeUnit.mm).convertToPixels() : 0; + double raX = boardX + boardW - new Size(11.5d, SizeUnit.mm).convertToPixels() - raW - raShiftX; + double raY = boardY + new Size(12d, SizeUnit.mm).convertToPixels() + raShiftY; + g2d.setColor(Color.BLACK); + g2d.fill(new RoundRectangle2D.Double(raX, raY, raW, raH, 2, 2)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new RoundRectangle2D.Double(raX, raY, raW, raH, 2, 2)); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "RA4M1", raX + raW / 2.0, raY + raH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + if (getVersion() == ArduinoUnoVersion.R4_MINIMA) { + double swdW = SWD_WIDTH.convertToPixels(); + double swdH = SWD_HEIGHT.convertToPixels(); + double swdX = boardX + SWD_X.convertToPixels(); + double swdY = boardY + SWD_Y.convertToPixels(); + MakerBoardPainter.drawFpcConnector(g2d, swdX, swdY, swdW, swdH, true, ""); + + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "SWD", swdX + swdW / 2.0, + swdY + swdH + new Size(1.5d, SizeUnit.mm).convertToPixels(), + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + } else if (getVersion() == ArduinoUnoVersion.LEONARDO) { + // The 32U4 is a square TQFP-44 where the R3 carries its DIP-28 + double chipSize = new Size(0.4d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawChip(g2d, boardX + new Size(1.62d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.255d, SizeUnit.in).convertToPixels(), chipSize, chipSize, "32U4"); + } else { + MakerBoardPainter.drawChip(g2d, boardX + new Size(1.095d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.345d, SizeUnit.in).convertToPixels(), + new Size(1.46d, SizeUnit.in).convertToPixels(), + new Size(0.22d, SizeUnit.in).convertToPixels(), "ATmega328P"); + } + + // Reset Button near USB + double btnW = BUTTON_WIDTH.convertToPixels(); + double btnH = BUTTON_LENGTH.convertToPixels(); + double btnX = boardX + new Size(0.235d, SizeUnit.in).convertToPixels() - btnW / 2.0; + double btnY = boardY + new Size(0.1d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH); + + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "RST", btnX + btnW / 2.0, + btnY + btnH + new Size(1.0d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // The Minima prints its branding lower, in the space the WiFi gives to the radio module + double brandShiftY = getVersion() == ArduinoUnoVersion.R4_MINIMA + ? new Size(24d, SizeUnit.mm).convertToPixels() : 0; + + // Arduino Infinity Logo + MakerBoardLogos.drawArduinoLogo(g2d, boardX + new Size(1.26d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels(), + boardY + new Size(0.4411d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels() + brandShiftY); + + // Silkscreen text & branding + g2d.setColor(SILK_COLOR); + + double arduinoX = boardX + new Size(1.5d, SizeUnit.in).convertToPixels() - new Size(3.0d, SizeUnit.mm).convertToPixels(); + double arduinoY = boardY + new Size(0.775d, SizeUnit.in).convertToPixels() - new Size(4.0d, SizeUnit.mm).convertToPixels() + brandShiftY; + g2d.setFont(SILK_FONT_LARGE.deriveFont(SILK_FONT_LARGE.getSize2D() * 1.5f)); + StringUtils.drawCenteredText(g2d, "ARDUINO", arduinoX, arduinoY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + String subText = getVersion().toString().toUpperCase(Locale.ROOT); + double subTextY = arduinoY + new Size(2.5d, SizeUnit.mm).convertToPixels(); + g2d.setFont(SILK_FONT_LARGE); + StringUtils.drawCenteredText(g2d, subText, arduinoX, subTextY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Header silkscreen labels + g2d.setFont(SILK_FONT_SMALL); + // the header captions sit beyond the rotated pin names, which take up the space right next to + // the headers themselves + StringUtils.drawCenteredText(g2d, "POWER", boardX + new Size(1.45d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.75d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "ANALOG IN", boardX + new Size(2.25d, SizeUnit.in).convertToPixels(), + boardY + new Size(1.75d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "DIGITAL (PWM ~)", boardX + new Size(2.05d, SizeUnit.in).convertToPixels(), + boardY + new Size(0.34d, SizeUnit.in).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Text above right-most 3x2 header (ICSP / SPI) + String icspText = isR4() ? "SPI" : "ICSP"; + double icspLabelX = boardX + new Size(2.505d, SizeUnit.in).convertToPixels() + new Size(0.05d, SizeUnit.in).convertToPixels(); + double icspLabelY = boardY + new Size(0.9d, SizeUnit.in).convertToPixels() - new Size(2.5d, SizeUnit.mm).convertToPixels(); + StringUtils.drawCenteredText(g2d, icspText, icspLabelX, icspLabelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Header pin names, printed above the power / analog row and below the digital row the way + // they are silkscreened on the board + double[][] relativeOffsets = getRelativeOffsets(); + drawRowPinLabels(g2d, x, y, relativeOffsets, 0, 14, false, SILK_COLOR); + drawRowPinLabels(g2d, x, y, relativeOffsets, 14, 18, true, SILK_COLOR); + if (getVersion() == ArduinoUnoVersion.R4_WIFI) { + drawRowPinLabels(g2d, x, y, relativeOffsets, 44, 3, false, SILK_COLOR); + } + } + + g2d.setTransform(oldTx); + + // Draw header pins with continuity tracking; the Minima's SWD point sits inside the connector + // body drawn above and gets no header pin of its own + int headerPinCount = getVersion() == ArduinoUnoVersion.R4_MINIMA + ? controlPoints.length - 1 : controlPoints.length; + drawPinHeader(g2d, 0, headerPinCount, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + double boardX = 5; + double boardY = 3; + double boardW = width - boardX - 2; + double boardH = height - 6; + + // USB Type-B Jack (metallic silver, top left protruding) + double usbW = 7; + double usbH = 8; + double usbX = 1; + double usbY = boardY + 2; + + // DC Power Jack (dark body, bottom left protruding) + double dcW = 7; + double dcH = 6; + double dcX = 1; + double dcY = boardY + boardH - dcH - 3; + + // Board PCB + g2d.setColor(this.bodyColor); + g2d.fill(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4)); + g2d.setColor(this.bodyColor.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4)); + + // Draw USB Jack + g2d.setColor(USB_METAL_COLOR); + g2d.fill(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2)); + g2d.setColor(METAL_SHIELD_BORDER); + g2d.draw(new RoundRectangle2D.Double(usbX, usbY, usbW, usbH, 2, 2)); + + // Draw DC Jack + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2)); + g2d.setColor(Color.BLACK); + g2d.draw(new RoundRectangle2D.Double(dcX, dcY, dcW, dcH, 2, 2)); + + // Arduino Infinity logo + double scale = 15.0 / 95.56; + double logoW = 95.56 * scale; + double logoH = 45.33 * scale; + double logoX = boardX + (boardW - logoW) / 2.0 + 1.0; + double logoY = boardY + 3.5; + MakerBoardLogos.drawArduinoLogo(g2d, logoX, logoY, scale); + + // UNO text below logo + g2d.setColor(SILK_COLOR); + String iconText = getVersion().toString().toUpperCase(Locale.ROOT).replace("UNO R4 ", "R4 "); + int fontSize = Math.max(5, (int) Math.round(boardH * 0.20)); + g2d.setFont(new Font("SansSerif", Font.BOLD, fontSize)); + double textY = logoY + logoH + (boardY + boardH - (logoY + logoH)) / 2.0; + double textX = boardX + (boardW / 2.0) + 0.5; + StringUtils.drawCenteredText(g2d, iconText, textX, textY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} 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 new file mode 100644 index 000000000..eb8ce412f --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP32DevKit.java @@ -0,0 +1,862 @@ +/* + * + * 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 java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "ESP32 DevKit", category = "Controllers", + author = "Branislav Stojkovic", + description = "ESP32 DevKit Wi-Fi and Bluetooth Development Board " + + "(30-Pin / 38-Pin / 44-Pin S3 / 30-Pin C3 / 32-Pin C6)", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ESP32DevKit extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum DevKitVersion { + DevKit_V1_30Pin("ESP32 DevKit V1 (30-Pin)"), + DevKitC_V4_38Pin("ESP32 DevKitC V4 (38-Pin)"), + ESP32_S3_DevKitC_44Pin("ESP32-S3 DevKitC-1 (44-Pin)"), + ESP32_C3_DevKitM_1("ESP32-C3 DevKitM-1 (30-Pin)"), + ESP32_C6_DevKitC_1("ESP32-C6 DevKitC-1 (32-Pin)"); + + private final String label; + + DevKitVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + + public static Color ESP_BLACK = Color.decode("#3E3E3E"); + public static Color ANTENNA_BG_COLOR = Color.decode("#1E1E1E"); + public static Color SILK_COLOR = Color.WHITE; + + public static Size BOARD_WIDTH_30 = new Size(28.33d, SizeUnit.mm); + public static Size BOARD_LENGTH_30 = new Size(51.45d, SizeUnit.mm); + public static Size TOP_MARGIN_30 = new Size(6.7d, SizeUnit.mm); + public static Size ANTENNA_LENGTH = new Size(7.0d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size SHIELD_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size SHIELD_LENGTH = new Size(18.0d, SizeUnit.mm); + public static Size HOLE_DIAMETER_30 = new Size(2.8d, SizeUnit.mm); + public static Size HOLE_EDGE_MARGIN_30 = new Size(0.8d, SizeUnit.mm); + + // 38-Pin (DevKitC V4) Dimensions + public static Size BOARD_WIDTH_38 = new Size(27.9d, SizeUnit.mm); + public static Size BOARD_LENGTH_38 = new Size(54.4d, SizeUnit.mm); + public static Size MAIN_BODY_LENGTH_38 = new Size(48.2d, SizeUnit.mm); + public static Size ANTENNA_LENGTH_38 = new Size(6.2d, SizeUnit.mm); + + // Common Spacing + public static Size ROW_SPACING = new Size(1.0d, SizeUnit.in); + + // ESP32-S3 DevKitC-1 (44-Pin) Dimensions + // BOARD_LENGTH_S3 is the main PCB body only; antenna extends above separately + public static Size BOARD_WIDTH_S3 = new Size(25.40d, SizeUnit.mm); + public static Size BOARD_LENGTH_S3 = new Size(62.74d, SizeUnit.mm); + public static Size ROW_SPACING_S3 = new Size(22.86d, SizeUnit.mm); + public static Size TOP_MARGIN_S3 = new Size(1.40d, SizeUnit.mm); + public static Size BOTTOM_MARGIN_S3 = new Size(8.0d, SizeUnit.mm); + public static Size ANTENNA_WIDTH_S3 = new Size(18.0d, SizeUnit.mm); + public static Size ANTENNA_LENGTH_S3 = new Size(6.0d, SizeUnit.mm); + public static Size SHIELD_WIDTH_S3 = new Size(16.0d, SizeUnit.mm); + public static Size SHIELD_LENGTH_S3 = new Size(16.0d, SizeUnit.mm); + + // Node names follow the convention used by the other boards in this package: the name a pin is + // identified by, followed by its alternate functions in parentheses. The silkscreen prints only + // a bare token, so each variant carries a parallel SILK_NAMES array with what is on the board. + // Function lists come from the Espressif esp-dev-kits user guides for the DevKitC boards; the + // DevKit V1 is a DOIT board with no vendor documentation, so its functions are those of the + // ESP32-WROOM-32 module it carries. + public static final String[] PIN_NAMES_30 = new String[] { + // Left row (pins 0..14, top to bottom) + "EN (CHIP_PU, Reset)", "VP (GPIO36, ADC1_CH0, S_VP)", "VN (GPIO39, ADC1_CH3, S_VN)", + "GPIO34 (ADC1_CH6, VDET_1)", "GPIO35 (ADC1_CH7, VDET_2)", + "GPIO32 (ADC1_CH4, TOUCH_CH9, XTAL_32K_P)", "GPIO33 (ADC1_CH5, TOUCH_CH8, XTAL_32K_N)", + "GPIO25 (ADC2_CH8, DAC_1)", "GPIO26 (ADC2_CH9, DAC_2)", "GPIO27 (ADC2_CH7, TOUCH_CH7)", + "GPIO14 (ADC2_CH6, TOUCH_CH6, MTMS)", "GPIO12 (ADC2_CH5, TOUCH_CH5, MTDI)", + "GPIO13 (ADC2_CH4, TOUCH_CH4, MTCK)", "GND_1", "VIN", + // Right row (pins 15..29, top to bottom) + "GPIO23", "GPIO22 (SCL)", "TX0 (GPIO1, U0TXD)", "RX0 (GPIO3, U0RXD)", "GPIO21 (SDA)", + "GPIO19", "GPIO18", "GPIO5", "TX2 (GPIO17, U2TXD)", "RX2 (GPIO16, U2RXD)", + "GPIO4 (ADC2_CH0, TOUCH_CH0)", "GPIO2 (ADC2_CH2, TOUCH_CH2)", + "GPIO15 (ADC2_CH3, TOUCH_CH3, MTDO)", "GND_2", "3V3" + }; + + public static final String[] SILK_NAMES_30 = new String[] { + // Left row (pins 0..14, top to bottom) + "EN", "VP", "VN", "D34", "D35", "D32", "D33", "D25", "D26", "D27", "D14", "D12", "D13", "GND", "VIN", + // Right row (pins 15..29, top to bottom) + "D23", "D22", "TX0", "RX0", "D21", "D19", "D18", "D5", "TX2", "RX2", "D4", "D2", "D15", "GND", "3V3" + }; + + public static final String[] PIN_NAMES_38 = new String[] { + // Left row (J2: pins 0..18, top to bottom) + "3V3", "EN (CHIP_PU, Reset)", "VP (GPIO36, ADC1_CH0, S_VP)", "VN (GPIO39, ADC1_CH3, S_VN)", + "GPIO34 (ADC1_CH6, VDET_1)", "GPIO35 (ADC1_CH7, VDET_2)", + "GPIO32 (ADC1_CH4, TOUCH_CH9, XTAL_32K_P)", "GPIO33 (ADC1_CH5, TOUCH_CH8, XTAL_32K_N)", + "GPIO25 (ADC2_CH8, DAC_1)", "GPIO26 (ADC2_CH9, DAC_2)", "GPIO27 (ADC2_CH7, TOUCH_CH7)", + "GPIO14 (ADC2_CH6, TOUCH_CH6, MTMS)", "GPIO12 (ADC2_CH5, TOUCH_CH5, MTDI)", "GND_1", + "GPIO13 (ADC2_CH4, TOUCH_CH4, MTCK)", "D2 (GPIO9, flash)", "D3 (GPIO10, flash)", + "CMD (GPIO11, flash)", "5V", + // Right row (J3: pins 19..37, top to bottom) + "GND_2", "GPIO23", "GPIO22", "TX (GPIO1, U0TXD)", "RX (GPIO3, U0RXD)", "GPIO21", "GND_3", + "GPIO19", "GPIO18", "GPIO5", "GPIO17", "GPIO16", "GPIO4 (ADC2_CH0, TOUCH_CH0)", + "GPIO0 (ADC2_CH1, TOUCH_CH1, Boot)", "GPIO2 (ADC2_CH2, TOUCH_CH2)", + "GPIO15 (ADC2_CH3, TOUCH_CH3, MTDO)", "D1 (GPIO8, flash)", "D0 (GPIO7, flash)", + "CLK (GPIO6, flash)" + }; + + public static final String[] SILK_NAMES_38 = new String[] { + // Left row (J2: pins 0..18, top to bottom) + "3V3", "EN", "VP", "VN", "IO34", "IO35", "IO32", "IO33", "IO25", "IO26", "IO27", "IO14", + "IO12", "GND", "IO13", "D2", "D3", "CMD", "5V", + // Right row (J3: pins 19..37, top to bottom) + "GND", "IO23", "IO22", "TX", "RX", "IO21", "GND", "IO19", "IO18", "IO5", "IO17", "IO16", + "IO4", "IO0", "IO2", "IO15", "D1", "D0", "CLK" + }; + + // ESP32-C3-DevKitM-1 and ESP32-C6-DevKitC-1 share the DevKitC form factor: 25.4 mm wide with + // 0.9" between the pin rows, an on-module PCB antenna at the top and the connectors at the + // bottom. Espressif publishes their outlines only as dimension drawings, so the body length is + // derived from the pin field plus the same top and bottom margins the S3 DevKitC-1 uses. + public static Size BOARD_WIDTH_RISCV = new Size(25.40d, SizeUnit.mm); + public static Size TOP_MARGIN_RISCV = new Size(1.40d, SizeUnit.mm); + public static Size BOTTOM_MARGIN_RISCV = new Size(8.0d, SizeUnit.mm); + + // Module footprints are split into the shielded can and the antenna above it, so shield plus + // antenna adds up to the module's overall length: ESP32-C3-MINI-1 is 13.2 x 16.6 mm and + // ESP32-C6-WROOM-1 is 18.0 x 25.5 mm. + public static Size SHIELD_WIDTH_C3 = new Size(13.2d, SizeUnit.mm); + public static Size SHIELD_LENGTH_C3 = new Size(10.6d, SizeUnit.mm); + public static Size ANTENNA_WIDTH_C3 = new Size(13.2d, SizeUnit.mm); + public static Size ANTENNA_LENGTH_C3 = new Size(6.0d, SizeUnit.mm); + + public static Size SHIELD_WIDTH_C6 = new Size(18.0d, SizeUnit.mm); + public static Size SHIELD_LENGTH_C6 = new Size(19.5d, SizeUnit.mm); + public static Size ANTENNA_WIDTH_C6 = new Size(18.0d, SizeUnit.mm); + public static Size ANTENNA_LENGTH_C6 = new Size(6.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES_S3_44 = new String[] { + // Left row (J1: pins 0..21, top to bottom) + "3V3_1", "3V3_2", "RST (EN)", "GPIO4 (RTC_GPIO4, TOUCH4, ADC1_CH3)", + "GPIO5 (RTC_GPIO5, TOUCH5, ADC1_CH4)", "GPIO6 (RTC_GPIO6, TOUCH6, ADC1_CH5)", + "GPIO7 (RTC_GPIO7, TOUCH7, ADC1_CH6)", "GPIO15 (U0RTS, ADC2_CH4, XTAL_32K_P)", + "GPIO16 (U0CTS, ADC2_CH5, XTAL_32K_N)", "GPIO17 (U1TXD, ADC2_CH6)", + "GPIO18 (U1RXD, ADC2_CH7, CLK_OUT3)", "GPIO8 (TOUCH8, ADC1_CH7, SUBSPICS1)", + "GPIO3 (RTC_GPIO3, TOUCH3, ADC1_CH2)", "GPIO46", + "GPIO9 (TOUCH9, ADC1_CH8, FSPIHD)", "GPIO10 (TOUCH10, ADC1_CH9, FSPICS0)", + "GPIO11 (TOUCH11, ADC2_CH0, FSPID)", "GPIO12 (TOUCH12, ADC2_CH1, FSPICLK)", + "GPIO13 (TOUCH13, ADC2_CH2, FSPIQ)", "GPIO14 (TOUCH14, ADC2_CH3, FSPIWP)", "5V", "GND_1", + // Right row (J3: pins 22..43, top to bottom) + "GND_2", "TX (GPIO43, U0TXD, CLK_OUT1)", "RX (GPIO44, U0RXD, CLK_OUT2)", + "GPIO1 (RTC_GPIO1, TOUCH1, ADC1_CH0)", "GPIO2 (RTC_GPIO2, TOUCH2, ADC1_CH1)", + "GPIO42 (MTMS)", "GPIO41 (MTDI, CLK_OUT1)", "GPIO40 (MTDO, CLK_OUT2)", + "GPIO39 (MTCK, CLK_OUT3, SUBSPICS1)", "GPIO38 (FSPIWP, RGB LED)", "GPIO37 (SPIDQS, FSPIQ)", + "GPIO36 (SPIIO7, FSPICLK)", "GPIO35 (SPIIO6, FSPID)", "GPIO0 (RTC_GPIO0, Boot)", "GPIO45", + "GPIO48 (SPICLK_N)", "GPIO47 (SPICLK_P)", "GPIO21 (RTC_GPIO21)", + "GPIO20 (U1CTS, ADC2_CH9, USB_D+)", "GPIO19 (U1RTS, ADC2_CH8, USB_D-)", "GND_3", "GND_4" + }; + + public static final String[] SILK_NAMES_S3_44 = new String[] { + // Left row (J1: pins 0..21, top to bottom) + "3V3", "3V3", "RST", "4", "5", "6", "7", "15", "16", "17", "18", "8", "3", "46", "9", "10", + "11", "12", "13", "14", "5V", "G", + // Right row (J3: pins 22..43, top to bottom) + "G", "TX", "RX", "1", "2", "42", "41", "40", "39", "38", "37", "36", "35", "0", "45", "48", + "47", "21", "20", "19", "G", "G" + }; + + public static final String[] PIN_NAMES_C3_30 = new String[] { + // J1 (left row: pins 0..14, top to bottom) + "GND_1", "3V3_1", "3V3_2", "GPIO2 (ADC1_CH2, FSPIQ)", "GPIO3 (ADC1_CH3)", "GND_2", + "RST (CHIP_PU)", "GND_3", "GPIO0 (ADC1_CH0, XTAL_32K_P)", "GPIO1 (ADC1_CH1, XTAL_32K_N)", + "GPIO10 (FSPICS0)", "GND_4", "5V_1", "5V_2", "GND_5", + // J3 (right row: pins 15..29, top to bottom) + "GND_6", "TX (GPIO21, U0TXD)", "RX (GPIO20, U0RXD)", "GND_7", "GPIO9", "GPIO8 (RGB LED)", + "GND_8", "GPIO7 (FSPID, MTDO)", "GPIO6 (FSPICLK, MTCK)", "GPIO5 (ADC2_CH0, FSPIWP, MTDI)", + "GPIO4 (ADC1_CH4, FSPIHD, MTMS)", "GND_9", "GPIO18 (USB_D-)", "GPIO19 (USB_D+)", "GND_10" + }; + + public static final String[] SILK_NAMES_C3_30 = new String[] { + // J1 (left row: pins 0..14, top to bottom) + "GND", "3V3", "3V3", "IO2", "IO3", "GND", "RST", "GND", "IO0", "IO1", "IO10", "GND", "5V", + "5V", "GND", + // J3 (right row: pins 15..29, top to bottom) + "GND", "TX", "RX", "GND", "IO9", "IO8", "GND", "IO7", "IO6", "IO5", "IO4", "GND", "IO18", + "IO19", "GND" + }; + + public static final String[] PIN_NAMES_C6_32 = new String[] { + // J1 (left row: pins 0..15, top to bottom) + "3V3", "RST", "GPIO4 (MTMS, LP_GPIO4, LP_UART_RXD, ADC1_CH4, FSPIHD)", + "GPIO5 (MTDI, LP_GPIO5, LP_UART_TXD, ADC1_CH5, FSPIWP)", + "GPIO6 (MTCK, LP_GPIO6, LP_I2C_SDA, ADC1_CH6, FSPICLK)", + "GPIO7 (MTDO, LP_GPIO7, LP_I2C_SCL, FSPID)", + "GPIO0 (XTAL_32K_P, LP_GPIO0, LP_UART_DTRN, ADC1_CH0)", + "GPIO1 (XTAL_32K_N, LP_GPIO1, LP_UART_DSRN, ADC1_CH1)", "GPIO8 (RGB LED)", "GPIO10", + "GPIO11", "GPIO2 (LP_GPIO2, LP_UART_RTSN, ADC1_CH2, FSPIQ)", + "GPIO3 (LP_GPIO3, LP_UART_CTSN, ADC1_CH3)", "5V", "GND_1", "NC_1", + // J3 (right row: pins 16..31, top to bottom) + "GND_2", "TX (GPIO16, U0TXD, FSPICS0)", "RX (GPIO17, U0RXD, FSPICS1)", "GPIO15", + "GPIO23 (SDIO_DATA3)", "GPIO22 (SDIO_DATA2)", "GPIO21 (SDIO_DATA1, FSPICS5)", + "GPIO20 (SDIO_DATA0, FSPICS4)", "GPIO19 (SDIO_CLK, FSPICS3)", "GPIO18 (SDIO_CMD, FSPICS2)", + "GPIO9", "GND_3", "GPIO13 (USB_D+)", "GPIO12 (USB_D-)", "GND_4", "NC_2" + }; + + public static final String[] SILK_NAMES_C6_32 = new String[] { + // J1 (left row: pins 0..15, top to bottom) + "3V3", "RST", "4", "5", "6", "7", "0", "1", "8", "10", "11", "2", "3", "5V", "G", "NC", + // J3 (right row: pins 16..31, top to bottom) + "G", "TX", "RX", "15", "23", "22", "21", "20", "19", "18", "9", "G", "13", "12", "G", "NC" + }; + + private DevKitVersion version = DevKitVersion.DevKit_V1_30Pin; + protected boolean headers = false; + + public ESP32DevKit() { + super(); + this.bodyColor = ESP_BLACK; + updateControlPoints(); + } + + @EditableProperty(name = "Version") + public DevKitVersion getVersion() { + if (version == null) { + version = DevKitVersion.DevKit_V1_30Pin; + } + return version; + } + + /** + * True for the RISC-V DevKits, which share one body outline and differ only in module size, + * pin count and how many USB connectors sit on the bottom edge. + */ + private boolean isRiscV() { + return getVersion() == DevKitVersion.ESP32_C3_DevKitM_1 + || getVersion() == DevKitVersion.ESP32_C6_DevKitC_1; + } + + private String[] getPinNames() { + switch (getVersion()) { + case DevKitC_V4_38Pin: + return PIN_NAMES_38; + case ESP32_S3_DevKitC_44Pin: + return PIN_NAMES_S3_44; + case ESP32_C3_DevKitM_1: + return PIN_NAMES_C3_30; + case ESP32_C6_DevKitC_1: + return PIN_NAMES_C6_32; + default: + return PIN_NAMES_30; + } + } + + private String[] getSilkNames() { + switch (getVersion()) { + case DevKitC_V4_38Pin: + return SILK_NAMES_38; + case ESP32_S3_DevKitC_44Pin: + return SILK_NAMES_S3_44; + case ESP32_C3_DevKitM_1: + return SILK_NAMES_C3_30; + case ESP32_C6_DevKitC_1: + return SILK_NAMES_C6_32; + default: + return SILK_NAMES_30; + } + } + + @Override + protected String getSilkPinLabel(int index) { + String[] silkNames = getSilkNames(); + if (index >= 0 && index < silkNames.length) { + return silkNames[index]; + } + return super.getSilkPinLabel(index); + } + + private Size getRiscVShieldWidth() { + return getVersion() == DevKitVersion.ESP32_C3_DevKitM_1 ? SHIELD_WIDTH_C3 : SHIELD_WIDTH_C6; + } + + private Size getRiscVShieldLength() { + return getVersion() == DevKitVersion.ESP32_C3_DevKitM_1 ? SHIELD_LENGTH_C3 : SHIELD_LENGTH_C6; + } + + private Size getRiscVAntennaWidth() { + return getVersion() == DevKitVersion.ESP32_C3_DevKitM_1 ? ANTENNA_WIDTH_C3 : ANTENNA_WIDTH_C6; + } + + private Size getRiscVAntennaLength() { + return getVersion() == DevKitVersion.ESP32_C3_DevKitM_1 ? ANTENNA_LENGTH_C3 : ANTENNA_LENGTH_C6; + } + + private double getRiscVBoardLength() { + int pinsPerRow = getPinNames().length / 2; + return TOP_MARGIN_RISCV.convertToPixels() + (pinsPerRow - 1) * PIN_SPACING.convertToPixels() + + BOTTOM_MARGIN_RISCV.convertToPixels(); + } + + public void setVersion(DevKitVersion version) { + this.version = version; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + String[] pinNames = getPinNames(); + if (index >= 0 && index < pinNames.length) { + return pinNames[index]; + } + return "Pin " + (index + 1); + } + + /** + * Calculates unrotated relative offsets of all control points for the current version. + */ + private double[][] getRelativeOffsets() { + double spacing = PIN_SPACING.convertToPixels(); + + if (getVersion() == DevKitVersion.DevKit_V1_30Pin) { + double rowSpacing = ROW_SPACING.convertToPixels(); + double[][] relativeOffsets = new double[30][2]; + for (int i = 0; i < 15; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 15; i++) { + relativeOffsets[15 + i][0] = rowSpacing; + relativeOffsets[15 + i][1] = i * spacing; + } + return relativeOffsets; + } else if (getVersion() == DevKitVersion.DevKitC_V4_38Pin) { + double rowSpacing = ROW_SPACING.convertToPixels(); + double[][] relativeOffsets = new double[PIN_NAMES_38.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; + } + return relativeOffsets; + } else if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin) { + double rowSpacingS3 = ROW_SPACING_S3.convertToPixels(); + double[][] relativeOffsets = new double[PIN_NAMES_S3_44.length][2]; + for (int i = 0; i < 22; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 22; i++) { + relativeOffsets[22 + i][0] = rowSpacingS3; + relativeOffsets[22 + i][1] = i * spacing; + } + return relativeOffsets; + } else if (isRiscV()) { + double rowSpacing = ROW_SPACING_S3.convertToPixels(); + int pinsPerRow = getPinNames().length / 2; + double[][] relativeOffsets = new double[pinsPerRow * 2][2]; + for (int i = 0; i < pinsPerRow; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < pinsPerRow; i++) { + relativeOffsets[pinsPerRow + i][0] = rowSpacing; + relativeOffsets[pinsPerRow + i][1] = i * spacing; + } + return relativeOffsets; + } + return new double[0][0]; + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double[][] relativeOffsets = getRelativeOffsets(); + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + if (getVersion() == DevKitVersion.DevKit_V1_30Pin) { + double rowSpacing = ROW_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH_30.convertToPixels(); + double boardH = BOARD_LENGTH_30.convertToPixels(); + double topMargin = TOP_MARGIN_30.convertToPixels(); + double boardX = (x + rowSpacing / 2.0) - boardW / 2.0; + double boardY = y - topMargin; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } else if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin) { + double rowSpacingS3 = ROW_SPACING_S3.convertToPixels(); + double boardW = BOARD_WIDTH_S3.convertToPixels(); + double boardH = BOARD_LENGTH_S3.convertToPixels(); + double antennaW = ANTENNA_WIDTH_S3.convertToPixels(); + double antennaH = ANTENNA_LENGTH_S3.convertToPixels(); + double topMargin = TOP_MARGIN_S3.convertToPixels(); + + double mainX = (x + rowSpacingS3 / 2.0) - boardW / 2.0; + double mainY = y - topMargin; + double antennaX = (x + rowSpacingS3 / 2.0) - antennaW / 2.0; + double antennaY = mainY - antennaH; + + Area bodyArea = new Area(new Rectangle2D.Double(mainX, mainY, boardW, boardH)); + bodyArea.add(new Area(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH + 1))); + return bodyArea; + } else if (isRiscV()) { + double rowSpacing = ROW_SPACING_S3.convertToPixels(); + double boardW = BOARD_WIDTH_RISCV.convertToPixels(); + double boardH = getRiscVBoardLength(); + double antennaW = getRiscVAntennaWidth().convertToPixels(); + double antennaH = getRiscVAntennaLength().convertToPixels(); + double topMargin = TOP_MARGIN_RISCV.convertToPixels(); + + double mainX = (x + rowSpacing / 2.0) - boardW / 2.0; + double mainY = y - topMargin; + double antennaX = (x + rowSpacing / 2.0) - antennaW / 2.0; + double antennaY = mainY - antennaH; + + Area bodyArea = new Area(new Rectangle2D.Double(mainX, mainY, boardW, boardH)); + bodyArea.add(new Area(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH + 1))); + return bodyArea; + } else { + double rowSpacing = ROW_SPACING.convertToPixels(); + double mainW = BOARD_WIDTH_38.convertToPixels(); + double mainH = MAIN_BODY_LENGTH_38.convertToPixels(); + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH_38.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); + } + + Shape boardShape = getBodyShape(); + Composite oldComposite = applyAlpha(g2d, componentState); + + // Determine the center X for the current version + double centerX; + if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin || isRiscV()) { + centerX = x + ROW_SPACING_S3.convertToPixels() / 2.0; + } else { + centerX = x + ROW_SPACING.convertToPixels() / 2.0; + } + + drawingObserver.startTracking(); + if (outlineMode) { + g2d.setColor(Constants.TRANSPARENT_COLOR); + g2d.fill(boardShape); + } else { + if (getVersion() == DevKitVersion.DevKit_V1_30Pin) { + g2d.setColor(bodyColor); + g2d.fill(boardShape); + } else if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin) { + double boardW = BOARD_WIDTH_S3.convertToPixels(); + double boardH = BOARD_LENGTH_S3.convertToPixels(); + double antennaW = ANTENNA_WIDTH_S3.convertToPixels(); + double antennaH = ANTENNA_LENGTH_S3.convertToPixels(); + double topMargin = TOP_MARGIN_S3.convertToPixels(); + + double mainX = centerX - boardW / 2.0; + double mainY = y - topMargin; + double antennaX = centerX - antennaW / 2.0; + double antennaY = mainY - antennaH; + + // Main board body (sharp rectangular edges) + g2d.setColor(bodyColor); + g2d.fill(new Rectangle2D.Double(mainX, mainY, boardW, boardH)); + + // Antenna tab in #1e1e1e + g2d.setColor(ANTENNA_BG_COLOR); + g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH)); + } else if (isRiscV()) { + double boardW = BOARD_WIDTH_RISCV.convertToPixels(); + double boardH = getRiscVBoardLength(); + double antennaW = getRiscVAntennaWidth().convertToPixels(); + double antennaH = getRiscVAntennaLength().convertToPixels(); + double topMargin = TOP_MARGIN_RISCV.convertToPixels(); + + double mainX = centerX - boardW / 2.0; + double mainY = y - topMargin; + double antennaX = centerX - antennaW / 2.0; + double antennaY = mainY - antennaH; + + g2d.setColor(bodyColor); + g2d.fill(new Rectangle2D.Double(mainX, mainY, boardW, boardH)); + + // Antenna tab in #1e1e1e + g2d.setColor(ANTENNA_BG_COLOR); + g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH)); + } else { + double rowSpacing = ROW_SPACING.convertToPixels(); + double mainW = BOARD_WIDTH_38.convertToPixels(); + double mainH = MAIN_BODY_LENGTH_38.convertToPixels(); + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH_38.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; + + // Main board body + g2d.setColor(bodyColor); + g2d.fill(new RoundRectangle2D.Double(mainX, mainY, mainW, mainH, 8, 8)); + + // Antenna tab in #1e1e1e + g2d.setColor(ANTENNA_BG_COLOR); + g2d.fill(new Rectangle2D.Double(antennaX, antennaY, antennaW, antennaH)); + } + } + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + double shift1mm = new Size(1.0d, SizeUnit.mm).convertToPixels(); + + if (getVersion() == DevKitVersion.DevKit_V1_30Pin) { + double btnW = BUTTON_LENGTH.convertToPixels(); + double btnH = BUTTON_WIDTH.convertToPixels(); + double rowSpacing = ROW_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH_30.convertToPixels(); + double boardH = BOARD_LENGTH_30.convertToPixels(); + double topMargin = TOP_MARGIN_30.convertToPixels(); + double boardX = centerX - boardW / 2.0; + double boardY = y - topMargin; + + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH.convertToPixels(); + double antennaX = centerX - antennaW / 2.0; + double antennaY = boardY; + + // Antenna (dark rectangle underneath + gold serpentine trace) + MakerBoardPainter.drawPcbAntenna(g2d, antennaX, antennaY, antennaW, antennaH); + + // ESP32-WROOM-32 metal shield module below antenna + double shieldW = SHIELD_WIDTH.convertToPixels(); + double shieldH = SHIELD_LENGTH.convertToPixels(); + double shieldX = centerX - shieldW / 2.0; + double shieldY = boardY + antennaH; + MakerBoardPainter.drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP32-WROOM-32"); + + // 4 Corner Mounting Holes (diameter 2.8mm, 0.8mm away from edges in both directions) + double holeDiameter = HOLE_DIAMETER_30.convertToPixels(); + double holeRadius = holeDiameter / 2.0; + double edgeMargin = HOLE_EDGE_MARGIN_30.convertToPixels(); + double leftHoleX = boardX + edgeMargin + holeRadius; + double rightHoleX = boardX + boardW - edgeMargin - holeRadius; + double topHoleY = boardY + edgeMargin + holeRadius; + double bottomHoleY = boardY + boardH - edgeMargin - holeRadius; + + MakerBoardPainter.drawMountingHole(g2d, leftHoleX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, rightHoleX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, leftHoleX, bottomHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, rightHoleX, bottomHoleY, holeDiameter); + + // Micro-USB Jack at bottom (7.5mm x 5.6mm, 0.5mm overhang) + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = USB_MICRO_OVERHANG.convertToPixels(); + double usbX = centerX - usbW / 2.0; + double usbY = boardY + boardH - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, usbX, usbY, usbW, usbH, "USB"); + + // EN & BOOT tactile buttons at bottom (flanking the Micro-USB port) + double btnY = boardY + boardH - 25 - 1.5 * shift1mm; + double btnLeftX = leftHoleX + 14 + 0.5 * shift1mm; + double btnRightX = rightHoleX - 14 - 0.5 * shift1mm - btnW; + drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "EN", "BOOT"); + + // Silkscreen + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "ESP32 DevKit V1", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + } else if (getVersion() == DevKitVersion.ESP32_S3_DevKitC_44Pin) { + double btnW = BUTTON_WIDTH.convertToPixels(); + double btnH = BUTTON_LENGTH.convertToPixels(); + double boardW = BOARD_WIDTH_S3.convertToPixels(); + double boardH = BOARD_LENGTH_S3.convertToPixels(); + double topMargin = TOP_MARGIN_S3.convertToPixels(); + double antennaW = ANTENNA_WIDTH_S3.convertToPixels(); + double antennaH = ANTENNA_LENGTH_S3.convertToPixels(); + + double mainX = centerX - boardW / 2.0; + double mainY = y - topMargin; + double antennaX = centerX - antennaW / 2.0; + double antennaY = mainY - antennaH; + + // Antenna at top (dark rectangle + gold serpentine trace) + MakerBoardPainter.drawPcbAntenna(g2d, antennaX, antennaY, antennaW, antennaH); + + // ESP32-S3-WROOM-1 metal shield module below antenna + double shieldW = SHIELD_WIDTH_S3.convertToPixels(); + double shieldH = SHIELD_LENGTH_S3.convertToPixels(); + double shieldX = centerX - shieldW / 2.0; + double shieldY = mainY + 4; + MakerBoardPainter.drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP32-S3-WROOM-1"); + + // BOOT & RESET tactile buttons (per DXF: BOOT on left near pin 13/14, RESET on right near pin 20/19) + double btnY = y + 18.5 * PIN_SPACING.convertToPixels() - btnH / 2.0; + double btnLeftX = mainX + 49; + double btnRightX = mainX + boardW - 49 - btnW; + drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "BOOT", "RST"); + + // Two Micro-USB connectors at bottom edge (UART on left, USB on right) + // Each is 7.5x5.6mm, centered 6mm from the board edge, protruding 0.5mm below + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = USB_MICRO_OVERHANG.convertToPixels(); + double usbEdgeDist = new Size(6.0d, SizeUnit.mm).convertToPixels(); + double usbLeftX = mainX + usbEdgeDist - usbW / 2.0; + double usbRightX = mainX + boardW - usbEdgeDist - usbW / 2.0; + double usbY = mainY + boardH - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, usbLeftX, usbY, usbW, usbH, "UART"); + MakerBoardPainter.drawMicroUsb(g2d, usbRightX, usbY, usbW, usbH, "USB"); + + // Silkscreen + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "ESP32-S3 DevKitC-1", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + } else if (isRiscV()) { + boolean isC3 = getVersion() == DevKitVersion.ESP32_C3_DevKitM_1; + double btnW = BUTTON_WIDTH.convertToPixels(); + double btnH = BUTTON_LENGTH.convertToPixels(); + double boardW = BOARD_WIDTH_RISCV.convertToPixels(); + double boardH = getRiscVBoardLength(); + double topMargin = TOP_MARGIN_RISCV.convertToPixels(); + double antennaW = getRiscVAntennaWidth().convertToPixels(); + double antennaH = getRiscVAntennaLength().convertToPixels(); + + double mainX = centerX - boardW / 2.0; + double mainY = y - topMargin; + double antennaX = centerX - antennaW / 2.0; + double antennaY = mainY - antennaH; + + // Antenna at top (dark rectangle + gold serpentine trace) + MakerBoardPainter.drawPcbAntenna(g2d, antennaX, antennaY, antennaW, antennaH); + + // ESP32-C3-MINI-1 / ESP32-C6-WROOM-1 metal shield module below the antenna + double shieldW = getRiscVShieldWidth().convertToPixels(); + double shieldH = getRiscVShieldLength().convertToPixels(); + double shieldX = centerX - shieldW / 2.0; + double shieldY = mainY + 4; + MakerBoardPainter.drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, + isC3 ? "ESP32-C3-MINI-1" : "ESP32-C6-WROOM-1"); + + // BOOT & RST tactile buttons flanking the connectors, inset clear of the pin columns + int pinsPerRow = getPinNames().length / 2; + double btnY = y + (pinsPerRow - 3.5) * PIN_SPACING.convertToPixels() - btnH / 2.0; + double btnLeftX = mainX + 49; + double btnRightX = mainX + boardW - 49 - btnW; + drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "BOOT", "RST"); + + // The C3 has a single Micro-USB port; the C6 has a USB-to-UART and a native port, both + // USB Type-C, on the bottom edge + if (isC3) { + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = USB_MICRO_OVERHANG.convertToPixels(); + MakerBoardPainter.drawMicroUsb(g2d, centerX - usbW / 2.0, mainY + boardH - usbH + usbOverhang, usbW, usbH, "USB"); + } else { + double usbW = USB_C_WIDTH.convertToPixels(); + double usbH = USB_C_LENGTH.convertToPixels(); + double usbOverhang = USB_C_OVERHANG.convertToPixels(); + double usbEdgeDist = new Size(6.5d, SizeUnit.mm).convertToPixels(); + double usbY = mainY + boardH - usbH + usbOverhang; + MakerBoardPainter.drawUsbC(g2d, mainX + usbEdgeDist - usbW / 2.0, usbY, usbW, usbH, "UART"); + MakerBoardPainter.drawUsbC(g2d, mainX + boardW - usbEdgeDist - usbW / 2.0, usbY, usbW, usbH, "USB"); + } + + // Silkscreen + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, isC3 ? "ESP32-C3 DevKitM-1" : "ESP32-C6 DevKitC-1", centerX, + shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + } else { + // 38-Pin (DevKitC V4) + double btnW = BUTTON_LENGTH.convertToPixels(); + double btnH = BUTTON_WIDTH.convertToPixels(); + double rowSpacing = ROW_SPACING.convertToPixels(); + double mainW = BOARD_WIDTH_38.convertToPixels(); + double mainH = MAIN_BODY_LENGTH_38.convertToPixels(); + double mainX = centerX - mainW / 2.0; + double mainY = (y + 18 * PIN_SPACING.convertToPixels() / 2.0) - mainH / 2.0; + + double antennaW = ANTENNA_WIDTH.convertToPixels(); + double antennaH = ANTENNA_LENGTH_38.convertToPixels(); + double antennaX = centerX - antennaW / 2.0; + double antennaY = mainY - antennaH; + + // Antenna (dark rectangle underneath + gold serpentine trace) + MakerBoardPainter.drawPcbAntenna(g2d, antennaX, antennaY, antennaW, antennaH); + + // ESP32-WROOM-32 metal shield module below antenna + double shieldW = SHIELD_WIDTH.convertToPixels(); + double shieldH = SHIELD_LENGTH.convertToPixels(); + double shieldX = centerX - shieldW / 2.0; + double shieldY = mainY + 8; + MakerBoardPainter.drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP32-WROOM-32"); + + // Micro-USB Jack at bottom (7.5mm x 5.6mm, 0.5mm overhang) + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = USB_MICRO_OVERHANG.convertToPixels(); + double usbX = centerX - usbW / 2.0; + double usbY = mainY + mainH - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, usbX, usbY, usbW, usbH, "USB"); + + // EN & BOOT tactile buttons at bottom + double btnY = mainY + mainH - 25 - new Size(2.0d, SizeUnit.mm).convertToPixels(); + double btnLeftX = mainX + 38; + double btnRightX = mainX + mainW - 38 - btnW; + drawButtons(g2d, btnLeftX, btnRightX, btnY, btnW, btnH, "EN", "BOOT"); + + // Silkscreen + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "ESP32 DevKitC V4", centerX, shieldY + shieldH + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // Draw pin labels next to control points + drawPinLabels(g2d, x, y, getRelativeOffsets(), SILK_COLOR); + } + + g2d.setTransform(oldTx); + + if (headers) { + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + } else { + drawPcbSolderPads(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver); + } + + g2d.setComposite(oldComposite); + } + + /** + * Helper to draw a pair of tactile buttons with labels. + */ + private void drawButtons(Graphics2D g2d, double btnLeftX, double btnRightX, double btnY, double btnW, double btnH, + String leftLabel, String rightLabel) { + MakerBoardPainter.drawButton(g2d, btnLeftX, btnY, btnW, btnH); + MakerBoardPainter.drawButton(g2d, btnRightX, btnY, btnW, btnH); + + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, leftLabel, btnLeftX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, rightLabel, btnRightX + btnW / 2.0, btnY - 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(ESP_BLACK); + g2d.fill(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3)); + 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); + + g2d.setColor(SILK_COLOR); + 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 new file mode 100644 index 000000000..40f6f0ba2 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/ESP8266NodeMCU.java @@ -0,0 +1,296 @@ +/* + * + * 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 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.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "ESP8266 NodeMCU", category = "Controllers", + author = "Branislav Stojkovic", description = "ESP8266 NodeMCU V2/V3 Wi-Fi Development Board", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class ESP8266NodeMCU extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color NODEMCU_BLACK = Color.decode("#3E3E3E"); + public static Color SILK_COLOR = Color.WHITE; + 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(7.0d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size SHIELD_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size SHIELD_LENGTH = new Size(15.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); + + // Node names carry the GPIO number and function, because the NodeMCU's Dn labels are board + // aliases that do not follow the ESP8266's own numbering -- D1 is GPIO5, not GPIO1 -- and that + // mapping is the single thing people most often get wrong on this board. SILK_NAMES holds what + // is actually printed next to each pin. + public static final String[] PIN_NAMES = new String[] { + // Left row (pins 0..14) + "A0 (ADC0)", "RSV_1", "RSV_2", "SD3 (GPIO10, flash)", "SD2 (GPIO9, flash)", + "SD1 (GPIO8, flash)", "CMD (GPIO11, flash)", "SD0 (GPIO7, flash)", "CLK (GPIO6, flash)", + "GND_1", "3V3_1", "EN (CH_PD)", "RST", "GND_2", "VIN", + // Right row (pins 15..29) + "D0 (GPIO16, WAKE)", "D1 (GPIO5, SCL)", "D2 (GPIO4, SDA)", "D3 (GPIO0, FLASH)", + "D4 (GPIO2, LED)", "3V3_2", "GND_3", "D5 (GPIO14, HSPI SCLK)", "D6 (GPIO12, HSPI MISO)", + "D7 (GPIO13, HSPI MOSI)", "D8 (GPIO15, HSPI CS)", "RX (GPIO3, U0RXD)", "TX (GPIO1, U0TXD)", + "GND_4", "3V3_3" + }; + + public static final String[] SILK_NAMES = new String[] { + // Left row (pins 0..14) + "A0", "RSV", "RSV", "SD3", "SD2", "SD1", "CMD", "SD0", "CLK", "GND", "3V3", "EN", "RST", "GND", "VIN", + // Right row (pins 15..29) + "D0", "D1", "D2", "D3", "D4", "3V3", "GND", "D5", "D6", "D7", "D8", "RX", "TX", "GND", "3V3" + }; + + protected boolean headers = false; + + public ESP8266NodeMCU() { + super(); + this.bodyColor = NODEMCU_BLACK; + updateControlPoints(); + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected String getSilkPinLabel(int index) { + if (index >= 0 && index < SILK_NAMES.length) { + return SILK_NAMES[index]; + } + return super.getSilkPinLabel(index); + } + + private double[][] getRelativeOffsets() { + double spacing = PIN_SPACING.convertToPixels(); + double rowSpacing = ROW_SPACING.convertToPixels(); // 180px + + double[][] relativeOffsets = new double[30][2]; + for (int i = 0; i < 15; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 15; i++) { + relativeOffsets[15 + i][0] = rowSpacing; + relativeOffsets[15 + i][1] = i * spacing; + } + return relativeOffsets; + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + rotatePoints(firstPoint, getRelativeOffsets()); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + 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); + } + + @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 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; + + double shieldW = SHIELD_WIDTH.convertToPixels(); + double shieldH = SHIELD_LENGTH.convertToPixels(); + double shieldX = (x + rowSpacing / 2.0) - shieldW / 2.0; + double shieldY = antennaY + antennaH; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 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; + + MakerBoardPainter.drawMountingHole(g2d, leftHoleX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, rightHoleX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, leftHoleX, bottomHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, rightHoleX, bottomHoleY, holeDiameter); + + // Antenna (dark rectangle underneath + gold serpentine trace) + MakerBoardPainter.drawPcbAntenna(g2d, antennaX, antennaY, antennaW, antennaH); + + // ESP-12 Metal shield module below antenna + MakerBoardPainter.drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP8266"); + + // Micro-USB Jack at bottom (7.5mm x 5.6mm, 0.5mm overhang) + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = USB_MICRO_OVERHANG.convertToPixels(); + double usbX = (x + rowSpacing / 2.0) - usbW / 2.0; + double usbY = boardY + boardH - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, usbX, usbY, usbW, usbH, "USB"); + + // RST & FLASH tactile buttons at bottom (flanking the Micro-USB port) + double btnW = BUTTON_LENGTH.convertToPixels(); + double btnH = BUTTON_WIDTH.convertToPixels(); + double btnY = boardY + boardH - 25 - new Size(2.0d, SizeUnit.mm).convertToPixels(); + double shift1mm = new Size(1.0d, SizeUnit.mm).convertToPixels(); + double btnLeftX = leftHoleX + 10 + shift1mm; + double btnRightX = rightHoleX - 10 - shift1mm - btnW; + + MakerBoardPainter.drawButton(g2d, btnLeftX, btnY, btnW, btnH); + MakerBoardPainter.drawButton(g2d, btnRightX, btnY, btnW, btnH); + + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT_SMALL); + 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); + double labelY = shieldY + shieldH + new Size(4.0d, SizeUnit.mm).convertToPixels(); + StringUtils.drawCenteredText(g2d, "NodeMCU", x + rowSpacing / 2.0, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Pin labels + drawPinLabels(g2d, x, y, getRelativeOffsets(), SILK_COLOR); + } + + g2d.setTransform(oldTx); + + if (headers) { + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + } else { + drawPcbSolderPads(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver); + } + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(NODEMCU_BLACK); + g2d.fill(new RoundRectangle2D.Double(5, 2, width - 10, height - 4, 3, 3)); + 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); + + g2d.setColor(SILK_COLOR); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "8266", width / 2, height / 2 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java new file mode 100644 index 000000000..40e381beb --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPi.java @@ -0,0 +1,582 @@ +/* + * + * 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 java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.*; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardLogos; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Raspberry Pi", category = "Controllers", + author = "Branislav Stojkovic", description = "Raspberry Pi 3/4/5 Single Board Computer with 40-pin GPIO", + instanceNamePrefix = "SBC", zOrder = IDIYComponent.BOARD, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class RaspberryPi extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color RPI_GREEN = Color.decode("#1B5E20"); + public static Size BOARD_WIDTH = new Size(85.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(56.0d, SizeUnit.mm); + public static Font RPI_TITLE_FONT = new Font("SansSerif", Font.BOLD, 20); + + public static final String[] PIN_NAMES = new String[] { + "3.3V (Pin 1)", "5V (Pin 2)", + "GPIO2/SDA (Pin 3)", "5V (Pin 4)", + "GPIO3/SCL (Pin 5)", "GND (Pin 6)", + "GPIO4 (Pin 7)", "GPIO14/TXD (Pin 8)", + "GND (Pin 9)", "GPIO15/RXD (Pin 10)", + "GPIO17 (Pin 11)", "GPIO18 (Pin 12)", + "GPIO27 (Pin 13)", "GND (Pin 14)", + "GPIO22 (Pin 15)", "GPIO23 (Pin 16)", + "3.3V (Pin 17)", "GPIO24 (Pin 18)", + "GPIO10/MOSI (Pin 19)", "GND (Pin 20)", + "GPIO9/MISO (Pin 21)", "GPIO25 (Pin 22)", + "GPIO11/SCLK (Pin 23)", "GPIO8/CE0 (Pin 24)", + "GND (Pin 25)", "GPIO7/CE1 (Pin 26)", + "ID_SD (Pin 27)", "ID_SC (Pin 28)", + "GPIO5 (Pin 29)", "GND (Pin 30)", + "GPIO6 (Pin 31)", "GPIO12 (Pin 32)", + "GPIO13 (Pin 33)", "GND (Pin 34)", + "GPIO19 (Pin 35)", "GPIO16 (Pin 36)", + "GPIO26 (Pin 37)", "GPIO20 (Pin 38)", + "GND (Pin 39)", "GPIO21 (Pin 40)", + "PoE TR0 (Pin 1)", "PoE TR1 (Pin 2)", + "PoE TR2 (Pin 3)", "PoE TR3 (Pin 4)", + "PCIe", "MIPI 1", "MIPI 0", "UART" + }; + + public enum RaspberryPiVersion { + PI_3_B("Pi 3 (Model B+)"), + PI_4_B("Pi 4 (Model B)"), + PI_5("Pi 5"); + + private String label; + + private RaspberryPiVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + + private RaspberryPiVersion version = RaspberryPiVersion.PI_5; + + @org.diylc.core.annotations.EditableProperty(name = "Version") + public RaspberryPiVersion getVersion() { + return version; + } + + public void setVersion(RaspberryPiVersion version) { + this.version = version; + updateControlPoints(); + invalidateCache(); + } + + public RaspberryPi() { + super(); + this.bodyColor = RPI_GREEN; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index == 44 && version != RaspberryPiVersion.PI_5) { + return "DISP"; + } + if (index == 45 && version != RaspberryPiVersion.PI_5) { + return "CAM"; + } + 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(); + + // 2x20 header: Pin 1 at (0,0), Pin 2 at (0, -spacing), Pin 3 at (spacing, 0), Pin 4 at (spacing, -spacing)... + int numPins = (version == RaspberryPiVersion.PI_5) ? PIN_NAMES.length : PIN_NAMES.length - 2; + double[][] relativeOffsets = new double[numPins][2]; + for (int col = 0; col < 20; col++) { + int pinOdd = col * 2; // Pin 1, 3, 5... (bottom/inner row of header) + int pinEven = col * 2 + 1; // Pin 2, 4, 6... (top/outer row of header) + relativeOffsets[pinOdd][0] = col * spacing; + relativeOffsets[pinOdd][1] = 0; + relativeOffsets[pinEven][0] = col * spacing; + relativeOffsets[pinEven][1] = -spacing; + } + + // 2x2 PoE header (pins 40..43) + double pin1OffsetX = new Size(8.37d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(3.5d, SizeUnit.mm).convertToPixels() + spacing / 2.0; + double poeHoleY = (version == RaspberryPiVersion.PI_5) ? 52.5d : 3.5d; + double poeDir = (version == RaspberryPiVersion.PI_5) ? -6.0d : 6.0d; + double poeCenterX = new Size(61.5d, SizeUnit.mm).convertToPixels() - pin1OffsetX; + double poeCenterY = new Size(poeHoleY + poeDir, SizeUnit.mm).convertToPixels() - pin1OffsetY; + + relativeOffsets[40] = new double[] {poeCenterX - spacing / 2.0, poeCenterY - spacing / 2.0}; + relativeOffsets[41] = new double[] {poeCenterX - spacing / 2.0, poeCenterY + spacing / 2.0}; + relativeOffsets[42] = new double[] {poeCenterX + spacing / 2.0, poeCenterY - spacing / 2.0}; + relativeOffsets[43] = new double[] {poeCenterX + spacing / 2.0, poeCenterY + spacing / 2.0}; + + // Left edge connector (pin 44): PCIe for PI 5, MIPI (DISP) for PI 3 and 4 + double leftEdgeCenterX; + double leftEdgeCenterY; + if (version == RaspberryPiVersion.PI_5) { + double pcieW = new Size(3.0d, SizeUnit.mm).convertToPixels(); + leftEdgeCenterX = new Size(0.1d, SizeUnit.in).convertToPixels() - new Size(1.0d, SizeUnit.mm).convertToPixels() + pcieW / 2.0 - pin1OffsetX; + leftEdgeCenterY = new Size(1.0d, SizeUnit.in).convertToPixels() + new Size(0.75d, SizeUnit.mm).convertToPixels() - pin1OffsetY; + } else { + leftEdgeCenterX = new Size(4.0d, SizeUnit.mm).convertToPixels() - pin1OffsetX; + leftEdgeCenterY = BOARD_HEIGHT.convertToPixels() - new Size(28.0d, SizeUnit.mm).convertToPixels() - pin1OffsetY; + } + relativeOffsets[44] = new double[] {leftEdgeCenterX, leftEdgeCenterY}; + + // MIPI 1 (pin 45) and MIPI 0 (pin 46) connectors on bottom edge + double camH = (version == RaspberryPiVersion.PI_5) ? 15.5d : 22.0d; + double mipiH = new Size(camH, SizeUnit.mm).convertToPixels(); + double offset = (version == RaspberryPiVersion.PI_5) ? new Size(1.0d, SizeUnit.mm).convertToPixels() : 0.0d; + double mipiCenterY = BOARD_HEIGHT.convertToPixels() - offset - mipiH / 2.0 - pin1OffsetY; + double camXOffset = (version == RaspberryPiVersion.PI_4_B) ? 46.5d : 45.0d; + double mipi1CenterX = (version == RaspberryPiVersion.PI_5) ? new Size(48.5d, SizeUnit.mm).convertToPixels() - pin1OffsetX : new Size(camXOffset, SizeUnit.mm).convertToPixels() - pin1OffsetX; + relativeOffsets[45] = new double[] {mipi1CenterX, mipiCenterY}; + if (version == RaspberryPiVersion.PI_5) { + double mipi0CenterX = new Size(54.5d, SizeUnit.mm).convertToPixels() - pin1OffsetX; + relativeOffsets[46] = new double[] {mipi0CenterX, mipiCenterY}; + double uartCenterX = new Size(32.5d, SizeUnit.mm).convertToPixels() - pin1OffsetX; + double uartCenterY = new Size(52.5d, SizeUnit.mm).convertToPixels() - pin1OffsetY; + relativeOffsets[47] = new double[] {uartCenterX, uartCenterY}; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double pin1OffsetX = new Size(8.37d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(3.5d, SizeUnit.mm).convertToPixels() + PIN_SPACING.convertToPixels() / 2.0; + double boardX = x - pin1OffsetX; + double boardY = y - pin1OffsetY; + double cornerArc = new Size(6.0d, SizeUnit.mm).convertToPixels(); + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, cornerArc, cornerArc); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double pin1OffsetX = new Size(8.37d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(3.5d, SizeUnit.mm).convertToPixels() + PIN_SPACING.convertToPixels() / 2.0; + double boardX = x - pin1OffsetX; + double boardY = y - pin1OffsetY; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes (diameter 2.7mm, spaced 58mm apart horizontally and 49mm vertically) + double holeDiameter = new Size(2.7d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(3.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(3.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(3.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(52.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(61.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(3.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(61.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(52.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + + // USB & Ethernet Ports on the right edge + double topDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 45.75d : 47.0d; + double midDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 27.0d : 29.0d; + double botDistFromBottom = (version == RaspberryPiVersion.PI_4_B) ? 9.0d : 10.25d; + + double topCenterY = 56.0d - topDistFromBottom; + double middleCenterY = 56.0d - midDistFromBottom; + double bottomCenterY = 56.0d - botDistFromBottom; + + double ethernetCenterY = (version == RaspberryPiVersion.PI_4_B) ? topCenterY : bottomCenterY; + double topUsbCenterY = (version == RaspberryPiVersion.PI_4_B) ? bottomCenterY : topCenterY; + + // Top USB slot (USB 3.0) + MakerBoardPainter.drawUsbA(g2d, boardX + boardW - new Size(14.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(topUsbCenterY - 7.0d, SizeUnit.mm).convertToPixels(), + USB_A_DUAL_LENGTH.convertToPixels(), + new Size(14.0d, SizeUnit.mm).convertToPixels(), "USB 3.0"); + + // USB 2.0 (middle) + MakerBoardPainter.drawUsbA(g2d, boardX + boardW - new Size(14.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(middleCenterY - 7.0d, SizeUnit.mm).convertToPixels(), + USB_A_DUAL_LENGTH.convertToPixels(), + new Size(14.0d, SizeUnit.mm).convertToPixels(), "USB 2.0"); + + // Ethernet + MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - new Size(18.0d, SizeUnit.mm).convertToPixels(), + boardY + new Size(ethernetCenterY - 8.0d, SizeUnit.mm).convertToPixels(), + new Size(21.0d, SizeUnit.mm).convertToPixels(), + new Size(16.0d, SizeUnit.mm).convertToPixels(), "ETHERNET"); + + // Broadcom SoC + double socW; + double socH; + double socX; + double socY; + if (version == RaspberryPiVersion.PI_5) { + socW = new Size(17.0d, SizeUnit.mm).convertToPixels(); + socH = new Size(17.0d, SizeUnit.mm).convertToPixels(); + socX = boardX + new Size(32.5d, SizeUnit.mm).convertToPixels() - new Size(0.3d, SizeUnit.in).convertToPixels(); + socY = boardY + new Size(24.5d, SizeUnit.mm).convertToPixels(); + } else if (version == RaspberryPiVersion.PI_4_B) { + socW = new Size(14.0d, SizeUnit.mm).convertToPixels(); + socH = new Size(14.0d, SizeUnit.mm).convertToPixels(); + socX = boardX + new Size(28.75d, SizeUnit.mm).convertToPixels() - socW / 2.0; + socY = boardY + boardH - new Size(32.5d, SizeUnit.mm).convertToPixels() - socH / 2.0; + } else { + socW = new Size(14.0d, SizeUnit.mm).convertToPixels(); + socH = new Size(14.0d, SizeUnit.mm).convertToPixels(); + socX = boardX + new Size(20.0d, SizeUnit.mm).convertToPixels(); + socY = boardY + new Size(17.0d, SizeUnit.mm).convertToPixels(); + } + + String socName; + if (version == RaspberryPiVersion.PI_5) { + socName = "BCM2712"; + } else if (version == RaspberryPiVersion.PI_4_B) { + socName = "BCM2711"; + } else { + socName = "BCM2837"; + } + + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new Rectangle2D.Double(socX, socY, socW, socH)); + g2d.setColor(IC_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(socX, socY, socW, socH)); + + g2d.setColor(IC_TEXT_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, socName, socX + socW / 2.0, socY + socH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Bottom edge: USB-C Power & Micro-HDMI connectors + // PWR (USB-C): center X = 11.2 mm + double usbCW = USB_C_WIDTH.convertToPixels(); + double usbCH = USB_C_LENGTH.convertToPixels(); + MakerBoardPainter.drawUsbC(g2d, boardX + new Size(11.2d, SizeUnit.mm).convertToPixels() - usbCW / 2.0, + boardY + boardH - new Size(6.5d, SizeUnit.mm).convertToPixels(), + usbCW, usbCH, "PWR"); + + if (version == RaspberryPiVersion.PI_3_B) { + double hdmiW = new Size(15.0d, SizeUnit.mm).convertToPixels(); + double hdmiH = new Size(11.5d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMetalConnector(g2d, boardX + new Size(32.0d, SizeUnit.mm).convertToPixels() - hdmiW / 2.0, + boardY + boardH - new Size(10.5d, SizeUnit.mm).convertToPixels(), + hdmiW, hdmiH, "HDMI"); + } else { + // HDMI0: center X = 25.8 mm + MakerBoardPainter.drawMetalConnector(g2d, boardX + new Size(22.05d, SizeUnit.mm).convertToPixels(), + boardY + boardH - new Size(6.5d, SizeUnit.mm).convertToPixels(), + new Size(7.5d, SizeUnit.mm).convertToPixels(), + new Size(7.5d, SizeUnit.mm).convertToPixels(), "HDMI0"); + + // HDMI1: center X = 39.2 mm + MakerBoardPainter.drawMetalConnector(g2d, boardX + new Size(35.45d, SizeUnit.mm).convertToPixels(), + boardY + boardH - new Size(6.5d, SizeUnit.mm).convertToPixels(), + new Size(7.5d, SizeUnit.mm).convertToPixels(), + new Size(7.5d, SizeUnit.mm).convertToPixels(), "HDMI1"); + } + + // MIPI CSI/DSI connectors on bottom edge + double mipiW = new Size(3.5d, SizeUnit.mm).convertToPixels(); + double mipiH = new Size(15.5d, SizeUnit.mm).convertToPixels(); + double mipiY = boardY + boardH - mipiH - new Size(1.0d, SizeUnit.mm).convertToPixels(); + + if (version == RaspberryPiVersion.PI_5) { + double mipi1X = boardX + new Size(48.5d, SizeUnit.mm).convertToPixels() - mipiW / 2.0; + double mipi0X = boardX + new Size(54.5d, SizeUnit.mm).convertToPixels() - mipiW / 2.0; + MakerBoardPainter.drawFpcConnector(g2d, mipi1X, mipiY, mipiW, mipiH, true, ""); + MakerBoardPainter.drawFpcConnector(g2d, mipi0X, mipiY, mipiW, mipiH, true, ""); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "MIPI 1", boardX + new Size(48.5d, SizeUnit.mm).convertToPixels(), + mipiY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "MIPI 0", boardX + new Size(54.5d, SizeUnit.mm).convertToPixels(), + mipiY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // UART connector (5x3mm, center X = 32.5mm, center Y = 52.5mm) + double uartW = new Size(5.0d, SizeUnit.mm).convertToPixels(); + double uartH = new Size(3.0d, SizeUnit.mm).convertToPixels(); + double uartX = boardX + new Size(32.5d, SizeUnit.mm).convertToPixels() - uartW / 2.0; + double uartY = boardY + new Size(52.5d, SizeUnit.mm).convertToPixels() - uartH / 2.0; + + g2d.setColor(CONNECTOR_PLASTIC_COLOR); + g2d.fill(new Rectangle2D.Double(uartX, uartY, uartW, uartH)); + g2d.setColor(CONNECTOR_PLASTIC_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(uartX, uartY, uartW, uartH)); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "UART", boardX + new Size(32.5d, SizeUnit.mm).convertToPixels(), + uartY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } else { + double camW = new Size(2.5d, SizeUnit.mm).convertToPixels(); + double camH = new Size(22.0d, SizeUnit.mm).convertToPixels(); + double camXOffsetDraw = (version == RaspberryPiVersion.PI_4_B) ? 46.5d : 45.0d; + double camX = boardX + new Size(camXOffsetDraw, SizeUnit.mm).convertToPixels() - camW / 2.0; + double camY = boardY + boardH - camH; + + g2d.setColor(Color.decode("#181818")); + g2d.fill(new Rectangle2D.Double(camX, camY, camW, camH)); + g2d.setColor(Color.decode("#333333")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(camX, camY, camW, camH)); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "CAMERA", boardX + new Size(camXOffsetDraw, SizeUnit.mm).convertToPixels(), + camY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // A/V Jack (4-pole stereo and composite) + double avBaseW = new Size(7.0d, SizeUnit.mm).convertToPixels(); + double avBaseH = new Size(12.5d, SizeUnit.mm).convertToPixels(); + double avTipW = new Size(5.0d, SizeUnit.mm).convertToPixels(); + double avTipH = new Size(2.5d, SizeUnit.mm).convertToPixels(); + + double avCenterXOffset = (version == RaspberryPiVersion.PI_4_B) ? 54.0d : 53.5d; + double avCenterX = boardX + new Size(avCenterXOffset, SizeUnit.mm).convertToPixels(); + double avY = boardY + boardH - avBaseH; + + Path2D.Double avShape = new Path2D.Double(); + avShape.moveTo(avCenterX - avBaseW / 2.0, avY); + avShape.lineTo(avCenterX + avBaseW / 2.0, avY); + avShape.lineTo(avCenterX + avBaseW / 2.0, avY + avBaseH); + avShape.lineTo(avCenterX + avTipW / 2.0, avY + avBaseH); + avShape.lineTo(avCenterX + avTipW / 2.0, avY + avBaseH + avTipH); + avShape.lineTo(avCenterX - avTipW / 2.0, avY + avBaseH + avTipH); + avShape.lineTo(avCenterX - avTipW / 2.0, avY + avBaseH); + avShape.lineTo(avCenterX - avBaseW / 2.0, avY + avBaseH); + avShape.closePath(); + + g2d.setColor(Color.decode("#181818")); + g2d.fill(avShape); + g2d.setColor(Color.decode("#333333")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(avShape); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "A/V", avCenterX, + avY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // Left edge connector: PCIe for PI 5, MIPI (DISP) for PI 3 and 4 + if (version == RaspberryPiVersion.PI_5) { + double pcieW = new Size(3.0d, SizeUnit.mm).convertToPixels(); + double pcieH = new Size(10.5d, SizeUnit.mm).convertToPixels(); + double pcieX = boardX + new Size(0.1d, SizeUnit.in).convertToPixels() - new Size(1.0d, SizeUnit.mm).convertToPixels(); + double pcieY = boardY + new Size(1.0d, SizeUnit.in).convertToPixels() + new Size(0.75d, SizeUnit.mm).convertToPixels() - pcieH / 2.0; + MakerBoardPainter.drawFpcConnector(g2d, pcieX, pcieY, pcieW, pcieH, true, ""); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "PCIe", pcieX + pcieW / 2.0, + pcieY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } else { + double dispW = new Size(2.5d, SizeUnit.mm).convertToPixels(); + double dispH = new Size(22.0d, SizeUnit.mm).convertToPixels(); + double dispX = boardX + new Size(4.0d, SizeUnit.mm).convertToPixels() - dispW / 2.0; + double dispY = boardY + boardH - new Size(28.0d, SizeUnit.mm).convertToPixels() - dispH / 2.0; + + g2d.setColor(Color.decode("#181818")); + g2d.fill(new Rectangle2D.Double(dispX, dispY, dispW, dispH)); + g2d.setColor(Color.decode("#333333")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(dispX, dispY, dispW, dispH)); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "DISPLAY", dispX + dispW / 2.0, + dispY - new Size(1.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // 2x2 PoE header silkscreen label + double poeLabelHoleY = (version == RaspberryPiVersion.PI_5) ? 52.5d : 3.5d; + double poeLabelDir = (version == RaspberryPiVersion.PI_5) ? -6.0d : 6.0d; + double poeX = boardX + new Size(61.5d, SizeUnit.mm).convertToPixels(); + double poeY = boardY + new Size(poeLabelHoleY + poeLabelDir, SizeUnit.mm).convertToPixels(); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + double poeTextOffset = (version == RaspberryPiVersion.PI_5) ? + -(PIN_SPACING.convertToPixels() + new Size(1.5d, SizeUnit.mm).convertToPixels()) : + (PIN_SPACING.convertToPixels() + new Size(1.5d, SizeUnit.mm).convertToPixels()); + StringUtils.drawCenteredText(g2d, "PoE", poeX, + poeY + poeTextOffset, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // "GPIO" silkscreen below the 40-pin header, centered on the header span, which shares its + // center line with the board title + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "GPIO", boardX + new Size(32.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(7.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Raspberry Pi Silkscreen text below 40-pin header + g2d.setColor(Color.WHITE); + g2d.setFont(RPI_TITLE_FONT); + String versionLabel = version.toString(); + if (versionLabel.contains("(")) { + versionLabel = versionLabel.substring(0, versionLabel.indexOf("(")).trim(); + } + StringUtils.drawCenteredText(g2d, "Raspberry " + versionLabel, boardX + new Size(32.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(14.0d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Raspberry Pi Logo to the left of the SoC (shrunk 20% to 10.4mm, moved 0.2" up) + double logoSize = new Size(10.4d, SizeUnit.mm).convertToPixels(); + double logoX = boardX + new Size(8.3d, SizeUnit.mm).convertToPixels(); + double logoY = boardY + new Size(26.5d, SizeUnit.mm).convertToPixels() - new Size(0.2d, SizeUnit.in).convertToPixels(); + MakerBoardLogos.drawRaspberryPiLogo(g2d, logoX, logoY, logoSize); + } + + g2d.setTransform(oldTx); + + // Draw 40 GPIO header pins and 4 PoE header pins + drawPinHeader(g2d, 0, 44, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(RPI_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 4, 4)); + g2d.setColor(RPI_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 4, 4)); + + // USB & Ethernet (Top USB 3.0, Middle USB 2.0, Bottom Ethernet) + g2d.setColor(USB_METAL_COLOR); + if (version == RaspberryPiVersion.PI_4_B) { + g2d.fillRect(width - 9, 5, 7, 6); + g2d.fillRect(width - 8, 13, 6, 5); + g2d.fillRect(width - 8, 21, 6, 5); + } else { + g2d.fillRect(width - 8, 6, 6, 5); + g2d.fillRect(width - 8, 13, 6, 5); + g2d.fillRect(width - 9, 20, 7, 6); + } + + // SoC + g2d.setColor(IC_BODY_COLOR); + if (version == RaspberryPiVersion.PI_5) { + g2d.fillRect(13, 14, 7, 7); + } else if (version == RaspberryPiVersion.PI_4_B) { + g2d.fillRect(11, 11, 5, 5); + } else { + g2d.fillRect(10, 11, 5, 5); + } + + // GPIO Header + g2d.setColor(HEADER_BODY_COLOR); + g2d.fillRect(4, 5, 18, 3); + + // PCIe & MIPI connectors & PoE + g2d.setColor(IC_BODY_COLOR); + if (version == RaspberryPiVersion.PI_5) { + g2d.setColor(IC_BODY_COLOR); + g2d.fillRect(2, 13, 1, 5); + g2d.fillRect(18, height - 10, 1, 6); + g2d.fillRect(20, height - 10, 1, 6); + g2d.setColor(CONNECTOR_PLASTIC_COLOR); // UART + g2d.fillRect(13, height - 5, 2, 1); + } else { + g2d.fillRect(3, 11, 1, 7); // DISP + g2d.fillRect(15, height - 11, 1, 7); // CAMERA vertical + g2d.fillRect(20, height - 7, 2, 3); // A/V Jack + } + g2d.setColor(IC_BODY_COLOR); + if (version == RaspberryPiVersion.PI_5) { + g2d.fillRect(width - 11, height - 9, 2, 2); + } else { + g2d.fillRect(width - 11, 9, 2, 2); + } + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 6)); + StringUtils.drawCenteredText(g2d, "RPi", 14, 25, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java new file mode 100644 index 000000000..1f1569925 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiPico.java @@ -0,0 +1,516 @@ +/* + * + * 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 java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardLogos; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Raspberry Pi Pico", category = "Controllers", + author = "Branislav Stojkovic", + description = "Raspberry Pi Pico / Pico W (RP2040) and Pico 2 / Pico 2 W (RP2350) Microcontroller Board", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class RaspberryPiPico extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum PicoVersion { + PICO("Pi Pico"), + PICO_W("Pi Pico W"), + PICO_2("Pi Pico 2"), + PICO_2_W("Pi Pico 2 W"); + + private final String label; + + PicoVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + + public static Color RPI_GREEN = Color.decode("#1B5E20"); + public static Size BOARD_WIDTH = new Size(21.0d, SizeUnit.mm); + public static Size BOARD_LENGTH = new Size(51.0d, SizeUnit.mm); + + public static Color PAD_COLOR = GOLD_COLOR; + public static Size PAD_SIZE = new Size(1.7d, SizeUnit.mm); + public static Size HOLE_SIZE = new Size(0.8d, SizeUnit.mm); + public static Size NOTCH_SIZE = new Size(0.9d, SizeUnit.mm); + + public static Size MOUNTING_HOLE_DIAMETER = new Size(2.1d, SizeUnit.mm); + public static Size MOUNTING_HOLE_DISTANCE_X = new Size(11.4d, SizeUnit.mm); + public static Size MOUNTING_HOLE_MARGIN_Y = new Size(2.0d, SizeUnit.mm); + + public static Size BOOTSEL_WIDTH = new Size(3.0d, SizeUnit.mm); + public static Size BOOTSEL_LENGTH = new Size(4.0d, SizeUnit.mm); + public static Size BOOTSEL_OFFSET_X = new Size(3.5d, SizeUnit.mm); + public static Size BOOTSEL_OFFSET_Y = new Size(10.0d, SizeUnit.mm); + public static Size BOOTSEL_LABEL_OFFSET = new Size(1.3d, SizeUnit.mm); + // measured from the top edge of the chip up to the middle of the model name + public static Size MODEL_LABEL_OFFSET = new Size(2.5d, SizeUnit.mm); + + public static Size DEBUG_PAD_OFFSET_X = new Size(7.38d, SizeUnit.mm); + public static Size DEBUG_PAD_OFFSET_Y = new Size(19.8d, SizeUnit.mm); + public static Size WIFI_WIDTH = new Size(12.0d, SizeUnit.mm); + public static Size WIFI_LENGTH = new Size(11.0d, SizeUnit.mm); + // measured from the bottom board edge to the center of the shield, not to its top edge + public static Size WIFI_OFFSET_Y = new Size(12.2d, SizeUnit.mm); + + 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", + // 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", + // SWD debug pins (pins 40..42) + "SWCLK", "GND_SWD", "SWDIO" + }; + + protected PicoVersion version = PicoVersion.PICO; + protected boolean headers = false; + + public RaspberryPiPico() { + super(); + this.bodyColor = RPI_GREEN; + updateControlPoints(); + } + + @EditableProperty(name = "Version") + public PicoVersion getVersion() { + if (version == null) { + version = PicoVersion.PICO; + } + return version; + } + + public void setVersion(PicoVersion version) { + this.version = version; + updateControlPoints(); + invalidateCache(); + } + + /** + * True for the wireless variants, which carry the CYW43439 radio and move the SWD pads out of + * the bottom edge to make room for it. + */ + private boolean isW() { + return getVersion() == PicoVersion.PICO_W || getVersion() == PicoVersion.PICO_2_W; + } + + /** + * True for the RP2350 generation, which is pin- and size-identical to the RP2040 boards. + */ + private boolean isV2() { + return getVersion() == PicoVersion.PICO_2 || getVersion() == PicoVersion.PICO_2_W; + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double rowSpacing = new Size(0.7d, SizeUnit.in).convertToPixels(); // 140px + + double[][] relativeOffsets = new double[PIN_NAMES.length][2]; + + // Left row (0..19) + for (int i = 0; i < 20; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + // Right row (20..39) + for (int i = 0; i < 20; i++) { + relativeOffsets[20 + i][0] = rowSpacing; + relativeOffsets[20 + i][1] = i * spacing; + } + + if (isW()) { + double pin1OffsetX = new Size(1.61d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(1.37d, SizeUnit.mm).convertToPixels(); + double midX = BOARD_WIDTH.convertToPixels() - DEBUG_PAD_OFFSET_X.convertToPixels() - pin1OffsetX; + double midY = BOARD_LENGTH.convertToPixels() - DEBUG_PAD_OFFSET_Y.convertToPixels() - pin1OffsetY; + + relativeOffsets[40][0] = midX - spacing; + relativeOffsets[40][1] = midY; + relativeOffsets[41][0] = midX; + relativeOffsets[41][1] = midY; + relativeOffsets[42][0] = midX + spacing; + relativeOffsets[42][1] = midY; + } else { + // SWD Debug Header (40..42) at bottom center, aligned with bottom row of pins + double swdX = rowSpacing / 2.0 - spacing; + double swdY = 19 * spacing; + for (int i = 0; i < 3; i++) { + relativeOffsets[40 + i][0] = swdX + i * spacing; + relativeOffsets[40 + i][1] = swdY; + } + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_LENGTH.convertToPixels(); + double pin1OffsetX = new Size(1.61d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(1.37d, SizeUnit.mm).convertToPixels(); + double boardX = x - pin1OffsetX; + double boardY = y - pin1OffsetY; + + Area boardArea = new Area(new Rectangle2D.Double(boardX, boardY, boardW, boardH)); + + // the H variant ships with headers already soldered on and has plain edges instead of the + // castellated ones + if (!headers) { + double spacing = PIN_SPACING.convertToPixels(); + double pinY = boardY + pin1OffsetY; + subtractCastellationNotches(boardArea, pinY, 20, spacing, boardX); + subtractCastellationNotches(boardArea, pinY, 20, spacing, boardX + boardW); + } + + return boardArea; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_LENGTH.convertToPixels(); + double pin1OffsetX = new Size(1.61d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(1.37d, SizeUnit.mm).convertToPixels(); + double boardX = x - pin1OffsetX; + double boardY = y - pin1OffsetY; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Mounting holes, 2mm in from the top and bottom edges and symmetrical around the center line + double holeDiameter = MOUNTING_HOLE_DIAMETER.convertToPixels(); + double holeDistX = MOUNTING_HOLE_DISTANCE_X.convertToPixels() / 2.0; + double holeMarginY = MOUNTING_HOLE_MARGIN_Y.convertToPixels(); + double holeCenterX = boardX + boardW / 2.0; + double topHoleY = boardY + holeMarginY; + double bottomHoleY = boardY + boardH - holeMarginY; + MakerBoardPainter.drawMountingHole(g2d, holeCenterX - holeDistX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, holeCenterX + holeDistX, topHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, holeCenterX - holeDistX, bottomHoleY, holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, holeCenterX + holeDistX, bottomHoleY, holeDiameter); + + // Micro USB Connector + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = new Size(1.3d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMicroUsb(g2d, boardX + (boardW - usbW) / 2.0, boardY - usbOverhang, usbW, usbH, "USB"); + + // BOOTSEL button, sitting left of the board center line + double btnW = BOOTSEL_WIDTH.convertToPixels(); + double btnH = BOOTSEL_LENGTH.convertToPixels(); + double btnCenterX = boardX + boardW / 2.0 - BOOTSEL_OFFSET_X.convertToPixels(); + double btnX = btnCenterX - btnW / 2.0; + double btnY = boardY + BOOTSEL_OFFSET_Y.convertToPixels(); + MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH); + + // the label does not fit next to the button unless it is turned to read bottom to top, + // the way the real board prints it + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + double labelX = btnX - BOOTSEL_LABEL_OFFSET.convertToPixels(); + double labelY = btnY + btnH / 2.0; + AffineTransform oldLabelTx = g2d.getTransform(); + g2d.rotate(-Math.PI / 2, labelX, labelY); + StringUtils.drawCenteredText(g2d, "BOOTSEL", labelX, labelY, HorizontalAlignment.CENTER, + VerticalAlignment.CENTER); + g2d.setTransform(oldLabelTx); + + // RP2040 chip + double chipSize = new Size(7.0d, SizeUnit.mm).convertToPixels(); + double chipX = boardX + (boardW - chipSize) / 2.0; + double chipY = isW() + ? boardY + new Size(23.0d, SizeUnit.mm).convertToPixels() - new Size(0.1d, SizeUnit.in).convertToPixels() + : boardY + new Size(23.0d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawChip(g2d, chipX, chipY, chipSize, chipSize, isV2() ? "RP2350" : "RP2040"); + + // Model name, in the gap the board leaves between the BOOTSEL button and the chip + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, getVersion().toString(), boardX + boardW / 2.0, + chipY - MODEL_LABEL_OFFSET.convertToPixels(), HorizontalAlignment.CENTER, + VerticalAlignment.CENTER); + + if (isW()) { + double spacing = PIN_SPACING.convertToPixels(); + double midDebugX = boardX + boardW - DEBUG_PAD_OFFSET_X.convertToPixels(); + double midDebugY = boardY + boardH - DEBUG_PAD_OFFSET_Y.convertToPixels(); + + // DEBUG silkscreen text above SWD pins + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "DEBUG", midDebugX, midDebugY - new Size(2.2d, SizeUnit.mm).convertToPixels(), + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen outline box around 3 debug pins + double boxMarginX = new Size(1.2d, SizeUnit.mm).convertToPixels(); + double boxMarginY = new Size(1.2d, SizeUnit.mm).convertToPixels(); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double( + midDebugX - spacing - boxMarginX, + midDebugY - boxMarginY, + 2 * spacing + 2 * boxMarginX, + 2 * boxMarginY)); + + // Shielded CYW43439 wireless module, occupying the spot the Pi logo takes on the + // non-wireless boards + double wifiW = WIFI_WIDTH.convertToPixels(); + double wifiH = WIFI_LENGTH.convertToPixels(); + double wifiX = boardX + (boardW - wifiW) / 2.0; + double wifiY = boardY + boardH - WIFI_OFFSET_Y.convertToPixels() - wifiH / 2.0; + MakerBoardPainter.drawMetalConnector(g2d, wifiX, wifiY, wifiW, wifiH, ""); + } else { + // Raspberry Pi Logo at bottom of the board (~9mm height) + double logoSize = new Size(9.0d, SizeUnit.mm).convertToPixels(); + double logoWidth = logoSize * 72.515 / 92.604; + double logoX = boardX + (boardW - logoWidth) / 2.0; + double logoY = boardY + new Size(35.5d, SizeUnit.mm).convertToPixels(); + MakerBoardLogos.drawRaspberryPiLogo(g2d, logoX, logoY, logoSize); + + // DEBUG silkscreen text above SWD pins + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "DEBUG", boardX + boardW / 2.0, boardY + new Size(47.8d, SizeUnit.mm).convertToPixels(), + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + } + + drawingObserver.stopTracking(); + + if (!headers) { + drawCastellatedPads(g2d, boardX, boardY, boardW, boardH, pin1OffsetX, pin1OffsetY, outlineMode, drawingObserver); + } + + g2d.setTransform(oldTx); + + if (headers) { + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + } + + g2d.setComposite(oldComposite); + } + + @Override + protected Size getCastellatedPadSize() { + return PAD_SIZE; + } + + @Override + protected Size getCastellatedHoleSize() { + return HOLE_SIZE; + } + + @Override + protected Size getCastellatedNotchSize() { + return NOTCH_SIZE; + } + + @Override + protected Color getCastellatedPadColor() { + return PAD_COLOR; + } + + /** + * Helper to draw Raspberry Pi Pico's castellated solder pads along the left and right edges + * and the SWD round pads. + */ + protected void drawCastellatedPads(Graphics2D g2d, double boardX, double boardY, double boardW, double boardH, + double pin1OffsetX, double pin1OffsetY, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + + double spacing = PIN_SPACING.convertToPixels(); + int padD = getClosestOdd((int) Math.round(PAD_SIZE.convertToPixels())); + int holeD = getClosestOdd((int) Math.round(HOLE_SIZE.convertToPixels())); + double padR = padD / 2.0; + double holeR = holeD / 2.0; + double pinY = boardY + pin1OffsetY; + double rightEdge = boardX + boardW; + + // Left row (pins 0..19) and right row (pins 20..39) + drawCastellatedPads(g2d, boardX + pin1OffsetX, pinY, 20, spacing, boardX, outlineMode, drawingObserver); + drawCastellatedPads(g2d, rightEdge - pin1OffsetX, pinY, 20, spacing, rightEdge, outlineMode, + drawingObserver); + + drawingObserver.startTrackingContinuityArea(true); + + // SWD Debug Header (pins 40..42): standard round solder pads + if (isW()) { + double midX = boardX + boardW - DEBUG_PAD_OFFSET_X.convertToPixels(); + double midY = boardY + boardH - DEBUG_PAD_OFFSET_Y.convertToPixels(); + for (int i = 0; i < 3; i++) { + double px = midX + (i - 1) * spacing; + double py = midY; + + Area swdPadArea = new Area(new Ellipse2D.Double(px - padR, py - padR, padD, padD)); + swdPadArea.subtract(new Area(new Ellipse2D.Double(px - holeR, py - holeR, holeD, holeD))); + + g2d.setColor(PAD_COLOR); + g2d.fill(swdPadArea); + g2d.setColor(PAD_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(swdPadArea); + + // Inner through-hole drill hole (white circle) + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(px - holeR, py - holeR, holeD, holeD)); + g2d.setColor(PAD_COLOR.darker()); + g2d.draw(new Ellipse2D.Double(px - holeR, py - holeR, holeD, holeD)); + } + } else { + double swdX = boardX + (boardW - 2 * spacing) / 2.0; + double swdY = boardY + pin1OffsetY + 19 * spacing; + for (int i = 0; i < 3; i++) { + double px = swdX + i * spacing; + double py = swdY; + + Area swdPadArea = new Area(new Ellipse2D.Double(px - padR, py - padR, padD, padD)); + swdPadArea.subtract(new Area(new Ellipse2D.Double(px - holeR, py - holeR, holeD, holeD))); + + g2d.setColor(PAD_COLOR); + g2d.fill(swdPadArea); + g2d.setColor(PAD_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(swdPadArea); + + // Inner through-hole drill hole (white circle) + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(px - holeR, py - holeR, holeD, holeD)); + g2d.setColor(PAD_COLOR.darker()); + g2d.draw(new Ellipse2D.Double(px - holeR, py - holeR, holeD, holeD)); + } + } + + drawingObserver.stopTrackingContinuityArea(); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(RPI_GREEN); + g2d.fill(new RoundRectangle2D.Double(6, 2, width - 12, height - 4, 3, 3)); + g2d.setColor(RPI_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(6, 2, width - 12, height - 4, 3, 3)); + + // Castellated edge pads on left and right in icon + g2d.setColor(PAD_COLOR); + for (int y = 5; y <= height - 6; y += 3) { + g2d.fillRect(6, y, 3, 2); + g2d.fillRect(width - 9, y, 3, 2); + } + + // USB Connector + g2d.setColor(USB_METAL_COLOR); + g2d.fillRect(11, 2, 10, 3); + + // RP2040 chip + g2d.setColor(IC_BODY_COLOR); + g2d.fillRect(11, 12, 10, 10); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "PICO", width / 2, height / 2 + 10, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java new file mode 100644 index 000000000..d71228ff7 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/RaspberryPiZero.java @@ -0,0 +1,405 @@ +/* + * + * 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 java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardLogos; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Raspberry Pi Zero", category = "Controllers", + author = "Branislav Stojkovic", + description = "Raspberry Pi Zero / Zero W / Zero 2 W Compact Single Board Computer", + instanceNamePrefix = "SBC", zOrder = IDIYComponent.BOARD, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class RaspberryPiZero extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color RPI_GREEN = Color.decode("#1B5E20"); + public static Size BOARD_WIDTH = new Size(65.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(30.0d, SizeUnit.mm); + public static Font RPI_TITLE_FONT = new Font("SansSerif", Font.BOLD, 20); + + public static Color PAD_COLOR = GOLD_COLOR; + public static Size PAD_SIZE = new Size(0.065d, SizeUnit.in); + public static Size HOLE_SIZE = new Size(0.7d, SizeUnit.mm); + + public static final String[] PIN_NAMES; + static { + PIN_NAMES = new String[45]; + System.arraycopy(RaspberryPi.PIN_NAMES, 0, PIN_NAMES, 0, 40); + PIN_NAMES[40] = "MIPI (CSI)"; + PIN_NAMES[41] = "RUN 1"; + PIN_NAMES[42] = "RUN 2"; + PIN_NAMES[43] = "TV 1"; + PIN_NAMES[44] = "TV 2"; + } + + public enum ZeroVersion { + PI_ZERO("Pi Zero"), + PI_ZERO_W("Pi Zero W"), + PI_ZERO_2_W("Pi Zero 2 W"); + + private final String label; + + ZeroVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + + protected ZeroVersion version = ZeroVersion.PI_ZERO; + protected boolean headers = false; + + public RaspberryPiZero() { + super(); + this.bodyColor = RPI_GREEN; + updateControlPoints(); + } + + @EditableProperty(name = "Version") + public ZeroVersion getVersion() { + if (version == null) { + version = ZeroVersion.PI_ZERO; + } + return version; + } + + public void setVersion(ZeroVersion version) { + this.version = version; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + double[][] relativeOffsets = new double[PIN_NAMES.length][2]; + for (int col = 0; col < 20; col++) { + int pinOdd = col * 2; // Pin 1, 3, 5... (bottom/inner row of header) + int pinEven = col * 2 + 1; // Pin 2, 4, 6... (top/outer row of header) + relativeOffsets[pinOdd][0] = col * spacing; + relativeOffsets[pinOdd][1] = 0; + relativeOffsets[pinEven][0] = col * spacing; + relativeOffsets[pinEven][1] = -spacing; + } + + // Camera Connector (CSI / MIPI) on right edge + double pin1OffsetX = new Size(8.37d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(3.5d, SizeUnit.mm).convertToPixels() + spacing / 2.0; + double csiW = new Size(3.2d, SizeUnit.mm).convertToPixels(); + double csiH = new Size(16.5d, SizeUnit.mm).convertToPixels(); + double csiCenterX = BOARD_WIDTH.convertToPixels() - new Size(4.2d, SizeUnit.mm).convertToPixels() + csiW / 2.0 - pin1OffsetX; + double csiCenterY = new Size(6.75d, SizeUnit.mm).convertToPixels() + csiH / 2.0 - pin1OffsetY; + relativeOffsets[40] = new double[] {csiCenterX, csiCenterY}; + + // RUN and TV pads + double padX1 = 18 * spacing; + double padX2 = 19 * spacing; + double runY = 1 * spacing; + double tvY = 2 * spacing; + relativeOffsets[41] = new double[] {padX1, runY}; + relativeOffsets[42] = new double[] {padX2, runY}; + relativeOffsets[43] = new double[] {padX1, tvY}; + relativeOffsets[44] = new double[] {padX2, tvY}; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double pin1OffsetX = new Size(8.37d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(3.5d, SizeUnit.mm).convertToPixels() + PIN_SPACING.convertToPixels() / 2.0; + double boardX = x - pin1OffsetX; + double boardY = y - pin1OffsetY; + double cornerArc = new Size(6.0d, SizeUnit.mm).convertToPixels(); + + Area boardArea = new Area(new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, cornerArc, cornerArc)); + + double holeD = new Size(2.75d, SizeUnit.mm).convertToPixels(); + double holeR = holeD / 2.0; + double mX1 = boardX + new Size(3.5d, SizeUnit.mm).convertToPixels(); + double mX2 = boardX + boardW - new Size(3.5d, SizeUnit.mm).convertToPixels(); + double mY1 = boardY + new Size(3.5d, SizeUnit.mm).convertToPixels(); + double mY2 = boardY + boardH - new Size(3.5d, SizeUnit.mm).convertToPixels(); + + boardArea.subtract(new Area(new Ellipse2D.Double(mX1 - holeR, mY1 - holeR, holeD, holeD))); + boardArea.subtract(new Area(new Ellipse2D.Double(mX1 - holeR, mY2 - holeR, holeD, holeD))); + boardArea.subtract(new Area(new Ellipse2D.Double(mX2 - holeR, mY1 - holeR, holeD, holeD))); + boardArea.subtract(new Area(new Ellipse2D.Double(mX2 - holeR, mY2 - holeR, holeD, holeD))); + + return boardArea; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double pin1OffsetX = new Size(8.37d, SizeUnit.mm).convertToPixels(); + double pin1OffsetY = new Size(3.5d, SizeUnit.mm).convertToPixels() + PIN_SPACING.convertToPixels() / 2.0; + double boardX = x - pin1OffsetX; + double boardY = y - pin1OffsetY; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes (diameter 2.75mm, located 3.5mm from board edges) + double holeDiameter = new Size(2.75d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(3.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(3.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + new Size(3.5d, SizeUnit.mm).convertToPixels(), + boardY + boardH - new Size(3.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - new Size(3.5d, SizeUnit.mm).convertToPixels(), + boardY + new Size(3.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - new Size(3.5d, SizeUnit.mm).convertToPixels(), + boardY + boardH - new Size(3.5d, SizeUnit.mm).convertToPixels(), holeDiameter); + + // MicroSD card slot on the left edge + double sdW = new Size(11.0d, SizeUnit.mm).convertToPixels(); + double sdH = new Size(12.0d, SizeUnit.mm).convertToPixels(); + double sdX = boardX + new Size(1.5d, SizeUnit.mm).convertToPixels(); + double sdY = boardY + new Size(7.5d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMetalConnector(g2d, sdX, sdY, sdW, sdH, "SD"); + + // Mini HDMI Connector (center at X = 12.4mm, width = 11.2mm, height = 7.5mm, overhang = 1.5mm) + double hdmiW = new Size(11.2d, SizeUnit.mm).convertToPixels(); + double hdmiH = new Size(7.5d, SizeUnit.mm).convertToPixels(); + double hdmiX = boardX + new Size(12.4d, SizeUnit.mm).convertToPixels() - hdmiW / 2.0; + double hdmiY = boardY + boardH - new Size(6.0d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMetalConnector(g2d, hdmiX, hdmiY, hdmiW, hdmiH, "HDMI"); + + // Micro USB Data (center at X = 41.4mm, width = 7.5mm, height = 5.6mm, overhang = 1.4mm) + double usbW = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbOverhang = new Size(1.4d, SizeUnit.mm).convertToPixels(); + double usbX = boardX + new Size(41.4d, SizeUnit.mm).convertToPixels() - usbW / 2.0; + double usbY = boardY + boardH - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, usbX, usbY, usbW, usbH, "USB"); + + // Micro USB Power (center at X = 54.0mm, width = 7.5mm, height = 5.6mm, overhang = 1.4mm) + double pwrX = boardX + new Size(54.0d, SizeUnit.mm).convertToPixels() - usbW / 2.0; + double pwrY = boardY + boardH - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, pwrX, pwrY, usbW, usbH, "PWR"); + + // Camera Connector (CSI) on right edge + double csiW = new Size(3.2d, SizeUnit.mm).convertToPixels(); + double csiH = new Size(16.5d, SizeUnit.mm).convertToPixels(); + double csiX = boardX + boardW - new Size(4.2d, SizeUnit.mm).convertToPixels(); + double csiY = boardY + new Size(6.75d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawFpcConnector(g2d, csiX, csiY, csiW, csiH, true, ""); + + // Main SoC chip (Broadcom BCM2835 / RP3A0) + double socW = new Size(12.0d, SizeUnit.mm).convertToPixels(); + double socH = new Size(12.0d, SizeUnit.mm).convertToPixels(); + double socX = boardX + new Size(19.5d, SizeUnit.mm).convertToPixels(); + double socY = boardY + new Size(11.5d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawChip(g2d, socX, socY, socW, socH, ""); + + // Raspberry Pi logo on SoC chip + double logoSize = new Size(7.0d, SizeUnit.mm).convertToPixels(); + double logoW = logoSize * (72.51 / 92.604); + double logoX = socX + (socW - logoW) / 2.0; + double logoY = socY + (socH - logoSize) / 2.0; + MakerBoardLogos.drawRaspberryPiLogo(g2d, logoX, logoY, logoSize); + + // Version label to the right of the SoC. "Raspberry Pi Zero 2 W" overruns the board edge at + // the title size, so step the font down when the label does not fit. + String versionLabel = "Raspberry " + getVersion().toString(); + double labelCenterX = boardX + new Size(46.15d, SizeUnit.mm).convertToPixels(); + double labelMaxWidth = + 2 * (boardX + boardW - new Size(1.5d, SizeUnit.mm).convertToPixels() - labelCenterX); + g2d.setColor(Color.WHITE); + g2d.setFont(RPI_TITLE_FONT); + if (g2d.getFontMetrics().stringWidth(versionLabel) > labelMaxWidth) { + g2d.setFont(SILK_FONT_LARGE); + } + StringUtils.drawCenteredText(g2d, versionLabel, labelCenterX, + boardY + new Size(17.5d, SizeUnit.mm).convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // White silkscreen outline around GPIO pads + g2d.setColor(Color.WHITE); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + double hdrBoxX = boardX + new Size(7.0d, SizeUnit.mm).convertToPixels(); + double hdrBoxY = boardY + new Size(0.9d, SizeUnit.mm).convertToPixels(); + double hdrBoxW = new Size(51.0d, SizeUnit.mm).convertToPixels(); + double hdrBoxH = new Size(5.2d, SizeUnit.mm).convertToPixels(); + g2d.draw(new Rectangle2D.Double(hdrBoxX, hdrBoxY, hdrBoxW, hdrBoxH)); + + // Silkscreen "GPIO" label next to the top pins + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "GPIO", boardX + new Size(42.5d, SizeUnit.mm).convertToPixels(), + y + PIN_SPACING.convertToPixels(), HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen for RUN and TV pads + double spacing = PIN_SPACING.convertToPixels(); + double runBoxX = x + 18 * spacing - spacing / 2.0; + double runBoxY = y + 1 * spacing - spacing / 2.0; + g2d.draw(new Rectangle2D.Double(runBoxX, runBoxY, spacing * 2, spacing)); + + double tvBoxX = x + 18 * spacing - spacing / 2.0; + double tvBoxY = y + 2 * spacing - spacing / 2.0; + g2d.draw(new Rectangle2D.Double(tvBoxX, tvBoxY, spacing * 2, spacing)); + + StringUtils.drawCenteredText(g2d, "RUN", runBoxX - new Size(1.0d, SizeUnit.mm).convertToPixels(), + runBoxY + spacing / 2.0, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "TV", tvBoxX - new Size(1.0d, SizeUnit.mm).convertToPixels(), + tvBoxY + spacing / 2.0, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + + drawingObserver.stopTracking(); + + g2d.setTransform(oldTx); + + if (headers) { + drawPinHeader(g2d, 0, 40, outlineMode, drawingObserver); + // RUN and TV are typically left unpopulated even with headers + drawPcbSolderPads(g2d, 41, 2, true, outlineMode, drawingObserver); + drawPcbSolderPads(g2d, 43, 1, false, outlineMode, drawingObserver); + drawPcbSolderPads(g2d, 44, 1, true, outlineMode, drawingObserver); + } else { + drawSolderPads(g2d, outlineMode, drawingObserver); + } + + g2d.setComposite(oldComposite); + } + + /** + * Helper to draw GPIO solder pads (copper pads with drill holes, square for Pin 1). + */ + protected void drawSolderPads(Graphics2D g2d, boolean outlineMode, IDrawingObserver drawingObserver) { + drawPcbSolderPads(g2d, 0, 40, true, outlineMode, drawingObserver); + drawPcbSolderPads(g2d, 41, 2, true, outlineMode, drawingObserver); + drawPcbSolderPads(g2d, 43, 1, false, outlineMode, drawingObserver); + drawPcbSolderPads(g2d, 44, 1, true, outlineMode, drawingObserver); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(RPI_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(RPI_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + + // GPIO Solder Pads in icon + g2d.setColor(PAD_COLOR); + for (int x = 6; x <= width - 8; x += 3) { + g2d.fillRect(x, 7, 2, 2); + } + + // SoC + g2d.setColor(IC_BODY_COLOR); + g2d.fillRect(10, 12, 6, 6); + + // Connectors on bottom + g2d.setColor(USB_METAL_COLOR); + g2d.fillRect(6, height - 7, 4, 3); + g2d.fillRect(18, height - 7, 3, 3); + g2d.fillRect(23, height - 7, 3, 3); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "ZERO", width / 2 + 4, 21, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java new file mode 100644 index 000000000..413267cd6 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/Teensy.java @@ -0,0 +1,572 @@ +/* + * + * 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 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.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Teensy", category = "Controllers", + author = "Branislav Stojkovic", + description = "PJRC Teensy 4.0 / 4.1 (Cortex-M7) and Teensy 3.2 (Cortex-M4) Development Board", + 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_3_2("Teensy 3.2"), + Teensy_4_0("Teensy 4.0"), + Teensy_4_1("Teensy 4.1"); + + private final String label; + + TeensyVersion(String label) { + this.label = label; + } + + @Override + public String toString() { + return label; + } + } + + // Board color matches standard green PCB (like Pi Zero) + public static Color TEENSY_GREEN = Color.decode("#1B5E20"); + 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); + + // gap between the Ethernet header pads and the silkscreen box printed around them + public static Size ETH_BOX_MARGIN = new Size(1.2d, SizeUnit.mm); + + // ===== 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); + + // ===== Teensy 3.2 dimensions ===== + // The 3.2 shares the 4.0's 1.4" x 0.7" outline and pin positions, so it reuses BOARD_*_40. + + // 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" + }; + + // What the board prints between the two rows, which is shorter than the node name: the + // alternate functions the node names carry are not silkscreened at all, and the pads of the + // bottom cluster and the severable VUSB pad are printed on the underside, so these arrays cover + // the two pin rows only. + // + // The compact boards leave three of them blank because there is no room for them: the two bottom + // pads sit level with the five-hole cluster and the right ground with the VUSB pad. + public static final String[] SILK_NAMES_40 = new String[] { + // Left row (0..13) + "G", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "", + // Right row (14..27) + "5V", "", "3V", "23", "22", "21", "20", "19", "18", "17", "16", "15", "14", "" + }; + + // ===== 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, 3x2) + "ETH_TX-", "ETH_LED", "ETH_RX+", "ETH_TX+", "ETH_GND", "ETH_RX-", + // USB Host header (59..63, 1x5) + "USB_5V", "USB_D-", "USB_D+", "USB_GND1", "USB_GND2", + // VUSB (64) + "VUSB" + }; + + public static final String[] SILK_NAMES_41 = new String[] { + // Left row (0..23) + "G", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "3V", "24", + "25", "26", "27", "28", "29", "30", "31", "32", + // Right row (24..47) + "5V", "", "3V", "23", "22", "21", "20", "19", "18", "17", "16", "15", "14", "13", + "G", "41", "40", "39", "38", "37", "36", "35", "34", "33" + }; + + // ===== Teensy 3.2 Pinout (34 pins total) ===== + // Same edge-pin layout as the 4.0: two 1x14 rows, a 5-hole cluster on the bottom edge and the + // 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)", + "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)", + "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", + // VUSB (33) + "VUSB" + }; + + // Sharing the 4.0's outline, the 3.2 has the same three pads it cannot print. The one it brings + // out to the header in their place is the analog ground, one pin below Vin, and that is the pad + // the VUSB pad sits level with. + public static final String[] SILK_NAMES_32 = new String[] { + // Left row (0..13) + "G", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "", + // Right row (14..27) + "5V", "", "3V", "23", "22", "21", "20", "19", "18", "17", "16", "15", "14", "" + }; + + private TeensyVersion version = TeensyVersion.Teensy_4_0; + protected boolean headers = false; + + public Teensy() { + super(); + this.bodyColor = TEENSY_GREEN; + updateControlPoints(); + } + + @EditableProperty(name = "Version") + public TeensyVersion getVersion() { + if (version == null) { + version = TeensyVersion.Teensy_4_0; + } + return version; + } + + /** + * True for the 1.4" x 0.7" boards, which share the 2x14 edge rows, the bottom five-hole cluster + * and the VUSB pad. Only the 4.1 has the longer body with the Ethernet and USB host headers. + */ + private boolean isCompact() { + return getVersion() != TeensyVersion.Teensy_4_1; + } + + private Size getBoardWidth() { + return isCompact() ? BOARD_WIDTH_40 : BOARD_WIDTH_41; + } + + private Size getBoardLength() { + return isCompact() ? BOARD_LENGTH_40 : BOARD_LENGTH_41; + } + + public void setVersion(TeensyVersion version) { + this.version = version; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + private String[] getPinNames() { + switch (getVersion()) { + case Teensy_4_1: + return PIN_NAMES_41; + case Teensy_3_2: + return PIN_NAMES_32; + default: + return PIN_NAMES_40; + } + } + + private String[] getSilkNames() { + switch (getVersion()) { + case Teensy_4_1: + return SILK_NAMES_41; + case Teensy_3_2: + return SILK_NAMES_32; + default: + return SILK_NAMES_40; + } + } + + private int getPinsPerRow() { + return isCompact() ? 14 : 24; + } + + @Override + public String getControlPointNodeName(int index) { + String[] names = getPinNames(); + if (index >= 0 && index < names.length) { + return names[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected String getSilkPinLabel(int index) { + String[] silkNames = getSilkNames(); + if (index >= 0 && index < silkNames.length) { + return silkNames[index]; + } + return super.getSilkPinLabel(index); + } + + private double[][] getRelativeOffsets() { + double spacing = PIN_SPACING.convertToPixels(); + double rowSpacing = ROW_SPACING.convertToPixels(); + + if (isCompact()) { + double[][] relativeOffsets = new double[getPinNames().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; + + return 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 SD card / switch at Y=18*spacing (45.72mm) + for (int k = 1; k <= 5; k++) { + relativeOffsets[48 + k - 1][0] = k * spacing; + relativeOffsets[48 + k - 1][1] = 18 * spacing; + } + + // Ethernet Header (53..58): 3x2 header (2.0mm pitch, rotated 90 degrees) + // Center is 4.45mm left of right column, 13.97mm down from first pin + double ethPitch = new Size(2.0d, SizeUnit.mm).convertToPixels(); + double ethCenterX = rowSpacing - new Size(4.45d, SizeUnit.mm).convertToPixels(); + double ethCenterY = new Size(13.97d, SizeUnit.mm).convertToPixels(); + // Row 1 (top): TX-, LED, RX+ + relativeOffsets[53] = new double[] {ethCenterX - ethPitch, ethCenterY - ethPitch / 2.0}; + relativeOffsets[54] = new double[] {ethCenterX, ethCenterY - ethPitch / 2.0}; + relativeOffsets[55] = new double[] {ethCenterX + ethPitch, ethCenterY - ethPitch / 2.0}; + // Row 2 (bottom): TX+, GND, RX- + relativeOffsets[56] = new double[] {ethCenterX - ethPitch, ethCenterY + ethPitch / 2.0}; + relativeOffsets[57] = new double[] {ethCenterX, ethCenterY + ethPitch / 2.0}; + relativeOffsets[58] = new double[] {ethCenterX + ethPitch, ethCenterY + ethPitch / 2.0}; + + // USB Host Header (59..63): 1x5 header (2.54mm pitch) + // Spaced 3.05mm center-to-center from left column, Y = (2.5 + i) * spacing + double usbHostX = new Size(3.05d, SizeUnit.mm).convertToPixels(); + for (int i = 0; i < 5; i++) { + relativeOffsets[59 + i][0] = usbHostX; + relativeOffsets[59 + i][1] = (2.5 + i) * spacing; + } + + // VUSB (64): near VIN at X=rowSpacing - spacing, Y=spacing + relativeOffsets[64][0] = rowSpacing - spacing; + relativeOffsets[64][1] = spacing; + + return relativeOffsets; + } + } + + @Override + protected void updateControlPoints() { + rotatePoints(controlPoints[0], getRelativeOffsets()); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + double boardW = getBoardWidth().convertToPixels(); + double boardH = getBoardLength().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 = USB_MICRO_WIDTH.convertToPixels(); + double usbH = USB_MICRO_LENGTH.convertToPixels(); + double usbX = centerX - usbW / 2.0; + double usbY = boardY - new Size(1.0d, SizeUnit.mm).convertToPixels(); + MakerBoardPainter.drawMicroUsb(g2d, usbX, usbY, usbW, usbH, "USB"); + + // Main MCU chip: NXP i.MX RT1062 BGA on the 4.x, Kinetis MK20DX256 LQFP-64 on the 3.2 + double chipW = isCompact() + ? new Size(0.4d, SizeUnit.in).convertToPixels() + : new Size(11.0d, SizeUnit.mm).convertToPixels(); + double chipH = isCompact() + ? new Size(0.4d, SizeUnit.in).convertToPixels() + : new Size(11.0d, SizeUnit.mm).convertToPixels(); + double chipX = centerX - chipW / 2.0; + double chipY = isCompact() + ? boardY + new Size(9.5d, SizeUnit.mm).convertToPixels() + new Size(0.1d, SizeUnit.in).convertToPixels() + : boardY + new Size(19.0d, SizeUnit.mm).convertToPixels() + new Size(0.15d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawChip(g2d, chipX, chipY, chipW, chipH, + getVersion() == TeensyVersion.Teensy_3_2 ? "MK20DX256" : "iMXRT1062"); + + // Pushbutton (Program button) + double btnW = BUTTON_WIDTH.convertToPixels(); + double btnH = BUTTON_LENGTH.convertToPixels(); + double btnX = centerX - btnW / 2.0; + double btnY = isCompact() + ? boardY + PIN1_OFFSET_Y.convertToPixels() + 11.5 * PIN_SPACING.convertToPixels() - btnH / 2.0 + : boardY + new Size(35.5d, SizeUnit.mm).convertToPixels() + new Size(0.25d, SizeUnit.in).convertToPixels(); + MakerBoardPainter.drawButton(g2d, btnX, btnY, btnW, btnH); + + // Teensy 4.1 extras: Ethernet PHY + SD card slot + if (getVersion() == TeensyVersion.Teensy_4_1) { + // MicroSD card slot at bottom edge (12mm long, flush with bottom edge so it does not stick out) + double sdW = new Size(12.0d, SizeUnit.mm).convertToPixels(); + double sdH = new Size(12.0d, SizeUnit.mm).convertToPixels(); + double sdX = centerX - sdW / 2.0; + double sdY = boardY + boardH - sdH; + MakerBoardPainter.drawMetalConnector(g2d, sdX, sdY, sdW, sdH, "SD"); + + // Silkscreen outline box grouping the six Ethernet pads, taken from the corner pads of the + // header so it follows them if the footprint moves + double[][] offsets = getRelativeOffsets(); + double boxMargin = ETH_BOX_MARGIN.convertToPixels(); + g2d.setColor(SILK_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double( + x + offsets[53][0] - boxMargin, + y + offsets[53][1] - boxMargin, + offsets[58][0] - offsets[53][0] + 2 * boxMargin, + offsets[58][1] - offsets[53][1] + 2 * boxMargin)); + + // Silkscreen box around the USB host header, split so that the first pad sits in a square + // of its own the way the board marks pin 1. A half pitch margin is what squares it off. + double usbBoxMargin = PIN_SPACING.convertToPixels() / 2.0; + double usbBoxX = x + offsets[59][0] - usbBoxMargin; + double usbBoxY = y + offsets[59][1] - usbBoxMargin; + double usbBoxW = 2 * usbBoxMargin; + g2d.draw(new Rectangle2D.Double(usbBoxX, usbBoxY, usbBoxW, + offsets[63][1] - offsets[59][1] + 2 * usbBoxMargin)); + g2d.draw(new Rectangle2D.Double(usbBoxX, usbBoxY, usbBoxW, usbBoxW)); + } + + // Silkscreen "TEENSY" label + g2d.setColor(SILK_COLOR); + g2d.setFont(SILK_FONT); + String silkText = getVersion().toString(); + double textY = isCompact() + ? 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); + + // Pad names, printed between the two rows the way they are on the board + // the 4.1 has to fit twenty four names down each side, so it prints them cornerwise + if (isCompact()) { + drawPinLabels(g2d, x, y, getRelativeOffsets(), getPinsPerRow(), SILK_COLOR); + } else { + drawDiagonalPinLabels(g2d, x, y, getRelativeOffsets(), getPinsPerRow(), SILK_COLOR); + } + } + + drawingObserver.stopTracking(); + + g2d.setTransform(oldTx); + + // Render pin headers or gold solder pads with drill holes + if (headers) { + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + } else { + 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); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/micro/WemosD1Mini.java b/diylc/diylc-library/src/main/java/org/diylc/components/micro/WemosD1Mini.java new file mode 100644 index 000000000..01f891373 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/micro/WemosD1Mini.java @@ -0,0 +1,383 @@ +/* + * + * 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 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.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +@ComponentDescriptor(name = "Wemos D1 Mini", category = "Controllers", + author = "Branislav Stojkovic", description = "Wemos D1 Mini ESP8266 Wi-Fi Development Board", + instanceNamePrefix = "MCU", zOrder = IDIYComponent.COMPONENT, + bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, + enableCache = true) +public class WemosD1Mini extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color WEMOS_BLUE = Color.decode("#006699"); + public static Color ANTENNA_COLOR = Color.decode("#DAA520"); + public static Color ANTENNA_BG_COLOR = Color.decode("#1E1E1E"); + 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 Size BOARD_WIDTH = new Size(1.0d, SizeUnit.in); + public static Size BOARD_LENGTH = new Size(1.34d, SizeUnit.in); + public static Size ROW_SPACING = new Size(0.9d, SizeUnit.in); + public static Size TOP_MARGIN = new Size(0.275d, SizeUnit.in); + public static Size TOP_ROUNDING = new Size(4.6d, SizeUnit.mm); + public static Size BOTTOM_ROUNDING = new Size(0.8d, SizeUnit.mm); + + public static Size MODULE_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size MODULE_LENGTH = new Size(7.0d, SizeUnit.mm); + public static Size ANTENNA_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size ANTENNA_LENGTH = new Size(5.5d, SizeUnit.mm); + public static Size SHIELD_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size SHIELD_LENGTH = new Size(14.54d, SizeUnit.mm); + + public static Size USB_WIDTH = USB_MICRO_WIDTH; + public static Size USB_LENGTH = USB_MICRO_LENGTH; + public static Size USB_CUTOUT_WIDTH = new Size(8.0d, SizeUnit.mm); + public static Size USB_CUTOUT_DEPTH = new Size(1.8d, SizeUnit.mm); + + public static Size RST_CUTOUT_WIDTH = new Size(2.0d, SizeUnit.mm); + public static Size RST_CUTOUT_LENGTH = new Size(6.5d, SizeUnit.mm); + public static Size RST_CUTOUT_SLANT = new Size(2.0d, SizeUnit.mm); + public static Size RST_BTN_WIDTH = new Size(1.0d, SizeUnit.mm); + public static Size RST_BTN_LENGTH = new Size(2.0d, SizeUnit.mm); + public static Size RST_BTN_OFFSET_Y = new Size(2.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + // Left row (pins 0..7) + "RST", "A0", "D0 (GPIO16)", "D5 (GPIO14)", "D6 (GPIO12)", "D7 (GPIO13)", "D8 (GPIO15)", "3V3", + // Right row (pins 8..15) + "5V", "G (GND)", "D4 (GPIO2)", "D3 (GPIO0)", "D2 (GPIO4)", "D1 (GPIO5)", "RX (GPIO3)", "TX (GPIO1)" + }; + + public static final String[] SILK_PIN_NAMES_LEFT = new String[] { + "RST", "A0", "D0", "D5", "D6", "D7", "D8", "3V3" + }; + + public static final String[] SILK_PIN_NAMES_RIGHT = new String[] { + "TX", "RX", "D1", "D2", "D3", "D4", "G", "5V" + }; + + protected boolean headers = false; + + public WemosD1Mini() { + super(); + this.bodyColor = WEMOS_BLUE; + updateControlPoints(); + } + + @EditableProperty(name = "Headers") + public boolean getHeaders() { + return headers; + } + + public void setHeaders(boolean headers) { + this.headers = headers; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double rowSpacing = ROW_SPACING.convertToPixels(); + + double[][] relativeOffsets = new double[16][2]; + for (int i = 0; i < 8; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 8; i++) { + relativeOffsets[8 + i][0] = rowSpacing; + relativeOffsets[8 + i][1] = (7 - i) * spacing; + } + + 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 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 rTop = TOP_ROUNDING.convertToPixels(); + double rBottom = BOTTOM_ROUNDING.convertToPixels(); + double usbCutoutW = USB_CUTOUT_WIDTH.convertToPixels(); + double usbCutoutH = USB_CUTOUT_DEPTH.convertToPixels(); + double rstCutoutW = RST_CUTOUT_WIDTH.convertToPixels(); + double rstCutoutH = RST_CUTOUT_LENGTH.convertToPixels(); + double rstCutoutSlant = RST_CUTOUT_SLANT.convertToPixels(); + + double cx = boardX + boardW / 2.0; + double bottomY = boardY + boardH; + double rightX = boardX + boardW; + + Path2D.Double path = new Path2D.Double(); + // Start at top-left, after top-left corner curve + path.moveTo(boardX + rTop, boardY); + // Top edge + path.lineTo(rightX - rTop, boardY); + // Top-right corner curve (larger radius) + path.quadTo(rightX, boardY, rightX, boardY + rTop); + // Right edge down to bottom-right corner + path.lineTo(rightX, bottomY - rBottom); + // Bottom-right corner curve (smaller radius) + path.quadTo(rightX, bottomY, rightX - rBottom, bottomY); + // Bottom edge towards USB cutout + path.lineTo(cx + usbCutoutW / 2.0, bottomY); + // USB cutout (slightly wider than the USB connector) + path.lineTo(cx + usbCutoutW / 2.0, bottomY - usbCutoutH); + path.lineTo(cx - usbCutoutW / 2.0, bottomY - usbCutoutH); + path.lineTo(cx - usbCutoutW / 2.0, bottomY); + // Bottom edge towards Reset button cutout + path.lineTo(boardX + rstCutoutW, bottomY); + // Reset button cutout inner vertical wall + path.lineTo(boardX + rstCutoutW, bottomY - (rstCutoutH - rstCutoutSlant)); + // Slanted edge transitioning to outer board edge + path.lineTo(boardX, bottomY - rstCutoutH); + // Left edge up to top-left corner + path.lineTo(boardX, boardY + rTop); + // Top-left corner curve (larger radius) + path.quadTo(boardX, boardY, boardX + rTop, boardY); + path.closePath(); + + return path; + } + + @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 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 bottomY = boardY + boardH; + + double moduleW = MODULE_WIDTH.convertToPixels(); + double moduleH = MODULE_LENGTH.convertToPixels(); + double moduleX = (x + rowSpacing / 2.0) - moduleW / 2.0; + + double shieldW = SHIELD_WIDTH.convertToPixels(); + double shieldH = SHIELD_LENGTH.convertToPixels(); + double shieldX = (x + rowSpacing / 2.0) - shieldW / 2.0; + double shieldY = boardY + moduleH; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Antenna (dark rectangle underneath + gold serpentine trace) + MakerBoardPainter.drawPcbAntenna(g2d, moduleX, boardY, moduleW, moduleH); + + // ESP8266 Metal Shield (main chip module) + MakerBoardPainter.drawMetalConnector(g2d, shieldX, shieldY, shieldW, shieldH, "ESP8266"); + + // Micro-USB Jack drawn with 0.5mm overhang at the bottom edge + double usbW = USB_WIDTH.convertToPixels(); + double usbH = USB_LENGTH.convertToPixels(); + double usbOverhang = USB_MICRO_OVERHANG.convertToPixels(); + double usbX = (x + rowSpacing / 2.0) - usbW / 2.0; + double usbY = bottomY - usbH + usbOverhang; + MakerBoardPainter.drawMicroUsb(g2d, usbX, usbY, usbW, usbH, "USB"); + + // Reset button: small black rectangle touching the board in the bottom left cutout, moved 2mm up + double rstCutoutW = RST_CUTOUT_WIDTH.convertToPixels(); + double btnW = RST_BTN_WIDTH.convertToPixels(); + double btnH = RST_BTN_LENGTH.convertToPixels(); + double btnOffsetY = RST_BTN_OFFSET_Y.convertToPixels(); + double btnX = boardX + rstCutoutW - btnW; + double btnY = bottomY - btnH - btnOffsetY; + + g2d.setColor(BUTTON_BODY_COLOR); + g2d.fill(new Rectangle2D.Double(btnX, btnY, btnW, btnH)); + + // Reset button label on the board next to the button + g2d.setColor(SILK_COLOR); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + double rstLabelX = boardX + rstCutoutW + new Size(0.4d, SizeUnit.mm).convertToPixels(); + StringUtils.drawCenteredText(g2d, "RST", rstLabelX, btnY + btnH / 2.0, + HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + + // Silkscreen: D1 mini label + g2d.setFont(SILK_FONT); + double labelY = shieldY + shieldH + new Size(2.5d, SizeUnit.mm).convertToPixels(); + StringUtils.drawCenteredText(g2d, "D1 mini", x + rowSpacing / 2.0, labelY, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen: Pin labels + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + double spacing = PIN_SPACING.convertToPixels(); + double labelOffset = new Size(1.4d, SizeUnit.mm).convertToPixels(); + for (int i = 0; i < 8; i++) { + double pinY = y + i * spacing; + // Left pin labels + StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_LEFT[i], x + labelOffset, pinY, + HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + // Right pin labels + StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_RIGHT[i], x + rowSpacing - labelOffset, pinY, + HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + if (headers) { + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + } else { + drawPcbSolderPads(g2d, 0, controlPoints.length, false, outlineMode, drawingObserver); + } + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + Path2D.Double iconShape = new Path2D.Double(); + double rT = 8.0; + double rB = 2.0; + iconShape.moveTo(5 + rT, 3); + iconShape.lineTo(width - 5 - rT, 3); + iconShape.quadTo(width - 5, 3, width - 5, 3 + rT); + iconShape.lineTo(width - 5, height - 4 - rB); + iconShape.quadTo(width - 5, height - 4, width - 5 - rB, height - 4); + iconShape.lineTo(width / 2.0 + 4, height - 4); + iconShape.lineTo(width / 2.0 + 4, height - 7); + iconShape.lineTo(width / 2.0 - 4, height - 7); + iconShape.lineTo(width / 2.0 - 4, height - 4); + iconShape.lineTo(8, height - 4); + iconShape.lineTo(8, height - 9); + iconShape.lineTo(5, height - 12); + iconShape.lineTo(5, 3 + rT); + iconShape.quadTo(5, 3, 5 + rT, 3); + iconShape.closePath(); + + g2d.setColor(WEMOS_BLUE); + g2d.fill(iconShape); + g2d.setColor(WEMOS_BLUE.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(iconShape); + + // Black rectangle from top of board down to shield + g2d.setColor(Color.BLACK); + g2d.fillRect(8, 3, width - 16, 7); + + // Antenna trace + g2d.setColor(ANTENNA_COLOR); + g2d.drawLine(10, 4, 12, 4); + g2d.drawLine(12, 4, 12, 6); + g2d.drawLine(12, 6, 16, 6); + g2d.drawLine(16, 6, 16, 4); + g2d.drawLine(16, 4, 20, 4); + g2d.drawLine(20, 4, 20, 6); + g2d.drawLine(20, 6, 22, 6); + + // Metal shield + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fill(new RoundRectangle2D.Double(8, 10, width - 16, 11, 2, 2)); + + // Reset button in icon + g2d.setColor(Color.BLACK); + g2d.fillRect(6, height - 8, 2, 3); + + // USB connector flush with bottom edge + g2d.setColor(USB_METAL_COLOR); + g2d.fill(new RoundRectangle2D.Double(width / 2.0 - 3, height - 8, 6, 4, 1, 1)); + + // Text + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "D1", width / 2.0, height / 2.0 + 9, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/A4988StepperDriver.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/A4988StepperDriver.java new file mode 100644 index 000000000..03d072e88 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/A4988StepperDriver.java @@ -0,0 +1,199 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Stepper Driver (A4988 / DRV8825)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "A4988 / DRV8825 Microstepping Stepper Motor Driver Carrier", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class A4988StepperDriver extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color A4988_GREEN = Color.decode("#2E7D32"); + public static Color HEATSINK_COLOR = Color.decode("#B0BEC5"); + public static Color HEATSINK_FIN_COLOR = Color.decode("#78909C"); + public static Color TRIMPOT_COLOR = Color.decode("#CFD8DC"); + public static Size BOARD_WIDTH = new Size(15.2d, SizeUnit.mm); + public static Size BOARD_LENGTH = new Size(20.3d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + // Left row (pins 0..7) + "ENABLE", "MS1", "MS2", "MS3", "RESET", "SLEEP", "STEP", "DIR", + // Right row (pins 8..15) + "VMOT", "GND_PWR", "2B", "2A", "1A", "1B", "VDD", "GND_LOGIC" + }; + + public A4988StepperDriver() { + super(); + this.bodyColor = A4988_GREEN; + 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 = new Size(0.5d, SizeUnit.in).convertToPixels(); // 100px + + double[][] relativeOffsets = new double[16][2]; + for (int i = 0; i < 8; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 8; i++) { + relativeOffsets[8 + i][0] = rowSpacing; + relativeOffsets[8 + i][1] = (7 - i) * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double rowSpacing = new Size(0.5d, SizeUnit.in).convertToPixels(); + double boardW = rowSpacing + 20; + double boardH = 9 * PIN_SPACING.convertToPixels(); + double boardX = x - 10; + double boardY = y - 10; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double rowSpacing = new Size(0.5d, SizeUnit.in).convertToPixels(); + double boardW = rowSpacing + 20; + double boardH = 9 * PIN_SPACING.convertToPixels(); + double boardX = x - 10; + double boardY = y - 10; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Aluminum heatsink on top of chip in center + double hsSize = 45; + double hsX = boardX + (boardW - hsSize) / 2.0; + double hsY = boardY + 35; + + g2d.setColor(HEATSINK_COLOR); + g2d.fill(new RoundRectangle2D.Double(hsX, hsY, hsSize, hsSize, 3, 3)); + g2d.setColor(HEATSINK_FIN_COLOR); + g2d.draw(new RoundRectangle2D.Double(hsX, hsY, hsSize, hsSize, 3, 3)); + // Heatsink fins + g2d.drawLine((int) hsX + 15, (int) hsY + 4, (int) hsX + 15, (int) (hsY + hsSize - 4)); + g2d.drawLine((int) hsX + 30, (int) hsY + 4, (int) hsX + 30, (int) (hsY + hsSize - 4)); + + // Tiny metal trimpot at bottom + g2d.setColor(TRIMPOT_COLOR); + g2d.fill(new RoundRectangle2D.Double(boardX + (boardW - 25) / 2.0, boardY + boardH - 35, 25, 20, 2, 2)); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "A4988", boardX + boardW / 2.0, boardY + 20, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(A4988_GREEN); + g2d.fill(new RoundRectangle2D.Double(5, 3, width - 10, height - 6, 3, 3)); + g2d.setColor(A4988_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(5, 3, width - 10, height - 6, 3, 3)); + + // Heatsink + g2d.setColor(HEATSINK_COLOR); + g2d.fillRect(10, 8, width - 20, 10); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "STEP", width / 2, height / 2 + 7, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java new file mode 100644 index 000000000..223aafade --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/ActiveBuzzerModule.java @@ -0,0 +1,225 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Active Buzzer Module (KY-012)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "KY-012 Active Buzzer Module — produces 2.5kHz continuous tone on HIGH signal", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class ActiveBuzzerModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#1A6B1A"); + public static Color BUZZER_BLACK = Color.decode("#1A1A1A"); + public static Color BUZZER_VENT_COLOR = Color.decode("#333333"); + + public static Size BOARD_WIDTH = new Size(18.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(15.0d, SizeUnit.mm); + + // Standard Keyes KY-012 3-pin layout from left to right: - (GND), + (VCC), S (Signal) + private static final String[] PIN_NAMES = {"- (GND)", "+ (VCC)", "S (Signal)"}; + + public ActiveBuzzerModule() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 3 pins along the bottom edge: - (GND), + (VCC), S (Signal) + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { spacing * 2, 0 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + double spacing = PIN_SPACING.convertToPixels(); + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Buzzer cylinder — circular element on the board + double buzzerD = new Size(11.0d, SizeUnit.mm).convertToPixels(); + double buzzerX = boardX + (boardW - buzzerD) / 2.0; + double buzzerY = boardY + 4; + + // Outer black cylinder + g2d.setColor(BUZZER_BLACK); + g2d.fill(new Ellipse2D.Double(buzzerX, buzzerY, buzzerD, buzzerD)); + g2d.setColor(BUZZER_BLACK.brighter()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(buzzerX, buzzerY, buzzerD, buzzerD)); + + // Vent hole ring (inner concentric) + double ventD = buzzerD * 0.55; + double ventX = buzzerX + (buzzerD - ventD) / 2.0; + double ventY = buzzerY + (buzzerD - ventD) / 2.0; + g2d.setColor(BUZZER_VENT_COLOR); + g2d.draw(new Ellipse2D.Double(ventX, ventY, ventD, ventD)); + + // Small central sound vent hole + double holeD = buzzerD * 0.15; + double holeX = buzzerX + (buzzerD - holeD) / 2.0; + double holeY = buzzerY + (buzzerD - holeD) / 2.0; + g2d.setColor(Color.decode("#111111")); + g2d.fill(new Ellipse2D.Double(holeX, holeY, holeD, holeD)); + + // Polarity "+" marking on buzzer casing + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 10)); + StringUtils.drawCenteredText(g2d, "+", buzzerX + 6, buzzerY + 7, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // "KY-012" model text in top-left silkscreen corner + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + StringUtils.drawCenteredText(g2d, "KY-012", boardX + 14, boardY + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen pin labels (-, +, S) directly above the 3 header pins + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + double labelY = y - 9; + StringUtils.drawCenteredText(g2d, "-", x, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "+", x + spacing, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "S", x + spacing * 2, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + + // Black buzzer cylinder + int bD = width - 10; + int bX = (width - bD) / 2; + int bY = 5; + g2d.setColor(BUZZER_BLACK); + g2d.fillOval(bX, bY, bD, bD); + g2d.setColor(Color.DARK_GRAY); + g2d.drawOval(bX, bY, bD, bD); + + // Vent circle + int vD = bD / 2; + g2d.setColor(BUZZER_VENT_COLOR); + g2d.drawOval(bX + (bD - vD) / 2, bY + (bD - vD) / 2, vD, vD); + + // 3 pin dots + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 3; i++) { + g2d.fillRect(6 + i * 4, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/AnalogJoystickKY023.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/AnalogJoystickKY023.java new file mode 100644 index 000000000..24e71e548 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/AnalogJoystickKY023.java @@ -0,0 +1,201 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Analog Joystick (KY-023)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "KY-023 Dual-Axis PS2 Thumb Joystick Module with Push Switch", +// instanceNamePrefix = "JOY", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class AnalogJoystickKY023 extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color JOY_PCB = Color.decode("#1B2631"); + public static Color POT_GREEN = Color.decode("#27AE60"); + public static Color THUMB_CAP = Color.decode("#17202A"); + public static Color GIMBAL_METAL = Color.decode("#7F8C8D"); + public static Color THUMB_INNER = Color.decode("#2C3E50"); + public static Color THUMB_WELL = Color.decode("#111111"); + + public static Size BOARD_WIDTH = new Size(34.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(26.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"GND", "+5V", "VRx", "VRy", "SW"}; + + public AnalogJoystickKY023() { + super(); + this.bodyColor = JOY_PCB; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 5-pin 0.1" header: GND, +5V, VRx, VRy, SW + double[][] relativeOffsets = new double[5][2]; + for (int i = 0; i < 5; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 4 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 4 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting Holes in corners + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardH - 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 40, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 40, boardY + boardH - 16, 16); + + // Metal Gimbal Frame (Center) + double cx = boardX + (boardW - 30) / 2.0; + double cy = boardY + boardH / 2.0; + g2d.setColor(GIMBAL_METAL); + g2d.fill(new RoundRectangle2D.Double(cx - 50, cy - 50, 100, 100, 8, 8)); + + // Potentiometer cases (X and Y side blocks) + g2d.setColor(POT_GREEN); + g2d.fill(new RoundRectangle2D.Double(cx - 56, cy - 30, 12, 60, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(cx - 30, cy + 44, 60, 12, 3, 3)); + + // Circular Rubber Thumbstick Cap + g2d.setColor(THUMB_CAP); + g2d.fill(new Ellipse2D.Double(cx - 42, cy - 42, 84, 84)); + g2d.setColor(THUMB_INNER); + g2d.fill(new Ellipse2D.Double(cx - 30, cy - 30, 60, 60)); + g2d.setColor(THUMB_WELL); + g2d.fill(new Ellipse2D.Double(cx - 20, cy - 20, 40, 40)); + + // Silk Screen Pin Labels + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + String[] labels = new String[] { "GND", "+5V", "VRx", "VRy", "SW" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 5; i++) { + StringUtils.drawCenteredText(g2d, labels[i], boardX + boardW - 32, y + i * spacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw 5 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(JOY_PCB); + g2d.fill(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + g2d.setColor(THUMB_CAP); + g2d.fill(new Ellipse2D.Double(width / 2.0 - 10, height / 2.0 - 10, 20, 20)); + g2d.setColor(Color.BLACK); + g2d.fill(new Ellipse2D.Double(width / 2.0 - 5, height / 2.0 - 5, 10, 10)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java new file mode 100644 index 000000000..5150eec78 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/GPSModuleNEO6M.java @@ -0,0 +1,246 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "GPS Module (NEO-6M)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "u-blox NEO-6M GPS Module with ceramic patch antenna", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class GPSModuleNEO6M extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#1A6B1A"); + public static Color ANTENNA_BEIGE = Color.decode("#D5C4A1"); + public static Color ANTENNA_BORDER = Color.decode("#928374"); + public static Color SHIELD_COLOR = Color.decode("#B8B8B8"); + public static Color SHIELD_BORDER = Color.decode("#888888"); + public static Color BATTERY_COLOR = Color.decode("#D0D3D4"); + + public static Size BOARD_WIDTH = new Size(35.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(25.5d, SizeUnit.mm); + + // Standard GY-GPS6MV2 pinout from pin 1 to 4: VCC, RX, TX, GND + private static final String[] PIN_NAMES = {"VCC", "RX", "TX", "GND"}; + + public GPSModuleNEO6M() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 4 pins along the left edge: VCC, RX, TX, GND + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { 0, spacing }, + { 0, spacing * 2 }, + { 0, spacing * 3 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double spacing = PIN_SPACING.convertToPixels(); + double boardX = x - 14.0; + double boardY = y - (boardH - 3 * spacing) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double spacing = PIN_SPACING.convertToPixels(); + double boardX = x - 14.0; + double boardY = y - (boardH - 3 * spacing) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Small Mounting holes in the corners + double holeMargin = 7.0; + MakerBoardPainter.drawMountingHole(g2d, boardX + holeMargin, boardY + holeMargin, 8.0); + MakerBoardPainter.drawMountingHole(g2d, boardX + holeMargin, boardY + boardH - holeMargin, 8.0); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + holeMargin, 8.0); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + boardH - holeMargin, 8.0); + + // Silkscreen pin labels (VCC, RX, TX, GND) placed neatly next to left header + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + String[] pinLabels = new String[] {"VCC", "RX", "TX", "GND"}; + for (int i = 0; i < 4; i++) { + StringUtils.drawCenteredText(g2d, pinLabels[i], x + 9, y + i * spacing, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + } + + // NEO-6M metal shield can (center of board, strictly to the right of pin labels) + double icShieldX = boardX + 46.0; + double icShieldW = 42.0; + double icShieldH = 36.0; + double icShieldY = boardY + (boardH - icShieldH) / 2.0 - 8.0; + + g2d.setColor(SHIELD_COLOR); + g2d.fill(new RoundRectangle2D.Double(icShieldX, icShieldY, icShieldW, icShieldH, 4, 4)); + g2d.setColor(SHIELD_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(icShieldX, icShieldY, icShieldW, icShieldH, 4, 4)); + + g2d.setColor(Color.decode("#333333")); + g2d.setFont(new Font("SansSerif", Font.BOLD, 8)); + StringUtils.drawCenteredText(g2d, "NEO-6M", icShieldX + icShieldW / 2.0, icShieldY + icShieldH / 2.0 - 4, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setFont(new Font("SansSerif", Font.PLAIN, 6)); + StringUtils.drawCenteredText(g2d, "GPS", icShieldX + icShieldW / 2.0, icShieldY + icShieldH / 2.0 + 5, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Rechargeable backup coin cell battery (CR1220 / MS621) below shield + double battD = 18.0; + double battX = boardX + 58.0; + double battY = boardY + boardH - battD - 7.0; + g2d.setColor(BATTERY_COLOR); + g2d.fill(new Ellipse2D.Double(battX, battY, battD, battD)); + g2d.setColor(BATTERY_COLOR.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new Ellipse2D.Double(battX, battY, battD, battD)); + + // Ceramic patch antenna on right side (20x20mm) + double antSize = new Size(18.0d, SizeUnit.mm).convertToPixels(); + double antX = boardX + boardW - antSize - 8.0; + double antY = boardY + (boardH - antSize) / 2.0; + + g2d.setColor(ANTENNA_BEIGE); + g2d.fill(new RoundRectangle2D.Double(antX, antY, antSize, antSize, 2, 2)); + g2d.setColor(ANTENNA_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.2f)); + g2d.draw(new RoundRectangle2D.Double(antX, antY, antSize, antSize, 2, 2)); + + // Center silver electrode dot on antenna + double antCX = antX + antSize / 2.0; + double antCY = antY + antSize / 2.0; + g2d.setColor(Color.decode("#7F8C8D")); + g2d.fill(new Ellipse2D.Double(antCX - 2, antCY - 2, 4, 4)); + + // Status LED (top left area) + g2d.setColor(Color.RED); + g2d.fill(new Ellipse2D.Double(boardX + 24.0, boardY + 8.0, 5, 5)); + } + + g2d.setTransform(oldTx); + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + + // Beige ceramic patch antenna (right half) + int antS = height - 12; + g2d.setColor(ANTENNA_BEIGE); + g2d.fillRect(width - antS - 3, (height - antS) / 2, antS, antS); + g2d.setColor(ANTENNA_BORDER); + g2d.drawRect(width - antS - 3, (height - antS) / 2, antS, antS); + + // Shield + g2d.setColor(SHIELD_COLOR); + g2d.fillRoundRect(4, (height - 10) / 2, 10, 10, 2, 2); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/HC05Bluetooth.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/HC05Bluetooth.java new file mode 100644 index 000000000..223012fb1 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/HC05Bluetooth.java @@ -0,0 +1,205 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Bluetooth Module (HC-05 / HC-06)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "HC-05 / HC-06 Serial Bluetooth SPP Module Breakout", +// instanceNamePrefix = "BT", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class HC05Bluetooth extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color BT_BLUE = Color.decode("#1B4F72"); + public static Color DAUGHTER_BOARD = Color.decode("#154360"); + + public static Size BOARD_WIDTH = new Size(37.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(16.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"STATE", "RXD", "TXD", "GND", "VCC", "EN"}; + + public HC05Bluetooth() { + super(); + this.bodyColor = BT_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 6-pin 0.1" header: STATE, RXD, TXD, GND, VCC, EN + double[][] relativeOffsets = new double[6][2]; + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 5 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 5 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Bluetooth Daughterboard (HC-05 Core Module) + double dbX = boardX + 15; + double dbY = boardY + 12; + double dbW = boardW - 55; + double dbH = boardH - 24; + + g2d.setColor(DAUGHTER_BOARD); + g2d.fill(new RoundRectangle2D.Double(dbX, dbY, dbW, dbH, 3, 3)); + g2d.setColor(Color.decode("#2E86C1")); + g2d.draw(new RoundRectangle2D.Double(dbX, dbY, dbW, dbH, 3, 3)); + + // Metal RF Shield on daughterboard + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fill(new RoundRectangle2D.Double(dbX + 15, dbY + 10, dbW - 40, dbH - 20, 3, 3)); + g2d.setColor(METAL_SHIELD_BORDER); + g2d.draw(new RoundRectangle2D.Double(dbX + 15, dbY + 10, dbW - 40, dbH - 20, 3, 3)); + g2d.setColor(Color.decode("#555555")); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "HC-05", dbX + 15 + (dbW - 40) / 2.0, dbY + dbH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Gold PCB Antenna on left tip of daughterboard + g2d.setColor(Color.decode("#D4AC0D")); + g2d.fillRect((int)(dbX + 4), (int)(dbY + 8), 8, (int)(dbH - 16)); + + // Tactile Key/EN Push Button (Top Right) + g2d.setColor(Color.LIGHT_GRAY); + g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 35, boardY + 10, 14, 14, 2, 2)); + g2d.setColor(Color.DARK_GRAY); + g2d.fill(new Ellipse2D.Double(boardX + boardW - 32, boardY + 13, 8, 8)); + + // Status LED (Blue, Bottom Right) + g2d.setColor(Color.CYAN); + g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 34, boardY + boardH - 24, 12, 10, 2, 2)); + + // Silk Screen Pin Labels + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + String[] labels = new String[] { "STATE", "RXD", "TXD", "GND", "VCC", "EN" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 6; i++) { + StringUtils.drawCenteredText(g2d, labels[i], boardX + boardW - 24, y + i * spacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw 6 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(BT_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fillRect(6, height / 2 - 4, 10, 8); + g2d.setColor(Color.CYAN); + g2d.fill(new Ellipse2D.Double(width - 10, height / 2 - 3, 6, 6)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java new file mode 100644 index 000000000..4e0bb7442 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/INA219CurrentSensor.java @@ -0,0 +1,318 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Current/Power Sensor (INA219)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "INA219 I2C High-Side Current and Power Sensor Breakout", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class INA219CurrentSensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#115E1B"); + public static Color TERMINAL_BLUE = Color.decode("#1565C0"); + public static Color TERMINAL_BLUE_BORDER = Color.decode("#0D47A1"); + public static Color SHUNT_BODY = Color.decode("#7D6608"); + public static Color SHUNT_METAL = Color.decode("#D4AC0D"); + public static Color SHUNT_CORE = Color.decode("#2C3E50"); + public static Color SILK_YELLOW = Color.decode("#F4D03F"); + + public static Size BOARD_WIDTH = new Size(25.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(25.0d, SizeUnit.mm); + + // 6 header pins at bottom (0-5) + 2 screw terminal pins at top (6-7) + private static final String[] PIN_NAMES = { + "VCC", "GND", "SCL", "SDA", "VIN-", "VIN+", + "VIN- (Term)", "VIN+ (Term)" + }; + + public INA219CurrentSensor() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + + // 8 control points total: + // Pins 0-5: Bottom horizontal 6-pin header (VCC, GND, SCL, SDA, VIN-, VIN+) + // Pins 6-7: Top 2-pin screw terminal block (VIN-, VIN+) + double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels(); + double termCenterX = 2.5 * spacing; + double termY = -boardH + 24.0; + + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { spacing * 2, 0 }, + { spacing * 3, 0 }, + { spacing * 4, 0 }, + { spacing * 5, 0 }, + { termCenterX - termSpacing / 2.0, termY }, + { termCenterX + termSpacing / 2.0, termY } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double spacing = PIN_SPACING.convertToPixels(); + double boardX = x - (boardW - 5 * spacing) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double spacing = PIN_SPACING.convertToPixels(); + double boardX = x - (boardW - 5 * spacing) / 2.0; + double boardY = y - boardH + 12; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Large Gold Ring Mounting Holes in corners (20mm x 20mm pitch) + double holeMargin = 10.0; + MakerBoardPainter.drawMountingHole(g2d, boardX + holeMargin, boardY + holeMargin, 13.0); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + holeMargin, 13.0); + MakerBoardPainter.drawMountingHole(g2d, boardX + holeMargin, boardY + boardH - holeMargin, 13.0); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - holeMargin, boardY + boardH - holeMargin, 13.0); + + // Top Blue Screw Terminal Block (5.08mm 2-pin block) + double termW = 44.0; + double termH = 30.0; + double termX = boardX + (boardW - termW) / 2.0; + double termY = boardY + 2.0; + + // Blue outer block body + g2d.setColor(TERMINAL_BLUE); + g2d.fill(new RoundRectangle2D.Double(termX, termY, termW, termH, 3, 3)); + g2d.setColor(TERMINAL_BLUE_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(termX, termY, termW, termH, 3, 3)); + + // Terminal block wire entry slots on top + g2d.setColor(Color.decode("#0D47A1")); + g2d.fillRect((int) (termX + 4), (int) (termY + 1), (int) (termW - 8), 4); + + // Screw contacts + double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels(); + double screw1X = termX + termW / 2.0 - termSpacing / 2.0; + double screw2X = termX + termW / 2.0 + termSpacing / 2.0; + double screwY = termY + termH / 2.0 + 2.0; + double screwD = 14.0; + + g2d.setColor(SCREW_CIRCLE_COLOR); + g2d.fill(new Ellipse2D.Double(screw1X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.fill(new Ellipse2D.Double(screw2X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.setColor(SCREW_CIRCLE_COLOR.darker()); + g2d.draw(new Ellipse2D.Double(screw1X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.draw(new Ellipse2D.Double(screw2X - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + // Screw diagonal slots + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.drawLine((int) (screw1X - 4), (int) (screwY - 4), (int) (screw1X + 4), (int) (screwY + 4)); + g2d.drawLine((int) (screw2X - 4), (int) (screwY - 4), (int) (screw2X + 4), (int) (screwY + 4)); + + // Shunt Resistor (Center-Upper, directly below terminal block) + double shuntW = 32.0; + double shuntH = 14.0; + double shuntX = boardX + (boardW - shuntW) / 2.0; + double shuntY = boardY + termH + 6.0; + + // Copper outer tabs + g2d.setColor(Color.decode("#C0392B")); + g2d.fill(new RoundRectangle2D.Double(shuntX, shuntY, 7, shuntH, 2, 2)); + g2d.fill(new RoundRectangle2D.Double(shuntX + shuntW - 7, shuntY, 7, shuntH, 2, 2)); + + // Metal sense element (arched center) + g2d.setColor(Color.decode("#BDC3C7")); + g2d.fill(new Rectangle2D.Double(shuntX + 6, shuntY + 1, shuntW - 12, shuntH - 2)); + g2d.setColor(Color.decode("#7F8C8D")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new Rectangle2D.Double(shuntX + 6, shuntY + 1, shuntW - 12, shuntH - 2)); + + // "R100" label on shunt + g2d.setColor(Color.decode("#2C3E50")); + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + StringUtils.drawCenteredText(g2d, "R100", shuntX + shuntW / 2.0, shuntY + shuntH / 2.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // INA219 SOIC-8 IC (Center-Lower, directly below shunt) + double icW = 20.0; + double icH = 14.0; + double icX = boardX + (boardW - icW) / 2.0; + double icY = shuntY + shuntH + 4.0; + + // IC body + g2d.setColor(Color.decode("#1C2833")); + g2d.fill(new RoundRectangle2D.Double(icX, icY, icW, icH, 2, 2)); + g2d.setColor(Color.decode("#111111")); + g2d.draw(new RoundRectangle2D.Double(icX, icY, icW, icH, 2, 2)); + + // 8 silver IC pins (4 top, 4 bottom) + g2d.setColor(Color.decode("#BDC3C7")); + for (int i = 0; i < 4; i++) { + double px = icX + 2.5 + i * 4.5; + g2d.fillRect((int) px, (int) (icY - 2), 2, 2); + g2d.fillRect((int) px, (int) (icY + icH), 2, 2); + } + // Pin 1 dot + g2d.setColor(Color.decode("#F4D03F")); + g2d.fill(new Ellipse2D.Double(icX + 2.5, icY + 2.5, 2.5, 2.5)); + + // Vertical silkscreen text on left edge + AffineTransform preTextTx = g2d.getTransform(); + g2d.translate(boardX + 14, boardY + boardH / 2.0); + g2d.rotate(-Math.PI / 2.0); + g2d.setColor(SILK_YELLOW); + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + StringUtils.drawCenteredText(g2d, "INA219 SENSOR", 0, 0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setTransform(preTextTx); + + // Vertical silkscreen labels above bottom 6 header pins (VCC, GND, SCL, SDA, VIN-, VIN+) + String[] headerLabels = new String[] {"VCC", "GND", "SCL", "SDA", "VIN-", "VIN+"}; + g2d.setFont(new Font("SansSerif", Font.BOLD, 7)); + g2d.setColor(SILK_YELLOW); + + for (int i = 0; i < 6; i++) { + double pinX = x + i * spacing; + AffineTransform pTx = g2d.getTransform(); + g2d.translate(pinX, y - 11); + g2d.rotate(-Math.PI / 2.0); + StringUtils.drawCenteredText(g2d, headerLabels[i], 0, 0, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + g2d.setTransform(pTx); + } + } + + g2d.setTransform(oldTx); + + // Draw bottom 6 header pins + drawPinHeader(g2d, 0, 6, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green square PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + + // Blue terminal block at top + g2d.setColor(TERMINAL_BLUE); + g2d.fillRoundRect(width / 2 - 8, 3, 16, 8, 2, 2); + + // Shunt in middle + g2d.setColor(Color.decode("#C0392B")); + g2d.fillRect(width / 2 - 6, 13, 12, 4); + g2d.setColor(Color.decode("#BDC3C7")); + g2d.fillRect(width / 2 - 3, 13, 6, 4); + + // Small IC below shunt + g2d.setColor(Color.decode("#1C2833")); + g2d.fillRect(width / 2 - 4, 19, 8, 5); + + // 6 pin dots at bottom + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 6; i++) { + g2d.fillRect(4 + i * 4, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/L298NMotorDriver.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/L298NMotorDriver.java new file mode 100644 index 000000000..1981dbeb3 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/L298NMotorDriver.java @@ -0,0 +1,240 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Motor Driver (L298N)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "L298N Dual H-Bridge DC and Stepper Motor Driver Module with Heatsink", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class L298NMotorDriver extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color L298_RED = Color.decode("#B71C1C"); + public static Color HEATSINK_COLOR = Color.decode("#263238"); + public static Size BOARD_SIZE = new Size(43.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + // Power Terminal (0..2) + "12V (VCC)", "GND", "5V (Logic)", + // Motor A Terminal (3..4) + "OUT1", "OUT2", + // Motor B Terminal (5..6) + "OUT3", "OUT4", + // Logic Control Header (7..12) + "ENA", "IN1", "IN2", "IN3", "IN4", "ENB" + }; + + public L298NMotorDriver() { + super(); + this.bodyColor = L298_RED; + 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 termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels(); // 40px + double pinSpacing = PIN_SPACING.convertToPixels(); // 20px + double boardSizePx = BOARD_SIZE.convertToPixels(); + double boardX = -45; + + double[][] relativeOffsets = new double[][] { + // Power screw terminal (0..2) at bottom-left: 12V, GND, 5V + { 0, 0 }, + { termSpacing, 0 }, + { termSpacing * 2, 0 }, + // Motor A terminal (3..4) on left edge: OUT1, OUT2 + { boardX + 22.5, -140 }, + { boardX + 22.5, -180 }, + // Motor B terminal (5..6) on right edge: OUT3, OUT4 + { boardX + boardSizePx - 22.5, -140 }, + { boardX + boardSizePx - 22.5, -180 }, + // Logic header (7..12) at bottom-right: ENA, IN1, IN2, IN3, IN4, ENB + { 140, 0 }, + { 140 + pinSpacing, 0 }, + { 140 + pinSpacing * 2, 0 }, + { 140 + pinSpacing * 3, 0 }, + { 140 + pinSpacing * 4, 0 }, + { 140 + pinSpacing * 5, 0 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardSizePx = BOARD_SIZE.convertToPixels(); + double boardX = x - 45; + double boardY = y - boardSizePx + 25; + return new RoundRectangle2D.Double(boardX, boardY, boardSizePx, boardSizePx, 10, 10); + } + + @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 boardSizePx = BOARD_SIZE.convertToPixels(); + double boardX = x - 45; + double boardY = y - boardSizePx + 25; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + 20, boardY + boardSizePx - 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 20, boardY + 20, 20); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardSizePx - 20, boardY + boardSizePx - 20, 20); + + // Large black aluminum heatsink in center + double hsW = boardSizePx - 100; + double hsH = 90; + double hsX = boardX + (boardSizePx - hsW) / 2.0; + double hsY = boardY + 50; + + g2d.setColor(HEATSINK_COLOR); + g2d.fill(new RoundRectangle2D.Double(hsX, hsY, hsW, hsH, 4, 4)); + + // Heatsink fins + g2d.setColor(Color.decode("#455A64")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f)); + for (double fx = hsX + 15; fx < hsX + hsW - 10; fx += 18) { + g2d.drawLine((int) fx, (int) hsY + 5, (int) fx, (int) (hsY + hsH - 5)); + } + + // L298 Multiwatt IC mounted to heatsink + MakerBoardPainter.drawChip(g2d, hsX + (hsW - 100) / 2.0, hsY + hsH - 15, 100, 30, "L298N"); + + // Logic header black block + g2d.setColor(HEADER_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(x + 130, y - 10, 128, 20, 2, 2)); + + // Silkscreen + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "L298N DRIVER", boardX + boardSizePx / 2.0, boardY + 30, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Terminal Silk labels + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "12V GND 5V", x + 40, y - 32, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT1/2", boardX + 50, y - 160, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT3/4", boardX + boardSizePx - 50, y - 160, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + // Draw terminal blocks matching PCBTerminalBlock style + drawTerminalBlock(g2d, 0, 3, true, -1, 38.0, outlineMode, drawingObserver); + drawTerminalBlock(g2d, 3, 2, false, -1, 38.0, outlineMode, drawingObserver); + drawTerminalBlock(g2d, 5, 2, false, 1, 38.0, outlineMode, drawingObserver); + + // Draw logic header pins + drawPinHeader(g2d, 7, 6, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(L298_RED); + g2d.fill(new RoundRectangle2D.Double(3, 3, width - 6, height - 6, 3, 3)); + g2d.setColor(L298_RED.darker()); + g2d.draw(new RoundRectangle2D.Double(3, 3, width - 6, height - 6, 3, 3)); + + // Heatsink + g2d.setColor(HEATSINK_COLOR); + g2d.fillRect(7, 8, width - 14, 10); + + // Terminals + g2d.setColor(SCREW_TERMINAL_COLOR); + g2d.fillRect(5, height - 8, 10, 5); + g2d.fillRect(width - 15, height - 8, 10, 5); + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "L298", width / 2, height / 2 + 5, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/LM2596BuckConverter.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/LM2596BuckConverter.java new file mode 100644 index 000000000..e687e7171 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/LM2596BuckConverter.java @@ -0,0 +1,227 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Buck Converter (LM2596)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "LM2596 Adjustable DC-DC Step-Down Buck Converter Module", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class LM2596BuckConverter extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color BUCK_BLUE = Color.decode("#1F618D"); + public static Color POT_BLUE = Color.decode("#0055AA"); + public static Color COPPER_COIL = Color.decode("#D35400"); + public static Color IC_TAB_BODY = Color.decode("#333333"); + public static Color IC_TAB_METAL = Color.decode("#CCCCCC"); + public static Color INDUCTOR_CORE = Color.decode("#222222"); + public static Color POT_SCREW_BRASS = Color.decode("#D4AC0D"); + + public static Size BOARD_WIDTH = new Size(43.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(21.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"IN+", "IN-", "OUT+", "OUT-"}; + + public LM2596BuckConverter() { + super(); + this.bodyColor = BUCK_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + private static final Size TERMINAL_PITCH = new Size(5.08d, SizeUnit.mm); + private static final double PAD_MARGIN_X = 16.0; + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double termSpacing = TERMINAL_PITCH.convertToPixels(); + double w = BOARD_WIDTH.convertToPixels(); + + // 4 Solder / Screw terminal pads (5.08mm pitch, centered vertically on left and right edges): + // Input (Left): 0 = IN+, 1 = IN- + // Output (Right): 2 = OUT+, 3 = OUT- + double[][] relativeOffsets = new double[][] { + { 0, 0 }, // Pin 0: IN+ + { 0, termSpacing }, // Pin 1: IN- + { w - 2 * PAD_MARGIN_X, 0 }, // Pin 2: OUT+ + { w - 2 * PAD_MARGIN_X, termSpacing } // Pin 3: OUT- + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double termSpacing = TERMINAL_PITCH.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - (boardH - termSpacing) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double termSpacing = TERMINAL_PITCH.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - (boardH - termSpacing) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 2 Diagonal Mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 50, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 50, boardY + boardH - 16, 16); + + // Input Capacitor (Left) + g2d.setColor(Color.LIGHT_GRAY); + g2d.fill(new Ellipse2D.Double(boardX + 38, boardY + boardH / 2.0 - 18, 34, 34)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new Ellipse2D.Double(boardX + 38, boardY + boardH / 2.0 - 18, 34, 34)); + + // Output Capacitor (Right) + g2d.setColor(Color.LIGHT_GRAY); + g2d.fill(new Ellipse2D.Double(boardX + boardW - 72, boardY + boardH / 2.0 - 18, 34, 34)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new Ellipse2D.Double(boardX + boardW - 72, boardY + boardH / 2.0 - 18, 34, 34)); + + // LM2596 IC (Center Left) with tab + g2d.setColor(IC_TAB_BODY); + g2d.fill(new RoundRectangle2D.Double(boardX + 80, boardY + 16, 38, 48, 3, 3)); + g2d.setColor(IC_TAB_METAL); + g2d.fill(new RoundRectangle2D.Double(boardX + 80, boardY + 12, 38, 10, 2, 2)); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "LM2596", boardX + 99, boardY + 38, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Toroidal Power Inductor (Center Right) + g2d.setColor(INDUCTOR_CORE); + g2d.fill(new Ellipse2D.Double(boardX + 130, boardY + boardH / 2.0 - 24, 48, 48)); + g2d.setColor(COPPER_COIL); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3)); + g2d.draw(new Ellipse2D.Double(boardX + 134, boardY + boardH / 2.0 - 20, 40, 40)); + + // Blue 3296W Trimmer Potentiometer (Top Center) + g2d.setColor(POT_BLUE); + g2d.fill(new RoundRectangle2D.Double(boardX + 190, boardY + 16, 45, 26, 3, 3)); + // Brass screw head + g2d.setColor(POT_SCREW_BRASS); + g2d.fill(new Ellipse2D.Double(boardX + 195, boardY + 23, 12, 12)); + g2d.setColor(Color.BLACK); + g2d.drawLine((int)(boardX + 197), (int)(boardY + 29), (int)(boardX + 205), (int)(boardY + 29)); + + // Silk Screen Text + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "IN+", boardX + 38, y - 8, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "IN-", boardX + 38, y + termSpacing + 8, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT+", boardX + boardW - 38, y - 8, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT-", boardX + boardW - 38, y + termSpacing + 8, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + // Draw Input and Output terminal blocks matching PCBTerminalBlock style + drawTerminalBlock(g2d, 0, 2, false, -1, 35.0, outlineMode, drawingObserver); + drawTerminalBlock(g2d, 2, 2, false, 1, 35.0, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(BUCK_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(COPPER_COIL); + g2d.fill(new Ellipse2D.Double(width / 2 - 4, height / 2 - 4, 8, 8)); + g2d.setColor(POT_BLUE); + g2d.fillRect(width - 12, height / 2 - 5, 8, 10); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/LogicLevelConverter.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/LogicLevelConverter.java new file mode 100644 index 000000000..d502044be --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/LogicLevelConverter.java @@ -0,0 +1,190 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Logic Level Converter (3.3V-5V)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "4-Channel Bi-Directional Logic Level Shifter Module", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class LogicLevelConverter extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SHIFTER_RED = Color.decode("#C0392B"); + public static Size BOARD_WIDTH = new Size(13.0d, SizeUnit.mm); + public static Size BOARD_LENGTH = new Size(16.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + // Low Voltage Side (0..5) + "LV1", "LV2", "LV (3.3V)", "GND_LV", "LV3", "LV4", + // High Voltage Side (6..11) + "HV1", "HV2", "HV (5V)", "GND_HV", "HV3", "HV4" + }; + + public LogicLevelConverter() { + super(); + this.bodyColor = SHIFTER_RED; + 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 = new Size(0.5d, SizeUnit.in).convertToPixels(); // 100px + + double[][] relativeOffsets = new double[12][2]; + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + for (int i = 0; i < 6; i++) { + relativeOffsets[6 + i][0] = rowSpacing; + relativeOffsets[6 + 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 = new Size(0.5d, SizeUnit.in).convertToPixels(); + double boardW = rowSpacing + 20; + double boardH = 7 * PIN_SPACING.convertToPixels(); + double boardX = x - 10; + double boardY = y - 10; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double rowSpacing = new Size(0.5d, SizeUnit.in).convertToPixels(); + double boardW = rowSpacing + 20; + double boardH = 7 * PIN_SPACING.convertToPixels(); + double boardX = x - 10; + double boardY = y - 10; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 tiny SOT-23 MOSFETs + for (int i = 0; i < 4; i++) { + double my = boardY + 25 + i * 25; + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(boardX + boardW / 2.0 - 10, my, 20, 14, 2, 2)); + } + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "LV", x + 15, y - 2, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "HV", x + rowSpacing - 15, y - 2, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SHIFTER_RED); + g2d.fill(new RoundRectangle2D.Double(6, 4, width - 12, height - 8, 3, 3)); + g2d.setColor(SHIFTER_RED.darker()); + g2d.draw(new RoundRectangle2D.Double(6, 4, width - 12, height - 8, 3, 3)); + + // Pins on left & right + g2d.setColor(PIN_COLOR); + for (int y = 7; y < height - 7; y += 4) { + g2d.fillRect(7, y, 2, 2); + g2d.fillRect(width - 9, y, 2, 2); + } + + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 5)); + StringUtils.drawCenteredText(g2d, "LVL", width / 2, height / 2 + 2, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/MB102PowerSupply.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/MB102PowerSupply.java new file mode 100644 index 000000000..4da62db99 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/MB102PowerSupply.java @@ -0,0 +1,224 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Breadboard Power Supply (MB102)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "MB102 Dual 3.3V/5V Breadboard Power Supply Module", +// instanceNamePrefix = "PWR", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class MB102PowerSupply extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color MB102_PCB = Color.decode("#E8E4D9"); + public static Color JUMPER_YELLOW = Color.decode("#F1C40F"); + public static Color SWITCH_RED = Color.decode("#E74C3C"); + public static Color JACK_BODY_COLOR = Color.decode("#111111"); + public static Color JACK_BORDER_COLOR = Color.decode("#333333"); + public static Color POWER_LED_COLOR = Color.decode("#2ECC71"); + + public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(54.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = { + "V+ (Top)", "GND (Top)", "V+ (Top)", "GND (Top)", + "V+ (Bottom)", "GND (Bottom)", "V+ (Bottom)", "GND (Bottom)" + }; + + public MB102PowerSupply() { + super(); + this.bodyColor = MB102_PCB; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 8 output pins to plug directly into breadboard power rails: + // Top Power Rail (4 pins: 0..3): +, -, +, - + // Bottom Power Rail (4 pins: 4..7): +, -, +, - spaced 18 grid units apart + double railSpan = 18 * spacing; + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { 0, spacing }, + { spacing, spacing }, + { 0, railSpan }, + { spacing, railSpan }, + { 0, railSpan + spacing }, + { spacing, railSpan + spacing } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + PIN_SPACING.convertToPixels() + 25; + double boardY = y - 20; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + PIN_SPACING.convertToPixels() + 25; + double boardY = y - 20; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // DC Barrel Jack (Left Edge Center) + g2d.setColor(JACK_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(boardX - 8, boardY + boardH / 2.0 - 25, 45, 50, 4, 4)); + g2d.setColor(JACK_BORDER_COLOR); + g2d.draw(new RoundRectangle2D.Double(boardX - 8, boardY + boardH / 2.0 - 25, 45, 50, 4, 4)); + g2d.setColor(Color.LIGHT_GRAY); + g2d.fill(new Ellipse2D.Double(boardX - 4, boardY + boardH / 2.0 - 10, 20, 20)); + g2d.setColor(Color.BLACK); + g2d.fill(new Ellipse2D.Double(boardX + 1, boardY + boardH / 2.0 - 5, 10, 10)); + + // USB-A Connector (Center) + MakerBoardPainter.drawUsbA(g2d, boardX + 45, boardY + boardH / 2.0 - 22, 48, 44, "USB"); + + // Push Button Latch Switch (Left-ish) + g2d.setColor(Color.LIGHT_GRAY); + g2d.fill(new RoundRectangle2D.Double(boardX + 48, boardY + 45, 28, 28, 4, 4)); + g2d.setColor(SWITCH_RED); + g2d.fill(new Ellipse2D.Double(boardX + 53, boardY + 50, 18, 18)); + + // Power Green LED + g2d.setColor(POWER_LED_COLOR); + g2d.fill(new Ellipse2D.Double(boardX + 85, boardY + 54, 10, 10)); + + // AMS1117 Voltage Regulators (3.3V and 5.0V) + MakerBoardPainter.drawChip(g2d, boardX + 110, boardY + 70, 28, 22, "3.3V"); + MakerBoardPainter.drawChip(g2d, boardX + 110, boardY + boardH - 92, 28, 22, "5.0V"); + + // Voltage Selection Jumpers (Top & Bottom rail selectors) + g2d.setColor(JUMPER_YELLOW); + g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 75, boardY + 20, 26, 18, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 75, boardY + boardH - 38, 26, 18, 3, 3)); + + // Silk Screen Text + g2d.setColor(Color.DARK_GRAY); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "MB102", boardX + boardW / 2.0, boardY + boardH - 60, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "3.3V / 5V", boardX + boardW / 2.0, boardY + boardH - 48, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Rail markings (+ / -) + g2d.setColor(Color.RED); + g2d.drawString("+", (int)(x - 12), (int)(y + 8)); + g2d.drawString("+", (int)(x - 12), (int)(y + 18 * PIN_SPACING.convertToPixels() + 8)); + g2d.setColor(Color.BLUE); + g2d.drawString("-", (int)(x + PIN_SPACING.convertToPixels() + 6), (int)(y + 8)); + g2d.drawString("-", (int)(x + PIN_SPACING.convertToPixels() + 6), (int)(y + 18 * PIN_SPACING.convertToPixels() + 8)); + } + + g2d.setTransform(oldTx); + + // Draw header pins connecting to breadboard rails + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(MB102_PCB); + g2d.fill(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3)); + g2d.setColor(SWITCH_RED); + g2d.fill(new Ellipse2D.Double(width / 2.0 - 4, height / 2.0 - 4, 8, 8)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new RoundRectangle2D.Double(4, 2, width - 8, height - 4, 3, 3)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/MOSFETSwitchModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/MOSFETSwitchModule.java new file mode 100644 index 000000000..3889ced57 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/MOSFETSwitchModule.java @@ -0,0 +1,215 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "MOSFET Driver Module (IRF520)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "IRF520 High-Current DC MOSFET Power Switch Driver Module", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class MOSFETSwitchModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color MOSFET_RED = Color.decode("#922B21"); + + public static Size BOARD_WIDTH = new Size(33.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(24.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"SIG", "VCC", "GND", "VIN", "GND (Power)", "V+", "V-"}; + + public MOSFETSwitchModule() { + super(); + this.bodyColor = MOSFET_RED; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + private static final Size TERMINAL_PITCH = new Size(5.08d, SizeUnit.mm); + private static final double PAD_MARGIN_X = 15.0; + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double termSpacing = TERMINAL_PITCH.convertToPixels(); + double w = BOARD_WIDTH.convertToPixels(); + double h = BOARD_HEIGHT.convertToPixels(); + + // 7 pins total: + // Pin 0..2: Control header (SIG, VCC, GND on left, 2.54mm pitch) + // Pin 3, 4: VIN, GND (Power in screw terminal, top right, 5.08mm pitch) + // Pin 5, 6: V+, V- (Load out screw terminal, bottom right, 5.08mm pitch) + double rightX = w - PAD_MARGIN_X - 18.0; + double topTermY = 18.0; + double btmTermY = h - 18.0 - termSpacing; + + double[][] relativeOffsets = new double[][] { + { 0, 0 }, // Pin 0: SIG + { 0, spacing }, // Pin 1: VCC + { 0, spacing * 2 }, // Pin 2: GND + { rightX, topTermY - (h - 2 * spacing) / 2.0 }, // Pin 3: VIN + { rightX, topTermY + termSpacing - (h - 2 * spacing) / 2.0 }, // Pin 4: GND (Power) + { rightX, btmTermY - (h - 2 * spacing) / 2.0 }, // Pin 5: V+ + { rightX, btmTermY + termSpacing - (h - 2 * spacing) / 2.0 } // Pin 6: V- + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - (boardH - 2 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - (boardH - 2 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 2 Mounting Holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + 14, 14); + MakerBoardPainter.drawMountingHole(g2d, boardX + 14, boardY + boardH - 14, 14); + + // TO-220 Power MOSFET (Center) + double mosX = boardX + 50; + double mosY = boardY + boardH / 2.0 - 24; + g2d.setColor(Color.decode("#222222")); + g2d.fill(new RoundRectangle2D.Double(mosX, mosY, 36, 48, 3, 3)); + // Heatsink tab + g2d.setColor(Color.decode("#BDC3C7")); + g2d.fill(new RoundRectangle2D.Double(mosX, mosY - 10, 36, 12, 2, 2)); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "IRF520", mosX + 18, mosY + 24, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Gate Trigger Status LED (Red) + g2d.setColor(Color.RED); + g2d.fill(new RoundRectangle2D.Double(boardX + 32, boardY + boardH / 2.0 - 5, 10, 10, 2, 2)); + + // Silk Screen Pin Labels + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + String[] labels = new String[] { "SIG", "VCC", "GND" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 3; i++) { + StringUtils.drawCenteredText(g2d, labels[i], x + 16, y + i * spacing, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + } + double termX = boardX + boardW - 42; + StringUtils.drawCenteredText(g2d, "VIN", termX - 6, boardY + 36, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT", termX - 6, boardY + boardH - 36, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + // Draw control header pins on left + drawPinHeader(g2d, 0, 3, outlineMode, drawingObserver); + + // Draw dual terminal blocks on right matching PCBTerminalBlock style + drawTerminalBlock(g2d, 3, 2, false, 1, 38.0, outlineMode, drawingObserver); + drawTerminalBlock(g2d, 5, 2, false, 1, 38.0, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(MOSFET_RED); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(Color.decode("#222222")); + g2d.fillRect(width / 2 - 4, height / 2 - 6, 8, 12); + g2d.setColor(SCREW_TERMINAL_COLOR); + g2d.fillRect(width - 10, height / 2 - 6, 8, 12); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/MT3608BoostConverter.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/MT3608BoostConverter.java new file mode 100644 index 000000000..a8f52f741 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/MT3608BoostConverter.java @@ -0,0 +1,233 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Boost Converter (MT3608)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "MT3608 2A Adjustable DC-DC Step-Up Boost Converter Module", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class MT3608BoostConverter extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color BOOST_BLUE = Color.decode("#1B4F72"); + public static Color POT_BLUE = Color.decode("#0055AA"); + public static Color INDUCTOR_BODY_COLOR = Color.decode("#2C3E50"); + public static Color INDUCTOR_CORE_COLOR = Color.decode("#1A252F"); + public static Color POT_SCREW_BRASS = Color.decode("#D4AC0D"); + + public static Font PAD_LABEL_FONT = new Font("SansSerif", Font.BOLD, 9); + + public static Size BOARD_WIDTH = new Size(36.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(17.0d, SizeUnit.mm); + + private static final double PAD_MARGIN_X = 22.0; + private static final double PAD_MARGIN_Y = 24.0; + + // Pinout matches physical MT3608 board: + // Left: Output (0 = VOUT+, 1 = VOUT-) + // Right: Input (2 = VIN+, 3 = VIN-) + private static final String[] PIN_NAMES = {"VOUT+", "VOUT-", "VIN+", "VIN-"}; + + public MT3608BoostConverter() { + super(); + this.bodyColor = BOOST_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double h = BOARD_HEIGHT.convertToPixels(); + double w = BOARD_WIDTH.convertToPixels(); + + // 4 Solder pads with realistic vertical spacing: + // Output (Left): 0 = VOUT+ (Top-Left), 1 = VOUT- (Bottom-Left) + // Input (Right): 2 = VIN+ (Top-Right), 3 = VIN- (Bottom-Right) + double[][] relativeOffsets = new double[][] { + { 0, 0 }, // Pin 0: VOUT+ + { 0, h - 2 * PAD_MARGIN_Y }, // Pin 1: VOUT- + { w - 2 * PAD_MARGIN_X, 0 }, // Pin 2: VIN+ + { w - 2 * PAD_MARGIN_X, h - 2 * PAD_MARGIN_Y } // Pin 3: VIN- + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - PAD_MARGIN_Y; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - PAD_MARGIN_Y; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Blue 3296W Trimmer Potentiometer (Left side) + double potX = boardX + 44.0; + double potY = boardY + 44.0; + double potW = 48.0; + double potH = 46.0; + g2d.setColor(POT_BLUE); + g2d.fill(new RoundRectangle2D.Double(potX, potY, potW, potH, 3, 3)); + g2d.setColor(POT_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(potX, potY, potW, potH, 3, 3)); + + // Brass screw head at bottom-left corner of the pot + double screwX = potX + 11.0; + double screwY = potY + potH - 11.0; + double screwD = 13.0; + g2d.setColor(POT_SCREW_BRASS); + g2d.fill(new Ellipse2D.Double(screwX - screwD / 2.0, screwY - screwD / 2.0, screwD, screwD)); + g2d.setColor(Color.BLACK); + g2d.drawLine((int) (screwX - 4), (int) screwY, (int) (screwX + 4), (int) screwY); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "3296", potX + potW / 2.0 + 4, potY + 18, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Power Inductor (Top-Right / Center, marked 220) + double indX = boardX + 115.0; + double indY = boardY + 12.0; + double indSize = 54.0; + g2d.setColor(INDUCTOR_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(indX, indY, indSize, indSize, 6, 6)); + g2d.setColor(INDUCTOR_CORE_COLOR); + g2d.fill(new Ellipse2D.Double(indX + 5, indY + 5, indSize - 10, indSize - 10)); + g2d.setColor(LIGHT_METAL_COLOR); + g2d.fillRect((int) indX, (int) (indY + 16), 4, 22); + g2d.fillRect((int) (indX + indSize - 4), (int) (indY + 16), 4, 22); + g2d.setColor(Color.LIGHT_GRAY); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "220", indX + indSize / 2.0, indY + indSize / 2.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // MT3608 SOT23-6 Boost Controller IC (Bottom-Right / Center) + MakerBoardPainter.drawChip(g2d, boardX + 150.0, boardY + 78.0, 26.0, 22.0, "3608"); + + // Silkscreen Text Labels (placed above/below pads without overlapping) + g2d.setColor(Color.WHITE); + g2d.setFont(PAD_LABEL_FONT); + StringUtils.drawCenteredText(g2d, "VOUT+", boardX + PAD_MARGIN_X, boardY + 9.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "VOUT-", boardX + PAD_MARGIN_X, boardY + boardH - 9.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "VIN+", boardX + boardW - PAD_MARGIN_X, boardY + 9.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "VIN-", boardX + boardW - PAD_MARGIN_X, boardY + boardH - 9.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + // Draw square/rounded rectangular tinned solder pads using LIGHT_METAL_COLOR + drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(BOOST_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(POT_BLUE); + g2d.fillRect(6, height / 2 - 4, 8, 8); + g2d.setColor(INDUCTOR_BODY_COLOR); + g2d.fillRect(width - 14, height / 2 - 4, 8, 8); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/NRF24L01Transceiver.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/NRF24L01Transceiver.java new file mode 100644 index 000000000..bdbded167 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/NRF24L01Transceiver.java @@ -0,0 +1,276 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "2.4GHz RF Transceiver (NRF24L01+)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "NRF24L01+ 2.4GHz Wireless SPI Transceiver Module", +// instanceNamePrefix = "RF", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class NRF24L01Transceiver extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color NRF_PCB = Color.decode("#111111"); + public static Color ANTENNA_GOLD = Color.decode("#D4AC0D"); + public static Color CRYSTAL_BODY = Color.decode("#85929E"); + public static Color CRYSTAL_BORDER = Color.decode("#BDC3C7"); + public static Color SILK_LINE_COLOR = Color.WHITE; + + public static Size BOARD_WIDTH = new Size(29.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(15.2d, SizeUnit.mm); + + // Pin offsets: pins start right at the top-left edge + private static final double LEFT_PIN_OFFSET_X = 14.0; + private static final double TOP_PIN_OFFSET_Y = 14.0; + + private static final String[] PIN_NAMES = {"GND", "VCC", "CE", "CSN", "SCK", "MOSI", "MISO", "IRQ"}; + + public NRF24L01Transceiver() { + super(); + this.bodyColor = NRF_PCB; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 4x2 Header (8 pins) — 4 rows, 2 columns positioned directly on top-left edge: + // Row 0: 0 = GND, 1 = VCC (3.3V) + // Row 1: 2 = CE, 3 = CSN + // Row 2: 4 = SCK, 5 = MOSI + // Row 3: 6 = MISO, 7 = IRQ + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { 0, spacing }, + { spacing, spacing }, + { 0, spacing * 2 }, + { spacing, spacing * 2 }, + { 0, spacing * 3 }, + { spacing, spacing * 3 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - LEFT_PIN_OFFSET_X; + double boardY = y - TOP_PIN_OFFSET_Y; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double spacing = PIN_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - LEFT_PIN_OFFSET_X; + double boardY = y - TOP_PIN_OFFSET_Y; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 2x4 Header silkscreen box on top-left edge + g2d.setColor(SILK_LINE_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.2f)); + g2d.draw(new Rectangle2D.Double(boardX + 4.0, boardY + 4.0, spacing * 2 + 3.0, spacing * 4 + 3.0)); + // Pin 1 square marker around top-left pin + g2d.draw(new Rectangle2D.Double(boardX + 4.0, boardY + 4.0, spacing + 1.0, spacing + 1.0)); + + // NRF24L01+ QFN-20 IC (Center Upper) + double icX = boardX + 88.0; + double icY = boardY + 20.0; + double icSize = 32.0; + MakerBoardPainter.drawChip(g2d, icX, icY, icSize, icSize, "nRF24"); + + // White corner silkscreen brackets around IC + g2d.setColor(SILK_LINE_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.2f)); + // Top-left bracket + g2d.drawLine((int) (icX - 3), (int) (icY + 4), (int) (icX - 3), (int) (icY - 3)); + g2d.drawLine((int) (icX - 3), (int) (icY - 3), (int) (icX + 4), (int) (icY - 3)); + // Top-right bracket + g2d.drawLine((int) (icX + icSize - 4), (int) (icY - 3), (int) (icX + icSize + 3), (int) (icY - 3)); + g2d.drawLine((int) (icX + icSize + 3), (int) (icY - 3), (int) (icX + icSize + 3), (int) (icY + 4)); + // Bottom-left bracket + g2d.drawLine((int) (icX - 3), (int) (icY + icSize - 4), (int) (icX - 3), (int) (icY + icSize + 3)); + g2d.drawLine((int) (icX - 3), (int) (icY + icSize + 3), (int) (icX + 4), (int) (icY + icSize + 3)); + // Bottom-right bracket + g2d.drawLine((int) (icX + icSize - 4), (int) (icY + icSize + 3), (int) (icX + icSize + 3), (int) (icY + icSize + 3)); + g2d.drawLine((int) (icX + icSize + 3), (int) (icY + icSize + 3), (int) (icX + icSize + 3), (int) (icY + icSize - 4)); + + // 16.000 MHz Metal Can Crystal (Bottom Center) + double xtalW = 74.0; + double xtalH = 24.0; + double xtalX = boardX + 68.0; + double xtalY = boardY + boardH - 30.0; + RoundRectangle2D xtal = new RoundRectangle2D.Double(xtalX, xtalY, xtalW, xtalH, 12, 12); + g2d.setColor(CRYSTAL_BODY); + g2d.fill(xtal); + g2d.setColor(CRYSTAL_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(xtal); + + // Exact 2.4GHz MIFA (Meandered Inverted-F Antenna) trace on right side + double antLeftX = boardX + 150.0; + double stubX = boardX + 164.0; + double rightX = boardX + boardW - 6.0; + double meanderLeftX = stubX + 12.0; + double topY = boardY + 10.0; + double bottomY = boardY + boardH - 10.0; + double feedY = topY + 16.0; + double stubBottomY = bottomY - 12.0; + + g2d.setColor(ANTENNA_GOLD); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3.5f)); + + Path2D mifa = new Path2D.Double(); + + // Continuous Meander Track: + // Starts at stubX, topY -> goes across top rail to right edge + mifa.moveTo(stubX, topY); + mifa.lineTo(rightX, topY); + + // Meander Loop 1 + mifa.lineTo(rightX, topY + 14.0); + mifa.lineTo(meanderLeftX, topY + 14.0); + mifa.lineTo(meanderLeftX, topY + 28.0); + mifa.lineTo(rightX, topY + 28.0); + + // Meander Loop 2 + mifa.lineTo(rightX, topY + 42.0); + mifa.lineTo(meanderLeftX, topY + 42.0); + mifa.lineTo(meanderLeftX, topY + 56.0); + mifa.lineTo(rightX, topY + 56.0); + + // Meander Loop 3 + mifa.lineTo(rightX, topY + 70.0); + mifa.lineTo(meanderLeftX, topY + 70.0); + mifa.lineTo(meanderLeftX, topY + 84.0); + mifa.lineTo(rightX, topY + 84.0); + + // Final drop to bottom-right corner + mifa.lineTo(rightX, bottomY); + + // Horizontal RF Feed line (from RF network into the vertical stub) + mifa.moveTo(antLeftX, feedY); + mifa.lineTo(stubX, feedY); + + // Left Vertical Ground Shunt Stub (runs from top rail down past feed line) + mifa.moveTo(stubX, topY); + mifa.lineTo(stubX, stubBottomY); + + g2d.draw(mifa); + + // Small gold ground via dot near bottom-right + g2d.fill(new Ellipse2D.Double(stubX + 20.0, bottomY - 6.0, 3.5, 3.5)); + } + + g2d.setTransform(oldTx); + + // Draw 8 header pins on top-left edge + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(NRF_PCB); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(ANTENNA_GOLD); + g2d.drawLine(width - 6, 8, width - 6, height - 8); + g2d.drawLine(width - 6, 8, width - 12, 8); + g2d.drawLine(width - 6, 14, width - 12, 14); + g2d.drawLine(width - 6, 20, width - 12, 20); + g2d.setColor(CRYSTAL_BODY); + g2d.fillRoundRect(6, height - 12, 16, 6, 3, 3); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/PCA9685ServoDriver.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/PCA9685ServoDriver.java new file mode 100644 index 000000000..9a918f644 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/PCA9685ServoDriver.java @@ -0,0 +1,426 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "16-Channel PWM/Servo Driver (PCA9685)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "PCA9685 16-Channel 12-Bit PWM I2C Servo Controller Board", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class PCA9685ServoDriver extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCA_BLUE = Color.decode("#1B4F72"); + public static Color PWM_YELLOW = Color.decode("#F1C40F"); + public static Color V_RED = Color.decode("#E74C3C"); + public static Color GND_BLACK = Color.decode("#1C1C1C"); + public static Color CAP_BODY_COLOR = Color.decode("#1C2833"); + public static Color CAP_STRIPE_COLOR = Color.decode("#BDC3C7"); + public static Color CAP_BORDER_COLOR = Color.decode("#7F8C8D"); + public static Color SMD_BODY_COLOR = Color.decode("#2C3E50"); + public static Color SMD_ARRAY_COLOR = Color.decode("#34495E"); + public static Color GOLD_PAD_COLOR = Color.decode("#D4AC0D"); + public static Color POWER_LED_COLOR = Color.decode("#2ECC71"); + + public static Font LABEL_FONT_BOLD = new Font("SansSerif", Font.BOLD, 9); + public static Font LABEL_FONT_TINY = new Font("SansSerif", Font.PLAIN, 8); + + // Real PCA9685 board dimensions: ~62.5mm wide x 25.4mm tall + public static Size BOARD_WIDTH = new Size(62.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(25.4d, SizeUnit.mm); + + private static final double EDGE_MARGIN = 15.0; + + // Pin name arrays + private static final String[] LEFT_I2C_NAMES = {"GND", "OE", "SCL", "SDA", "VCC", "V+"}; + private static final String[] RIGHT_I2C_NAMES = {"GND", "OE", "SCL", "SDA", "VCC", "V+"}; + + public PCA9685ServoDriver() { + super(); + this.bodyColor = PCA_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + // 0..5: Left I2C header + if (index < 6) { + return LEFT_I2C_NAMES[index]; + } + // 6..11: Right I2C header + if (index < 12) { + return RIGHT_I2C_NAMES[index - 6]; + } + // 12..13: Screw terminal (V+, GND) + if (index == 12) return "V+ (Ext Power)"; + if (index == 13) return "GND (Ext Power)"; + // 14..61: 16 channels x 3 pins (PWM, V+, GND) + if (index < 62) { + int chanIdx = index - 14; + int channel = chanIdx / 3; + int row = chanIdx % 3; + switch (row) { + case 0: return "PWM" + channel; + case 1: return "V+" + channel; + case 2: return "GND" + channel; + } + } + return Integer.toString(index + 1); + } + + /** + * Calculates the X coordinate of a specific channel column (0..15) relative to board center. + * Channels are split into 4 distinct sections with 4 pins each: + * Sec 0: ch 0..3 (left-outer) + * Sec 1: ch 4..7 (left-inner) + * Center Gap (housing IC and PWM/V+/GND labels) + * Sec 2: ch 8..11 (right-inner) + * Sec 3: ch 12..15 (right-outer) + */ + private double getChannelColumnX(int col, double centerX) { + int sec = col / 4; + int colInSec = col % 4; + + switch (sec) { + case 0: // Channels 0..3 + return centerX - 191.0 + colInSec * 20.0; + case 1: // Channels 4..7 + return centerX - 95.0 + colInSec * 20.0; + case 2: // Channels 8..11 + return centerX + 35.0 + colInSec * 20.0; + case 3: // Channels 12..15 + default: + return centerX + 131.0 + colInSec * 20.0; + } + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double topMargin = (boardH - 5 * spacing) / 2.0; + double centerX = boardW / 2.0 - EDGE_MARGIN; + + // 62 pins total: + // Left I2C header (6 pins): 0..5 (GND, OE, SCL, SDA, VCC, V+) + // Right I2C header (6 pins): 6..11 + // Screw terminal (2 pins): 12 = V+, 13 = GND (centered horizontally at top) + // 16 Servo Channels x 3 rows (PWM, V+, GND) = 48 pins (14..61) in 4 sections + double[][] relativeOffsets = new double[62][2]; + + // Left I2C header — vertically centered, spaced equally from left edge + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + // Right I2C header — vertically centered, spaced equally from right edge + double rightHeaderX = boardW - 2 * EDGE_MARGIN; + for (int i = 0; i < 6; i++) { + relativeOffsets[6 + i][0] = rightHeaderX; + relativeOffsets[6 + i][1] = i * spacing; + } + + // Screw terminal (V+, GND) — top center, pitch 28px (~3.5mm) + double screwY = 25.0 - topMargin; + relativeOffsets[12][0] = centerX - 14.0; + relativeOffsets[12][1] = screwY; + relativeOffsets[13][0] = centerX + 14.0; + relativeOffsets[13][1] = screwY; + + // 16-Channel servo pins in 4 distinct sections along the bottom: + // 3 rows: Row 0 (PWM / Yellow), Row 1 (V+ / Red), Row 2 (GND / Black) + double row0_Y = 140.0 - topMargin; + double row1_Y = 160.0 - topMargin; + double row2_Y = 180.0 - topMargin; + + int idx = 14; + for (int col = 0; col < 16; col++) { + double colX = getChannelColumnX(col, centerX); + // Row 0: PWM + relativeOffsets[idx][0] = colX; + relativeOffsets[idx][1] = row0_Y; + idx++; + // Row 1: V+ + relativeOffsets[idx][0] = colX; + relativeOffsets[idx][1] = row1_Y; + idx++; + // Row 2: GND + relativeOffsets[idx][0] = colX; + relativeOffsets[idx][1] = row2_Y; + idx++; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double spacing = PIN_SPACING.convertToPixels(); + double topMargin = (boardH - 5 * spacing) / 2.0; + double boardX = x - EDGE_MARGIN; + double boardY = y - topMargin; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 10, 10); + } + + @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 spacing = PIN_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double topMargin = (boardH - 5 * spacing) / 2.0; + double boardX = x - EDGE_MARGIN; + double boardY = y - topMargin; + double centerX = boardX + boardW / 2.0; + double rightHeaderX = boardW - 2 * EDGE_MARGIN; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes with annular silver rings + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 15); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardH - 16, 15); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + 16, 15); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + boardH - 16, 15); + + // 2-Pin Screw Terminal Block — centered horizontally at top of the board + double termW = 56.0; + double termH = 34.0; + double termX = centerX - termW / 2.0; + double termY = boardY + 8.0; + + // Terminal Silk labels (V+ on left, GND on right of the terminal block) + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT_BOLD); + StringUtils.drawCenteredText(g2d, "V+", termX - 8, termY + termH / 2.0, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "GND", termX + termW + 8, termY + termH / 2.0, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + + // PCA9685 TSSOP-28 IC — centered vertically right below the terminal block + double chipW = 28.0; + double chipH = 50.0; + double chipX = centerX - chipW / 2.0; + double chipY = termY + termH + 6.0; + MakerBoardPainter.drawChip(g2d, chipX, chipY, chipW, chipH, "PCA9685"); + + // Large Filter Electrolytic Capacitor (Top-Left) + double capX = boardX + 65.0; + double capY = boardY + 30.0; + double capD = 32.0; + g2d.setColor(CAP_BODY_COLOR); + g2d.fill(new Ellipse2D.Double(capX - capD / 2.0, capY - capD / 2.0, capD, capD)); + // Negative stripe on left side + g2d.setColor(CAP_STRIPE_COLOR); + Shape oldClip = g2d.getClip(); + g2d.clip(new Ellipse2D.Double(capX - capD / 2.0, capY - capD / 2.0, capD, capD)); + g2d.fill(new Rectangle2D.Double(capX - capD / 2.0, capY - capD / 2.0, 9, capD)); + g2d.setClip(oldClip); + g2d.setColor(CAP_BORDER_COLOR); + g2d.draw(new Ellipse2D.Double(capX - capD / 2.0, capY - capD / 2.0, capD, capD)); + + // Power LED & resistor (between capacitor and terminal block) + g2d.setColor(POWER_LED_COLOR); + g2d.fill(new RoundRectangle2D.Double(centerX - 60, boardY + 22, 8, 8, 2, 2)); + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT_TINY); + StringUtils.drawCenteredText(g2d, "POWER", centerX - 56, boardY + 14, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // I2C Address Solder Jumpers A0..A5 (Top-Right) + double addrX = centerX + 55.0; + double addrY = boardY + 12.0; + g2d.setColor(SMD_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(addrX, addrY, 60, 16, 2, 2)); + g2d.setColor(GOLD_PAD_COLOR); + for (int a = 0; a < 6; a++) { + g2d.fillRect((int) (addrX + 3 + a * 9.5), (int) (addrY + 3), 7, 10); + } + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT_TINY); + StringUtils.drawCenteredText(g2d, "I2C Address", addrX + 30, addrY + 24, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Middle Area: 4 SMD Resistor Array packs + g2d.setColor(SMD_ARRAY_COLOR); + // Left 2 packs (for channels 0..7) + g2d.fill(new RoundRectangle2D.Double(centerX - 120, boardY + 75, 42, 14, 2, 2)); + g2d.fill(new RoundRectangle2D.Double(centerX - 68, boardY + 75, 42, 14, 2, 2)); + // Right 2 packs (for channels 8..15) + g2d.fill(new RoundRectangle2D.Double(centerX + 26, boardY + 75, 42, 14, 2, 2)); + g2d.fill(new RoundRectangle2D.Double(centerX + 78, boardY + 75, 42, 14, 2, 2)); + + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT_TINY); + StringUtils.drawCenteredText(g2d, "16 x 12-bit PWM", centerX - 80, boardY + 65, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // 4 Sections of 3-row color-coded header strips for 16 channels + double row0_Y = boardY + 140.0; + double row1_Y = boardY + 160.0; + double row2_Y = boardY + 180.0; + double stripH = 14.0; + + double[] secStarts = { + centerX - 201.0, // Sec 0 (ch 0..3) + centerX - 105.0, // Sec 1 (ch 4..7) + centerX + 25.0, // Sec 2 (ch 8..11) + centerX + 121.0 // Sec 3 (ch 12..15) + }; + double secW = 80.0; + + for (int sec = 0; sec < 4; sec++) { + double secStartX = secStarts[sec]; + + // PWM row (Yellow) + g2d.setColor(PWM_YELLOW); + g2d.fill(new RoundRectangle2D.Double(secStartX, row0_Y - stripH / 2.0, secW, stripH, 2, 2)); + // V+ row (Red) + g2d.setColor(V_RED); + g2d.fill(new RoundRectangle2D.Double(secStartX, row1_Y - stripH / 2.0, secW, stripH, 2, 2)); + // GND row (Black) + g2d.setColor(GND_BLACK); + g2d.fill(new RoundRectangle2D.Double(secStartX, row2_Y - stripH / 2.0, secW, stripH, 2, 2)); + } + + // Center Column Labels between Section 1 and Section 2 (PWM, V+, GND) + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT_BOLD); + StringUtils.drawCenteredText(g2d, "PWM", centerX, row0_Y, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "V+", centerX, row1_Y, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "GND", centerX, row2_Y, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Channel numbers silkscreen above each channel column + g2d.setFont(LABEL_FONT_TINY); + for (int ch = 0; ch < 16; ch++) { + double colX = getChannelColumnX(ch, centerX); + StringUtils.drawCenteredText(g2d, Integer.toString(ch), colX, row0_Y - 12.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // Left I2C header labels + g2d.setFont(LABEL_FONT_BOLD); + for (int i = 0; i < 6; i++) { + StringUtils.drawCenteredText(g2d, LEFT_I2C_NAMES[i], boardX + 28, boardY + 50.0 + i * spacing, + HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + } + + // Right I2C header labels + for (int i = 0; i < 6; i++) { + StringUtils.drawCenteredText(g2d, RIGHT_I2C_NAMES[i], boardX + boardW - 28, boardY + 50.0 + i * spacing, + HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw I2C header pins (left & right) + drawPinHeader(g2d, 0, 6, outlineMode, drawingObserver); + drawPinHeader(g2d, 6, 6, outlineMode, drawingObserver); + + // Draw screw terminal block matching PCBTerminalBlock style (V+, GND) + drawTerminalBlock(g2d, 12, 2, true, -1, 34.0, outlineMode, drawingObserver); + + // Draw all 48 servo channel pins + drawPinHeader(g2d, 14, 48, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(PCA_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(PCA_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + // 4 Channel pin strip blocks + g2d.setColor(PWM_YELLOW); + g2d.fillRect(5, height - 11, width / 2 - 7, 2); + g2d.fillRect(width / 2 + 2, height - 11, width / 2 - 7, 2); + g2d.setColor(V_RED); + g2d.fillRect(5, height - 8, width / 2 - 7, 2); + g2d.fillRect(width / 2 + 2, height - 8, width / 2 - 7, 2); + g2d.setColor(GND_BLACK); + g2d.fillRect(5, height - 5, width / 2 - 7, 2); + g2d.fillRect(width / 2 + 2, height - 5, width / 2 - 7, 2); + // Screw terminal + g2d.setColor(SCREW_TERMINAL_COLOR); + g2d.fillRect(width / 2 - 4, 6, 8, 5); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/RFIDRC522.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RFIDRC522.java new file mode 100644 index 000000000..b5aab7f37 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RFIDRC522.java @@ -0,0 +1,209 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "RFID Reader (RC522)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "MFRC522 13.56MHz RFID Reader/Writer SPI Module", +// instanceNamePrefix = "RFID", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class RFIDRC522 extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color RFID_BLUE = Color.decode("#1B4F72"); + public static Color ANTENNA_GOLD = Color.decode("#D4AC0D"); + public static Color CRYSTAL_BODY = Color.decode("#BDC3C7"); + public static Color CRYSTAL_BORDER = Color.decode("#7F8C8D"); + + public static Size BOARD_WIDTH = new Size(60.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(40.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"SDA", "SCK", "MOSI", "MISO", "IRQ", "GND", "RST", "3.3V"}; + + public RFIDRC522() { + super(); + this.bodyColor = RFID_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 8-pin 0.1" header: SDA, SCK, MOSI, MISO, IRQ, GND, RST, 3.3V + double[][] relativeOffsets = new double[8][2]; + for (int i = 0; i < 8; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 7 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 7 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting Holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardH - 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 40, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 40, boardY + boardH - 16, 16); + + // Large PCB Loop Antenna (Left side) + double antX = boardX + 25; + double antY = boardY + 20; + double antW = boardW * 0.45; + double antH = boardH - 40; + + g2d.setColor(ANTENNA_GOLD); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + for (int i = 0; i < 4; i++) { + g2d.draw(new RoundRectangle2D.Double(antX + i * 4, antY + i * 4, antW - i * 8, antH - i * 8, 4, 4)); + } + + // MFRC522 QFN IC (Center Right) + double icX = boardX + boardW * 0.62; + double icY = boardY + boardH / 2.0 - 18; + MakerBoardPainter.drawChip(g2d, icX, icY, 36, 36, "RC522"); + + // 27.120 MHz Crystal + g2d.setColor(CRYSTAL_BODY); + g2d.fill(new RoundRectangle2D.Double(icX - 25, icY + 6, 18, 24, 3, 3)); + g2d.setColor(CRYSTAL_BORDER); + g2d.draw(new RoundRectangle2D.Double(icX - 25, icY + 6, 18, 24, 3, 3)); + + // Silk Screen Text + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT); + StringUtils.drawCenteredText(g2d, "RFID-RC522", antX + antW / 2.0, antY + antH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silk Screen Pin Labels + g2d.setFont(SILK_FONT_SMALL); + String[] labels = new String[] { "SDA", "SCK", "MOSI", "MISO", "IRQ", "GND", "RST", "3.3V" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 8; i++) { + StringUtils.drawCenteredText(g2d, labels[i], boardX + boardW - 24, y + i * spacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw 8 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(RFID_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + g2d.setColor(ANTENNA_GOLD); + g2d.drawRect(6, 6, width / 2 - 4, height - 12); + g2d.drawRect(8, 8, width / 2 - 8, height - 16); + g2d.setColor(Color.BLACK); + g2d.fillRect(width - 12, height / 2 - 4, 8, 8); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/RTCModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RTCModule.java new file mode 100644 index 000000000..12684bda6 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RTCModule.java @@ -0,0 +1,205 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Real-Time Clock (DS3231)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "DS3231 High-Precision I2C Real-Time Clock Module with Battery Backup", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class RTCModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color RTC_BLUE = Color.decode("#0055A5"); + public static Color BATTERY_HOLDER_COLOR = Color.decode("#E0E0E0"); + public static Color BATTERY_BORDER_COLOR = Color.decode("#9E9E9E"); + public static Color BATTERY_TEXT_COLOR = Color.decode("#757575"); + public static Size BOARD_WIDTH = new Size(38.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(22.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "32K", "SQW", "SCL", "SDA", "VCC", "GND" + }; + + public RTCModule() { + super(); + this.bodyColor = RTC_BLUE; + 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[][] relativeOffsets = new double[6][2]; + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 12; + double boardY = y - (boardH - 5 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 12; + double boardY = y - (boardH - 5 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 2 Mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + boardH - 16, 16); + + // Round CR2032 Coin Cell Battery holder + double battD = boardH - 30; + double battX = boardX + boardW - battD - 35; + double battY = boardY + 15; + + g2d.setColor(BATTERY_HOLDER_COLOR); + g2d.fill(new Ellipse2D.Double(battX, battY, battD, battD)); + g2d.setColor(BATTERY_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(battX, battY, battD, battD)); + + g2d.setColor(BATTERY_TEXT_COLOR); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "CR2032", battX + battD / 2.0, battY + battD / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // DS3231 SOIC-16 IC + MakerBoardPainter.drawChip(g2d, boardX + 35, boardY + 25, 65, 50, "DS3231"); + + // AT24C32 EEPROM + MakerBoardPainter.drawChip(g2d, boardX + 35, boardY + 90, 50, 35, "24C32"); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "RTC I2C", boardX + 60, boardY + boardH - 16, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(RTC_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(RTC_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + + // Coin cell + g2d.setColor(BATTERY_HOLDER_COLOR); + g2d.fillOval(width - 14, 8, 10, 10); + + // IC + g2d.setColor(IC_BODY_COLOR); + g2d.fillRect(8, 8, 8, 8); + + // Pins on left + g2d.setColor(PIN_COLOR); + for (int y = 7; y < height - 7; y += 3) { + g2d.fillRect(3, y, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/RelayModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RelayModule.java new file mode 100644 index 000000000..4a8a5bf80 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RelayModule.java @@ -0,0 +1,271 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Relay Module (1/2/4-Channel)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "5V Optocoupler-Isolated Relay Module with Screw Terminals", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class RelayModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum RelayChannels { + _1_Channel("1-Channel (50x26mm)", 1, 50.0, 26.0), + _2_Channel("2-Channel (50x38mm)", 2, 50.0, 38.0), + _4_Channel("4-Channel (75x55mm)", 4, 75.0, 55.0); + + private final String label; + private final int count; + private final double widthMm; + private final double heightMm; + + RelayChannels(String label, int count, double widthMm, double heightMm) { + this.label = label; + this.count = count; + this.widthMm = widthMm; + this.heightMm = heightMm; + } + + @Override public String toString() { return label; } + public int getCount() { return count; } + public double getWidthMm() { return widthMm; } + public double getHeightMm() { return heightMm; } + } + + public static Color PCB_BLUE = Color.decode("#0055A5"); + public static Color RELAY_CUBE_BLUE = Color.decode("#1565C0"); + + private RelayChannels channels = RelayChannels._2_Channel; + + public RelayModule() { + super(); + this.bodyColor = PCB_BLUE; + updateControlPoints(); + } + + @EditableProperty(name = "Channels") + public RelayChannels getChannels() { + return channels; + } + + public void setChannels(RelayChannels channels) { + this.channels = channels; + updateControlPoints(); + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + int chCount = channels.getCount(); + int termPinCount = chCount * 3; + + if (index < termPinCount) { + int ch = index / 3 + 1; + int pin = index % 3; + switch (pin) { + case 0: return "CH" + ch + "_NO"; + case 1: return "CH" + ch + "_COM"; + case 2: return "CH" + ch + "_NC"; + } + } else { + int ctrlIndex = index - termPinCount; + if (ctrlIndex == 0) return "VCC"; + if (ctrlIndex == 1) return "GND"; + return "IN" + (ctrlIndex - 1); + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + int chCount = channels.getCount(); + double termSpacing = new Size(5.08d, SizeUnit.mm).convertToPixels(); + double pinSpacing = PIN_SPACING.convertToPixels(); + + int totalPins = chCount * 3 + 2 + chCount; // 3 per relay + VCC, GND + IN1..INn + double[][] relativeOffsets = new double[totalPins][2]; + + // High Voltage screw terminals (NO, COM, NC per channel) along left edge + for (int ch = 0; ch < chCount; ch++) { + double chY = ch * (3.5 * termSpacing); + for (int p = 0; p < 3; p++) { + int idx = ch * 3 + p; + relativeOffsets[idx][0] = 0; + relativeOffsets[idx][1] = chY + p * termSpacing; + } + } + + // Low Voltage Control Header (VCC, GND, IN1..INn) on opposite right edge + double rightX = new Size(channels.getWidthMm() - 10, SizeUnit.mm).convertToPixels(); + int ctrlPins = 2 + chCount; + for (int p = 0; p < ctrlPins; p++) { + int idx = chCount * 3 + p; + relativeOffsets[idx][0] = rightX; + relativeOffsets[idx][1] = p * pinSpacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = new Size(channels.getWidthMm(), SizeUnit.mm).convertToPixels(); + double boardH = new Size(channels.getHeightMm(), SizeUnit.mm).convertToPixels(); + double boardX = x - 18; + double boardY = y - 18; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = new Size(channels.getWidthMm(), SizeUnit.mm).convertToPixels(); + double boardH = new Size(channels.getHeightMm(), SizeUnit.mm).convertToPixels(); + double boardX = x - 18; + double boardY = y - 18; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardH - 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + boardH - 16, 16); + + // Blue cubic relay blocks (Songle 10A style) + int chCount = channels.getCount(); + double relayW = 120; + double relayH = (boardH - 40) / chCount; + + for (int ch = 0; ch < chCount; ch++) { + double ry = boardY + 20 + ch * relayH; + double rx = boardX + 55; + + // Blue Relay cube + g2d.setColor(RELAY_CUBE_BLUE); + g2d.fill(new RoundRectangle2D.Double(rx, ry + 2, relayW, relayH - 4, 4, 4)); + g2d.setColor(RELAY_CUBE_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(rx, ry + 2, relayW, relayH - 4, 4, 4)); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "SONGLE", rx + relayW / 2.0, ry + relayH / 2.0 - 6, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "10A 250VAC", rx + relayW / 2.0, ry + relayH / 2.0 + 8, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Status LED + g2d.setColor(Color.RED); + g2d.fillOval((int) (rx + relayW + 15), (int) (ry + relayH / 2.0 - 4), 8, 8); + } + } + + g2d.setTransform(oldTx); + + // Draw terminal blocks matching PCBTerminalBlock style for each channel + int chCount = channels.getCount(); + for (int ch = 0; ch < chCount; ch++) { + drawTerminalBlock(g2d, ch * 3, 3, false, -1, 40.0, outlineMode, drawingObserver); + } + + // Draw control pins on right + drawPinHeader(g2d, chCount * 3, 2 + chCount, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(PCB_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(PCB_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + + // Relay block + g2d.setColor(RELAY_CUBE_BLUE); + g2d.fillRect(10, 8, 14, height - 16); + + // Terminal + g2d.setColor(SCREW_TERMINAL_COLOR); + g2d.fillRect(4, 9, 5, height - 18); + + g2d.setColor(Color.RED); + g2d.fillOval(width - 7, height / 2 - 2, 4, 4); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/RotaryEncoderKY040.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RotaryEncoderKY040.java new file mode 100644 index 000000000..f8b3b619b --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/RotaryEncoderKY040.java @@ -0,0 +1,195 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Rotary Encoder (KY-040)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "KY-040 Incremental Rotary Encoder Module with Push Button", +// instanceNamePrefix = "ENC", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class RotaryEncoderKY040 extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color ENCODER_PCB = Color.decode("#1B2631"); + public static Color METAL_SHAFT = Color.decode("#BDC3C7"); + + public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(19.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"CLK", "DT", "SW", "+", "GND"}; + + public RotaryEncoderKY040() { + super(); + this.bodyColor = ENCODER_PCB; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 5-pin 0.1" header: CLK, DT, SW, +, GND + double[][] relativeOffsets = new double[5][2]; + for (int i = 0; i < 5; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 4 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 4 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 2 Mounting Holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardH - 16, 16); + + // Rotary Encoder Metal Body (Center Left) + double cx = boardX + 90; + double cy = boardY + boardH / 2.0; + + g2d.setColor(Color.decode("#7F8C8D")); + g2d.fill(new RoundRectangle2D.Double(cx - 36, cy - 36, 72, 72, 6, 6)); + + // Threaded Collar / Bushing + g2d.setColor(METAL_SHAFT); + g2d.fill(new Ellipse2D.Double(cx - 24, cy - 24, 48, 48)); + g2d.setColor(Color.decode("#95A5A6")); + g2d.draw(new Ellipse2D.Double(cx - 24, cy - 24, 48, 48)); + + // D-Shaft (Flatted center shaft) + g2d.setColor(Color.decode("#34495E")); + g2d.fill(new Ellipse2D.Double(cx - 15, cy - 15, 30, 30)); + g2d.setColor(METAL_SHAFT); + g2d.fillRect((int)(cx + 4), (int)(cy - 14), 10, 28); + + // Silk Screen Pin Labels + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + String[] labels = new String[] { "CLK", "DT", "SW", "+", "GND" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 5; i++) { + StringUtils.drawCenteredText(g2d, labels[i], boardX + boardW - 32, y + i * spacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw 5 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(ENCODER_PCB); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(METAL_SHAFT); + g2d.fill(new Ellipse2D.Double(width / 2.0 - 8, height / 2.0 - 8, 16, 16)); + g2d.setColor(Color.decode("#34495E")); + g2d.fill(new Ellipse2D.Double(width / 2.0 - 4, height / 2.0 - 4, 8, 8)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/SDCardModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/SDCardModule.java new file mode 100644 index 000000000..5e147423c --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/SDCardModule.java @@ -0,0 +1,184 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "SD Card Reader Module", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "MicroSD Card SPI Interface Module with Level Shifter", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class SDCardModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SDCARD_BLUE = Color.decode("#0055A5"); + public static Size BOARD_WIDTH = new Size(42.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(24.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "GND", "VCC", "MISO", "MOSI", "SCK", "CS" + }; + + public SDCardModule() { + super(); + this.bodyColor = SDCARD_BLUE; + 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[][] relativeOffsets = new double[6][2]; + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 12; + double boardY = y - (boardH - 5 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 12; + double boardY = y - (boardH - 5 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 15, boardY + 15, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 15, boardY + boardH - 15, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 15, boardY + 15, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 15, boardY + boardH - 15, 16); + + // MicroSD metal push-push slot + MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW - 120, boardY + 25, 110, boardH - 50, "MicroSD"); + + // 74LVC125A Level Shifter chip + MakerBoardPainter.drawChip(g2d, boardX + 45, boardY + 40, 60, 45, "74LVC125"); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "SD SPI", boardX + 75, boardY + boardH - 25, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SDCARD_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(SDCARD_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + + // SD metal slot + g2d.setColor(USB_METAL_COLOR); + g2d.fillRect(14, 7, width - 18, height - 14); + + // Header pins on left + g2d.setColor(PIN_COLOR); + for (int y = 7; y < height - 7; y += 3) { + g2d.fillRect(4, y, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/TP4056Charger.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/TP4056Charger.java new file mode 100644 index 000000000..02c2c379c --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/TP4056Charger.java @@ -0,0 +1,207 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "LiPo Charger (TP4056)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "TP4056 1A Li-ion Battery Charging and Protection Module", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class TP4056Charger extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color TP4056_BLUE = Color.decode("#1B4F72"); + public static Size BOARD_WIDTH = new Size(28.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(17.5d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"IN+", "IN-", "OUT+", "B+", "B-", "OUT-"}; + + public TP4056Charger() { + super(); + this.bodyColor = TP4056_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + private static final double PAD_MARGIN_X = 14.0; + private static final double PAD_MARGIN_Y = 14.0; + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double h = BOARD_HEIGHT.convertToPixels(); + double w = BOARD_WIDTH.convertToPixels(); + + // 6 Solder pads: + // Input (Left): 0 = IN+ (Top-Left), 1 = IN- (Bottom-Left) + // Output (Right): 2 = OUT+ (Top-Right), 3 = B+, 4 = B-, 5 = OUT- (Bottom-Right) + double rightX = w - 2 * PAD_MARGIN_X; + double padSpanY = h - 2 * PAD_MARGIN_Y; + double rightSpacing = Math.min(spacing, padSpanY / 3.0); + + double[][] relativeOffsets = new double[][] { + { 0, 0 }, // Pin 0: IN+ (Top-Left) + { 0, padSpanY }, // Pin 1: IN- (Bottom-Left) + { rightX, 0 }, // Pin 2: OUT+ (Top-Right) + { rightX, rightSpacing }, // Pin 3: B+ + { rightX, padSpanY - rightSpacing }, // Pin 4: B- + { rightX, padSpanY } // Pin 5: OUT- (Bottom-Right) + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - PAD_MARGIN_Y; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - PAD_MARGIN_X; + double boardY = y - PAD_MARGIN_Y; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // USB Type-C Connector (Left Edge) + MakerBoardPainter.drawUsbC(g2d, boardX - 6, boardY + boardH / 2.0 - 18, 32, 36, "Type-C"); + + // TP4056 Linear Charger IC (SOP-8) + MakerBoardPainter.drawChip(g2d, boardX + 60, boardY + 20, 26, 32, "4056"); + + // DW01A Battery Protection IC & Dual MOSFET + MakerBoardPainter.drawChip(g2d, boardX + 110, boardY + 22, 18, 20, "DW01"); + MakerBoardPainter.drawChip(g2d, boardX + 110, boardY + boardH - 44, 20, 24, "8205"); + + // Dual Status LEDs (Red = CHRG, Blue = STDBY) + g2d.setColor(Color.RED); + g2d.fill(new RoundRectangle2D.Double(boardX + 42, boardY + 18, 8, 12, 2, 2)); + g2d.setColor(Color.CYAN); + g2d.fill(new RoundRectangle2D.Double(boardX + 42, boardY + boardH - 30, 8, 12, 2, 2)); + + // Silk Screen Labels (cleanly positioned beside pads) + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "IN+", x + 16, y, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "IN-", x + 16, y + boardH - 2 * PAD_MARGIN_Y, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + + double rightX = x + boardW - 2 * PAD_MARGIN_X; + double padSpanY = boardH - 2 * PAD_MARGIN_Y; + double rightSpacing = Math.min(PIN_SPACING.convertToPixels(), padSpanY / 3.0); + StringUtils.drawCenteredText(g2d, "OUT+", rightX - 16, y, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "B+", rightX - 16, y + rightSpacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "B-", rightX - 16, y + padSpanY - rightSpacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "OUT-", rightX - 16, y + padSpanY, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + // Draw tinned solder pads + drawSolderPads(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(TP4056_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(Color.LIGHT_GRAY); + g2d.fillRect(2, height / 2 - 4, 8, 8); + g2d.setColor(Color.RED); + g2d.fill(new Ellipse2D.Double(14, height / 2 - 3, 6, 6)); + g2d.setColor(Color.CYAN); + g2d.fill(new Ellipse2D.Double(22, height / 2 - 3, 6, 6)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/modules/ULN2003Driver.java b/diylc/diylc-library/src/main/java/org/diylc/components/modules/ULN2003Driver.java new file mode 100644 index 000000000..3dc8a2399 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/modules/ULN2003Driver.java @@ -0,0 +1,337 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.modules; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Stepper Driver Board (ULN2003)", category = "Modules & Breakouts", +// author = "Branislav Stojkovic", description = "ULN2003 Stepper Motor Driver Board with 4 Status LEDs", +// instanceNamePrefix = "MOD", zOrder = IDIYComponent.BOARD, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class ULN2003Driver extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color ULN_GREEN = Color.decode("#1E824C"); + public static Color JST_BODY_COLOR = Color.decode("#F4F6F6"); + public static Color JST_BORDER_COLOR = Color.decode("#BDC3C7"); + public static Color LED_DOME_COLOR = Color.decode("#FCF3CF"); + public static Color LED_BORDER_COLOR = Color.decode("#B7950B"); + public static Color JUMPER_COLOR = Color.decode("#1C1C1C"); + + public static Font LABEL_FONT_BOLD = new Font("SansSerif", Font.BOLD, 9); + public static Font LABEL_FONT_TINY = new Font("SansSerif", Font.PLAIN, 8); + + // Real ULN2003 stepper driver board dimensions: ~32mm wide x 35mm tall + public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(35.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = { + "IN1", "IN2", "IN3", "IN4", + "GND (-)", "+5-12V (+)", "PWR Jumper 1", "PWR Jumper 2", + "Motor A", "Motor B", "Motor C", "Motor D", "Motor COM (+)" + }; + + public ULN2003Driver() { + super(); + this.bodyColor = ULN_GREEN; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + + // 13 pins total: + // 0..3: IN1, IN2, IN3, IN4 (Left edge control input header) + // 4..7: GND, +5-12V, Jumper 1, Jumper 2 (Bottom power header) + // 8..12: Motor A, B, C, D, COM (5-Pin JST-XH socket) + double[][] relativeOffsets = new double[][] { + // IN1..IN4 along left edge + { 0, 0 }, + { 0, spacing }, + { 0, spacing * 2 }, + { 0, spacing * 3 }, + + // Bottom power header (GND, 5-12V, Jumper 1, Jumper 2) + { 15, boardH - 95.0 }, + { 15 + spacing, boardH - 95.0 }, + { 15 + spacing * 2, boardH - 95.0 }, + { 15 + spacing * 3, boardH - 95.0 }, + + // 5-Pin JST-XH Stepper Motor Socket (Motor A, B, C, D, COM) + { 123.0, -10.0 }, + { 123.0, -10.0 + spacing }, + { 123.0, -10.0 + spacing * 2 }, + { 123.0, -10.0 + spacing * 3 }, + { 123.0, -10.0 + spacing * 4 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 32.0; + double boardY = y - 65.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double spacing = PIN_SPACING.convertToPixels(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 32.0; + double boardY = y - 65.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 4 Mounting Holes with silver solder rings in corners + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + 16, boardY + boardH - 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + 16, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + boardH - 16, 16); + + // Left edge unpopulated through-holes (IN5, IN6, IN7) below IN4 + for (int i = 4; i < 7; i++) { + double px = x; + double py = y + i * spacing; + g2d.setColor(LIGHT_METAL_COLOR); + g2d.fill(new Ellipse2D.Double(px - 5, py - 5, 10, 10)); + g2d.setColor(Constants.CANVAS_COLOR); + g2d.fill(new Ellipse2D.Double(px - 2.5, py - 2.5, 5, 5)); + } + + // ULN2003A DIP-16 IC (Center) - Vertical orientation + double icX = boardX + 68.0; + double icY = boardY + 45.0; + double icW = 44.0; + double icH = 118.0; + + // DIP IC body + g2d.setColor(IC_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(icX, icY, icW, icH, 4, 4)); + g2d.setColor(IC_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(icX, icY, icW, icH, 4, 4)); + + // Pin 1 notch at top + g2d.setColor(IC_BORDER_COLOR); + g2d.drawArc((int) (icX + icW / 2.0 - 6), (int) (icY - 4), 12, 8, 180, 180); + + // Pin 1 dot + g2d.setColor(PIN_MARKER_COLOR); + g2d.fill(new Ellipse2D.Double(icX + 5, icY + 5, 4, 4)); + + // Vertical text on IC + AffineTransform preIC = g2d.getTransform(); + g2d.rotate(Math.PI / 2, icX + icW / 2.0, icY + icH / 2.0); + g2d.setColor(IC_TEXT_COLOR); + g2d.setFont(LABEL_FONT_BOLD); + StringUtils.drawCenteredText(g2d, "ULN2003A", icX + icW / 2.0, icY + icH / 2.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setTransform(preIC); + + // 5-Pin JST-XH Stepper Motor Shrouded Header (Center-Right) + double jstX = boardX + 140.0; + double jstY = boardY + 45.0; + double jstW = 32.0; + double jstH = 100.0; + g2d.setColor(JST_BODY_COLOR); + g2d.fill(new RoundRectangle2D.Double(jstX, jstY, jstW, jstH, 3, 3)); + g2d.setColor(JST_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new RoundRectangle2D.Double(jstX, jstY, jstW, jstH, 3, 3)); + + // Keying notch on JST body + g2d.setColor(JST_BORDER_COLOR); + g2d.drawLine((int) (jstX + 4), (int) (jstY + 10), (int) (jstX + 4), (int) (jstY + jstH - 10)); + + // 4 Status LEDs (A, B, C, D) along the right side + double ledX = boardX + 215.0; + for (int i = 0; i < 4; i++) { + double ly = boardY + 55.0 + i * spacing; + // Outer ring + g2d.setColor(LED_BORDER_COLOR); + g2d.draw(new Ellipse2D.Double(ledX - 10, ly - 10, 20, 20)); + // Clear / Yellow dome + g2d.setColor(LED_DOME_COLOR); + g2d.fill(new Ellipse2D.Double(ledX - 8, ly - 8, 16, 16)); + // Highlight reflection + g2d.setColor(Color.WHITE); + g2d.fill(new Ellipse2D.Double(ledX - 4, ly - 6, 5, 5)); + + // Silkscreen letter (A, B, C, D) + g2d.setFont(LABEL_FONT_BOLD); + String letter = String.valueOf((char) ('A' + i)); + StringUtils.drawCenteredText(g2d, letter, boardX + 185.0, ly, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + // Silkscreen Text Labels + g2d.setColor(Color.WHITE); + g2d.setFont(LABEL_FONT_BOLD); + for (int i = 0; i < 7; i++) { + StringUtils.drawCenteredText(g2d, "IN" + (i + 1), boardX + 16.0, y + i * spacing, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + double pwrHeaderY = boardY + boardH - 30.0; + double pwrX0 = boardX + 47.0; + double pwrX1 = boardX + 67.0; + double pwrJumperCenterX = boardX + 97.0; + + StringUtils.drawCenteredText(g2d, "-", pwrX0, pwrHeaderY + 16.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "+", pwrX1, pwrHeaderY + 16.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "5-12V", pwrJumperCenterX, pwrHeaderY + 16.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Model text along right edge + AffineTransform preModel = g2d.getTransform(); + g2d.rotate(Math.PI / 2, boardX + boardW - 14.0, boardY + boardH / 2.0); + g2d.setFont(LABEL_FONT_BOLD); + StringUtils.drawCenteredText(g2d, "28BYJ-48 DRIVER", boardX + boardW - 14.0, boardY + boardH / 2.0, + HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + g2d.setTransform(preModel); + } + + g2d.setTransform(oldTx); + + // Draw left input header pins (4 populated pins: IN1..IN4) + drawPinHeader(g2d, 0, 4, outlineMode, drawingObserver); + + // Draw bottom power header pins (4 pins: GND, 5-12V, Jumper 1, Jumper 2) + drawPinHeader(g2d, 4, 4, outlineMode, drawingObserver); + + // Draw 5-Pin JST motor connector contacts + drawPinHeader(g2d, 8, 5, outlineMode, drawingObserver); + + if (!outlineMode) { + // Black Jumper shunt cap installed over the two rightmost power selection pins (pins 6 & 7) + double pwrHeaderY = boardY + boardH - 30.0; + double pwrJumperCenterX = boardX + 97.0; + RoundRectangle2D jumperCap = new RoundRectangle2D.Double(pwrJumperCenterX - 16.0, pwrHeaderY - 7.0, 32.0, 14.0, 3, 3); + g2d.setColor(JUMPER_COLOR); + g2d.fill(jumperCap); + g2d.setColor(Color.decode("#444444")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(jumperCap); + } + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(ULN_GREEN); + g2d.fill(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + g2d.setColor(ULN_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 2, width - 4, height - 4, 3, 3)); + + // Vertical DIP IC + g2d.setColor(IC_BODY_COLOR); + g2d.fillRect(width / 2 - 5, 5, 8, height - 10); + + // JST White connector + g2d.setColor(JST_BODY_COLOR); + g2d.fillRect(width - 12, 6, 6, height - 12); + + // LEDs + g2d.setColor(LED_DOME_COLOR); + g2d.fillOval(width - 6, 8, 3, 3); + g2d.fillOval(width - 6, 14, 3, 3); + g2d.fillOval(width - 6, 20, 3, 3); + g2d.fillOval(width - 6, 26, 3, 3); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/robotics/AbstractMotor.java b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/AbstractMotor.java new file mode 100644 index 000000000..2930add02 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/AbstractMotor.java @@ -0,0 +1,231 @@ +/* + * + * 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.robotics; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.appframework.miscutils.ConfigurationManager; +import org.diylc.common.IPlugInPort; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.components.AbstractTransparentComponent; +import org.diylc.core.ComponentState; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Theme; +import org.diylc.core.VisibilityPolicy; +import org.diylc.core.annotations.EditableProperty; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +/** + * Base abstract class for robotics motors (servos, steppers, DC motors). + */ +public abstract class AbstractMotor extends AbstractTransparentComponent { + + private static final long serialVersionUID = 1L; + + public static Color SHAFT_COLOR = Color.decode("#E0E0E0"); + public static Color SHAFT_BORDER_COLOR = Color.decode("#A0A0A0"); + public static Color METAL_BODY_COLOR = Color.decode("#8899A6"); + public static Color BRASS_COLOR = Color.decode("#D4AF37"); + public static Color PIN_COLOR = Color.decode("#00B2EE"); + public static Color PIN_BORDER_COLOR = PIN_COLOR.darker(); + + public static Size PIN_SIZE = new Size(0.04d, SizeUnit.in); + public static Size PIN_SPACING = new Size(0.1d, SizeUnit.in); + + public static Font SILK_FONT_SMALL = new Font("SansSerif", Font.PLAIN, 8); + public static Font SILK_FONT = new Font("SansSerif", Font.BOLD, 9); + + protected Orientation orientation = Orientation.DEFAULT; + protected Point2D[] controlPoints = new Point2D[] {new Point2D.Double(0, 0)}; + protected Color bodyColor = METAL_BODY_COLOR; + + public AbstractMotor() { + super(); + } + + @EditableProperty(name = "Orientation") + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + updateControlPoints(); + invalidateCache(); + } + + @EditableProperty(name = "Body Color") + public Color getBodyColor() { + return bodyColor; + } + + public void setBodyColor(Color bodyColor) { + this.bodyColor = bodyColor; + invalidateCache(); + } + + @Override + public Point2D getControlPoint(int index) { + return controlPoints[index]; + } + + @Override + public void setControlPoint(Point2D point, int index) { + controlPoints[index].setLocation(point); + invalidateCache(); + } + + @Override + public int getControlPointCount() { + return controlPoints.length; + } + + @Override + public boolean isControlPointSticky(int index) { + return true; + } + + @Override + public VisibilityPolicy getControlPointVisibilityPolicy(int index) { + return VisibilityPolicy.WHEN_SELECTED; + } + + @Override + public boolean canControlPointOverlap(int index) { + return false; + } + + @Override + public boolean canPointMoveFreely(int pointIndex) { + return false; + } + + @Override + public Void getValue() { + return null; + } + + @Override + public void setValue(Void value) {} + + protected abstract void updateControlPoints(); + + protected void invalidateCache() {} + + protected void rotatePoints(Point2D firstPoint, double[][] relativeOffsets) { + controlPoints = new Point2D[relativeOffsets.length]; + controlPoints[0] = firstPoint; + double theta = orientation.toRadians(); + AffineTransform tx = AffineTransform.getRotateInstance(theta, firstPoint.getX(), firstPoint.getY()); + for (int i = 1; i < relativeOffsets.length; i++) { + Point2D p = new Point2D.Double(firstPoint.getX() + relativeOffsets[i][0], firstPoint.getY() + relativeOffsets[i][1]); + tx.transform(p, p); + controlPoints[i] = p; + } + } + + protected void drawPins(Graphics2D g2d, int startIndex, int count, boolean female, boolean outlineMode, IDrawingObserver drawingObserver) { + if (outlineMode) return; + int pinPx = (int) Math.round(PIN_SIZE.convertToPixels()); + drawingObserver.startTrackingContinuityArea(true); + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + g2d.setColor(PIN_COLOR); + g2d.fill(new Rectangle2D.Double(p.getX() - pinPx / 2.0, p.getY() - pinPx / 2.0, pinPx, pinPx)); + } + drawingObserver.stopTrackingContinuityArea(); + + for (int i = startIndex; i < startIndex + count && i < controlPoints.length; i++) { + Point2D p = controlPoints[i]; + g2d.setColor(PIN_BORDER_COLOR); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.draw(new Rectangle2D.Double(p.getX() - pinPx / 2.0, p.getY() - pinPx / 2.0, pinPx, pinPx)); + } + } + + protected Color getFinalBorderColor(ComponentState componentState, boolean outlineMode) { + if (outlineMode) { + Theme theme = Constants.DEFAULT_THEME; + try { + Theme t = (Theme) ConfigurationManager.getInstance().readObject(IPlugInPort.THEME_KEY, Constants.DEFAULT_THEME); + if (t != null) theme = t; + } catch (Exception ignored) {} + return (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) ? SELECTION_COLOR : theme.getOutlineColor(); + } + return (componentState == ComponentState.SELECTED || componentState == ComponentState.DRAGGING) ? SELECTION_COLOR : bodyColor.darker(); + } + + /** + * Returns the unrotated body shape in local coordinate space (relative to unrotated p0). + */ + public abstract Shape getBodyShape(); + + @Override + public Rectangle2D getCachingBounds() { + double minX = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxY = -Double.MAX_VALUE; + + if (controlPoints != null) { + for (Point2D p : controlPoints) { + if (p != null) { + if (p.getX() < minX) minX = p.getX(); + if (p.getX() > maxX) maxX = p.getX(); + if (p.getY() < minY) minY = p.getY(); + if (p.getY() > maxY) maxY = p.getY(); + } + } + } + + Shape bodyShape = getBodyShape(); + if (bodyShape != null && controlPoints != null && controlPoints.length > 0 && controlPoints[0] != null) { + Point2D p0 = controlPoints[0]; + AffineTransform tx = AffineTransform.getRotateInstance(orientation.toRadians(), p0.getX(), p0.getY()); + Shape rotated = tx.createTransformedShape(bodyShape); + Rectangle2D b = rotated.getBounds2D(); + if (b.getMinX() < minX) minX = b.getMinX(); + if (b.getMaxX() > maxX) maxX = b.getMaxX(); + if (b.getMinY() < minY) minY = b.getMinY(); + if (b.getMaxY() > maxY) maxY = b.getMaxY(); + } + + if (minX == Double.MAX_VALUE) { + Point2D p0 = (controlPoints != null && controlPoints.length > 0 && controlPoints[0] != null) + ? controlPoints[0] : new Point2D.Double(0, 0); + return new Rectangle2D.Double(p0.getX() - 50, p0.getY() - 50, 100, 100); + } + + int margin = 50; + return new Rectangle2D.Double(minX - margin, minY - margin, (maxX - minX) + 2 * margin, (maxY - minY) + 2 * margin); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/robotics/DCHobbyMotor.java b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/DCHobbyMotor.java new file mode 100644 index 000000000..706985ad9 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/DCHobbyMotor.java @@ -0,0 +1,193 @@ +/* + * + * 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.robotics; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "DC Motor (130-Size / 3V-6V)", category = "Robotics", +// author = "Branislav Stojkovic", description = "Standard 130-Size 3V-6V Toy and Hobby DC Motor with Solder Terminals", +// instanceNamePrefix = "MTR", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class DCHobbyMotor extends AbstractMotor { + + private static final long serialVersionUID = 1L; + + public static Color CAN_METAL = Color.decode("#B0BEC5"); + public static Color END_CAP = Color.decode("#E65100"); + public static Size MOTOR_WIDTH = new Size(15.0d, SizeUnit.mm); + public static Size MOTOR_LENGTH = new Size(25.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "Positive (+)", "Negative (-)" + }; + + public DCHobbyMotor() { + super(); + this.bodyColor = CAN_METAL; + 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 = new Size(0.2d, SizeUnit.in).convertToPixels(); // 40px + + double[][] relativeOffsets = new double[2][2]; + relativeOffsets[0] = new double[] {0, 0}; + relativeOffsets[1] = new double[] {spacing, 0}; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double motorW = MOTOR_WIDTH.convertToPixels(); + double motorL = MOTOR_LENGTH.convertToPixels(); + double motorX = x - (motorW - 40) / 2.0; + double motorY = y - motorL - 15; + return new RoundRectangle2D.Double(motorX, motorY - 35, motorW, motorL + 35, 16, 16); + } + + @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 motorW = MOTOR_WIDTH.convertToPixels(); + double motorL = MOTOR_LENGTH.convertToPixels(); + double motorX = x - (motorW - 40) / 2.0; + double motorY = y - motorL - 15; + + Shape canShape = new RoundRectangle2D.Double(motorX, motorY, motorW, motorL, 16, 16); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(canShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(canShape); + + if (!outlineMode) { + // Front 2mm steel shaft + double shaftW = 12; + double shaftH = 35; + double shaftX = motorX + (motorW - shaftW) / 2.0; + double shaftY = motorY - shaftH; + + g2d.setColor(SHAFT_COLOR); + g2d.fill(new RoundRectangle2D.Double(shaftX, shaftY, shaftW, shaftH, 4, 4)); + g2d.setColor(SHAFT_BORDER_COLOR); + g2d.draw(new RoundRectangle2D.Double(shaftX, shaftY, shaftW, shaftH, 4, 4)); + + // Rear plastic end cap + double capH = 20; + double capY = motorY + motorL - capH; + g2d.setColor(END_CAP); + g2d.fill(new RoundRectangle2D.Double(motorX + 4, capY, motorW - 8, capH, 4, 4)); + + // Solder lug terminals (+ and -) + g2d.setColor(COPPER_COLOR); + g2d.fill(new RoundRectangle2D.Double(x - 5, y - 12, 10, 16, 2, 2)); + g2d.fill(new RoundRectangle2D.Double(x + 35, y - 12, 10, 16, 2, 2)); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "+", x, y - 16, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "-", x + 40, y - 16, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "130 DC", motorX + motorW / 2.0, motorY + motorL / 2.0 - 5, 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(CAN_METAL); + g2d.fill(new RoundRectangle2D.Double(width / 2 - 7, 8, 14, height - 12, 6, 6)); + g2d.setColor(Color.GRAY); + g2d.draw(new RoundRectangle2D.Double(width / 2 - 7, 8, 14, height - 12, 6, 6)); + + // Shaft + g2d.setColor(SHAFT_COLOR); + g2d.fillRect(width / 2 - 2, 2, 4, 6); + + // End cap + g2d.setColor(END_CAP); + g2d.fillRect(width / 2 - 5, height - 8, 10, 4); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/robotics/MicroServoSG90.java b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/MicroServoSG90.java new file mode 100644 index 000000000..a06a2973d --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/MicroServoSG90.java @@ -0,0 +1,207 @@ +/* + * + * 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.robotics; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Micro Servo (SG90 / 9g)", category = "Robotics", +// author = "Branislav Stojkovic", description = "SG90 9g Micro Servo Motor with 3-Pin Lead Connector", +// instanceNamePrefix = "MTR", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class MicroServoSG90 extends AbstractMotor { + + private static final long serialVersionUID = 1L; + + public static Color SERVO_BLUE = Color.decode("#0077CC"); + public static Color HORN_WHITE = Color.decode("#FAFAFA"); + + public static Size BODY_WIDTH = new Size(12.2d, SizeUnit.mm); + public static Size BODY_LENGTH = new Size(22.8d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "GND (Brown/Black)", "VCC 5V (Red)", "PWM Signal (Orange/Yellow)" + }; + + public MicroServoSG90() { + super(); + this.bodyColor = SERVO_BLUE; + 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[][] relativeOffsets = new double[3][2]; + for (int i = 0; i < 3; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double bodyW = BODY_LENGTH.convertToPixels(); + double bodyH = BODY_WIDTH.convertToPixels(); + double bodyX = x - (bodyW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double bodyY = y - bodyH - 45; + double flangeW = bodyW + 40; + double flangeX = bodyX - 20; + return new RoundRectangle2D.Double(flangeX, bodyY, flangeW, bodyH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double bodyW = BODY_LENGTH.convertToPixels(); + double bodyH = BODY_WIDTH.convertToPixels(); + double bodyX = x - (bodyW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double bodyY = y - bodyH - 45; + + // Body with mounting flanges + double flangeW = bodyW + 40; + double flangeX = bodyX - 20; + + Shape bodyShape = new RoundRectangle2D.Double(bodyX, bodyY, bodyW, bodyH, 6, 6); + Shape flangeShape = new RoundRectangle2D.Double(flangeX, bodyY + 10, flangeW, bodyH - 20, 4, 4); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(flangeShape); + g2d.fill(bodyShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(flangeShape); + g2d.draw(bodyShape); + + if (!outlineMode) { + // Mounting holes on flanges + g2d.setColor(Color.WHITE); + g2d.fill(new Ellipse2D.Double(flangeX + 8, bodyY + bodyH / 2.0 - 4, 8, 8)); + g2d.fill(new Ellipse2D.Double(flangeX + flangeW - 16, bodyY + bodyH / 2.0 - 4, 8, 8)); + + // White Output Spline / Servo Horn + double splineX = bodyX + 25; + double splineY = bodyY + bodyH / 2.0; + g2d.setColor(HORN_WHITE); + g2d.fill(new Ellipse2D.Double(splineX - 18, splineY - 18, 36, 36)); + g2d.fill(new RoundRectangle2D.Double(splineX - 10, splineY - 35, 20, 70, 8, 8)); + g2d.setColor(Color.decode("#BDBDBD")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(splineX - 18, splineY - 18, 36, 36)); + g2d.draw(new RoundRectangle2D.Double(splineX - 10, splineY - 35, 20, 70, 8, 8)); + + // Center screw + g2d.setColor(Color.decode("#757575")); + g2d.fill(new Ellipse2D.Double(splineX - 5, splineY - 5, 10, 10)); + + // 3-wire colored cable from body to header connector + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3f)); + // Brown (GND) + g2d.setColor(Color.decode("#795548")); + g2d.drawLine((int) (bodyX + bodyW - 10), (int) (bodyY + bodyH / 2.0 - 8), (int) x, (int) (y - 8)); + // Red (VCC) + g2d.setColor(Color.decode("#E53935")); + g2d.drawLine((int) (bodyX + bodyW - 10), (int) (bodyY + bodyH / 2.0), (int) (x + PIN_SPACING.convertToPixels()), (int) (y - 8)); + // Orange (PWM) + g2d.setColor(Color.decode("#FF9800")); + g2d.drawLine((int) (bodyX + bodyW - 10), (int) (bodyY + bodyH / 2.0 + 8), (int) (x + 2 * PIN_SPACING.convertToPixels()), (int) (y - 8)); + } + + g2d.setTransform(oldTx); + + drawPins(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SERVO_BLUE); + g2d.fill(new RoundRectangle2D.Double(6, 8, width - 12, height - 16, 3, 3)); + g2d.setColor(SERVO_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(6, 8, width - 12, height - 16, 3, 3)); + + // White horn + g2d.setColor(HORN_WHITE); + g2d.fillOval(10, 10, 10, 10); + g2d.fillRect(13, 6, 4, 18); + g2d.setColor(Color.GRAY); + g2d.drawOval(10, 10, 10, 10); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/robotics/NEMA17Stepper.java b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/NEMA17Stepper.java new file mode 100644 index 000000000..ff5d210ca --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/NEMA17Stepper.java @@ -0,0 +1,201 @@ +/* + * + * 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.robotics; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "NEMA 17 Stepper Motor", category = "Robotics", +// author = "Branislav Stojkovic", description = "NEMA 17 42mm Hybrid Bipolar Stepper Motor with 4-Pin Connector", +// instanceNamePrefix = "MTR", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class NEMA17Stepper extends AbstractMotor { + + private static final long serialVersionUID = 1L; + + public static Color MOTOR_FACE = Color.decode("#424242"); + public static Color BOSS_COLOR = Color.decode("#B0BEC5"); + public static Size FACE_SIZE = new Size(42.3d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "1A (Black / A+)", "1B (Green / A-)", "2A (Red / B+)", "2B (Blue / B-)" + }; + + public NEMA17Stepper() { + super(); + this.bodyColor = MOTOR_FACE; + 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[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double faceSizePx = FACE_SIZE.convertToPixels(); + double faceX = x - (faceSizePx - 3 * PIN_SPACING.convertToPixels()) / 2.0; + double faceY = y - faceSizePx - 40; + return new RoundRectangle2D.Double(faceX, faceY, faceSizePx, faceSizePx, 10, 10); + } + + @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 faceSizePx = FACE_SIZE.convertToPixels(); + double faceX = x - (faceSizePx - 3 * PIN_SPACING.convertToPixels()) / 2.0; + double faceY = y - faceSizePx - 40; + + Shape faceShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(faceShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(faceShape); + + if (!outlineMode) { + // 4 M3 mounting holes (31mm pitch ~ 244px) + double holeOffset = new Size((42.3 - 31.0) / 2.0, SizeUnit.mm).convertToPixels(); + double pitchPx = new Size(31.0d, SizeUnit.mm).convertToPixels(); + g2d.setColor(Color.decode("#9E9E9E")); + g2d.fill(new Ellipse2D.Double(faceX + holeOffset - 8, faceY + holeOffset - 8, 16, 16)); + g2d.fill(new Ellipse2D.Double(faceX + holeOffset + pitchPx - 8, faceY + holeOffset - 8, 16, 16)); + g2d.fill(new Ellipse2D.Double(faceX + holeOffset - 8, faceY + holeOffset + pitchPx - 8, 16, 16)); + g2d.fill(new Ellipse2D.Double(faceX + holeOffset + pitchPx - 8, faceY + holeOffset + pitchPx - 8, 16, 16)); + + // Central 22mm round boss + double bossD = new Size(22.0d, SizeUnit.mm).convertToPixels(); + double bossX = faceX + (faceSizePx - bossD) / 2.0; + double bossY = faceY + (faceSizePx - bossD) / 2.0; + + g2d.setColor(BOSS_COLOR); + g2d.fill(new Ellipse2D.Double(bossX, bossY, bossD, bossD)); + g2d.setColor(Color.decode("#78909C")); + g2d.draw(new Ellipse2D.Double(bossX, bossY, bossD, bossD)); + + // 5mm D-Shaft in center + double shaftD = new Size(5.0d, SizeUnit.mm).convertToPixels(); + double shaftX = faceX + (faceSizePx - shaftD) / 2.0; + double shaftY = faceY + (faceSizePx - shaftD) / 2.0; + + g2d.setColor(SHAFT_COLOR); + g2d.fill(new Ellipse2D.Double(shaftX, shaftY, shaftD, shaftD)); + g2d.setColor(SHAFT_BORDER_COLOR); + g2d.draw(new Ellipse2D.Double(shaftX, shaftY, shaftD, shaftD)); + + // 4-wire lead cable to connector (Black, Green, Red, Blue) + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3f)); + Color[] wireColors = new Color[] { + Color.BLACK, Color.decode("#2E7D32"), Color.decode("#C62828"), Color.decode("#1565C0") + }; + for (int i = 0; i < 4; i++) { + g2d.setColor(wireColors[i]); + g2d.drawLine((int) (faceX + faceSizePx / 2.0 - 15 + i * 10), (int) (faceY + faceSizePx), + (int) (x + i * PIN_SPACING.convertToPixels()), (int) (y - 8)); + } + } + + g2d.setTransform(oldTx); + + drawPins(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(MOTOR_FACE); + g2d.fill(new RoundRectangle2D.Double(4, 4, width - 8, height - 8, 3, 3)); + g2d.setColor(Color.GRAY); + g2d.draw(new RoundRectangle2D.Double(4, 4, width - 8, height - 8, 3, 3)); + + // Boss & Shaft + g2d.setColor(BOSS_COLOR); + g2d.fillOval(width / 2 - 6, height / 2 - 6, 12, 12); + g2d.setColor(SHAFT_COLOR); + g2d.fillOval(width / 2 - 3, height / 2 - 3, 6, 6); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/robotics/StepperMotor28BYJ48.java b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/StepperMotor28BYJ48.java new file mode 100644 index 000000000..af6146ae1 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/robotics/StepperMotor28BYJ48.java @@ -0,0 +1,206 @@ +/* + * + * 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.robotics; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Stepper Motor (28BYJ-48)", category = "Robotics", +// author = "Branislav Stojkovic", description = "28BYJ-48 5V Geared Stepper Motor with 5-Pin Connector", +// instanceNamePrefix = "MTR", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class StepperMotor28BYJ48 extends AbstractMotor { + + private static final long serialVersionUID = 1L; + + public static Color MOTOR_BLUE = Color.decode("#1E88E5"); + public static Size BODY_DIAMETER = new Size(28.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "IN1 (Blue)", "IN2 (Pink)", "IN3 (Yellow)", "IN4 (Orange)", "VCC 5V (Red)" + }; + + public StepperMotor28BYJ48() { + super(); + this.bodyColor = MOTOR_BLUE; + 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[][] relativeOffsets = new double[5][2]; + for (int i = 0; i < 5; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double bodyD = BODY_DIAMETER.convertToPixels(); + double bodyX = x - (bodyD - 4 * PIN_SPACING.convertToPixels()) / 2.0; + double bodyY = y - bodyD - 40; + double tabW = bodyD + 60; + double tabX = bodyX - 30; + return new RoundRectangle2D.Double(tabX, bodyY, tabW, bodyD, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double bodyD = BODY_DIAMETER.convertToPixels(); + double bodyX = x - (bodyD - 4 * PIN_SPACING.convertToPixels()) / 2.0; + double bodyY = y - bodyD - 40; + + // Mounting tabs + double tabW = bodyD + 60; + double tabX = bodyX - 30; + + Shape tabShape = getBodyShape(); + Shape bodyShape = new Ellipse2D.Double(bodyX, bodyY, bodyD, bodyD); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(tabShape); + g2d.fill(bodyShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(tabShape); + g2d.draw(bodyShape); + + if (!outlineMode) { + // Mounting holes on side tabs + g2d.setColor(Color.WHITE); + g2d.fill(new Ellipse2D.Double(tabX + 8, bodyY + bodyD / 2.0 - 6, 12, 12)); + g2d.fill(new Ellipse2D.Double(tabX + tabW - 20, bodyY + bodyD / 2.0 - 6, 12, 12)); + + // Metal gearbox casing with flat D-shaft (offset to top) + double gbW = 70; + double gbH = 60; + double gbX = bodyX + (bodyD - gbW) / 2.0; + double gbY = bodyY + 15; + + g2d.setColor(Color.decode("#B0BEC5")); + g2d.fill(new RoundRectangle2D.Double(gbX, gbY, gbW, gbH, 6, 6)); + + // Brass D-Shaft + double shaftD = 30; + double shaftX = gbX + (gbW - shaftD) / 2.0; + double shaftY = gbY + (gbH - shaftD) / 2.0; + + g2d.setColor(BRASS_COLOR); + g2d.fill(new Ellipse2D.Double(shaftX, shaftY, shaftD, shaftD)); + g2d.setColor(BRASS_COLOR.darker()); + g2d.draw(new Ellipse2D.Double(shaftX, shaftY, shaftD, shaftD)); + + // Flat side of D-shaft + g2d.setColor(Color.decode("#8D6E63")); + g2d.drawLine((int) shaftX + 4, (int) shaftY + 6, (int) shaftX + 4, (int) (shaftY + shaftD - 6)); + + // 5-wire ribbon cable (Blue, Pink, Yellow, Orange, Red) + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(3f)); + Color[] wireColors = new Color[] { + Color.decode("#1E88E5"), Color.decode("#E91E63"), Color.decode("#FDD835"), Color.decode("#FF9800"), Color.decode("#E53935") + }; + for (int i = 0; i < 5; i++) { + g2d.setColor(wireColors[i]); + g2d.drawLine((int) (bodyX + bodyD / 2.0 - 20 + i * 10), (int) (bodyY + bodyD), + (int) (x + i * PIN_SPACING.convertToPixels()), (int) (y - 8)); + } + } + + g2d.setTransform(oldTx); + + drawPins(g2d, 0, controlPoints.length, true, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(MOTOR_BLUE); + g2d.fillOval(5, 5, width - 10, height - 10); + g2d.setColor(MOTOR_BLUE.darker()); + g2d.drawOval(5, 5, width - 10, height - 10); + + // Brass shaft + g2d.setColor(BRASS_COLOR); + g2d.fillOval(width / 2 - 4, height / 2 - 4, 8, 8); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/BME280Sensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/BME280Sensor.java new file mode 100644 index 000000000..55b601cf8 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/BME280Sensor.java @@ -0,0 +1,185 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Temp/Pressure/Humidity Sensor (BME280)", category = "Sensors", +// author = "Branislav Stojkovic", description = "BME280 / BMP280 Temperature, Humidity, and Pressure Sensor (I2C/SPI)", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class BME280Sensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color BME_PURPLE = Color.decode("#5B2C6F"); + public static Color SENSOR_METAL = Color.decode("#C0C0C0"); + + public static Size BOARD_WIDTH = new Size(15.2d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(12.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"VCC", "GND", "SCL", "SDA", "CSB", "SDO"}; + + public BME280Sensor() { + super(); + this.bodyColor = BME_PURPLE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 6-pin 0.1" header: VCC, GND, SCL, SDA, CSB, SDO + double[][] relativeOffsets = new double[6][2]; + for (int i = 0; i < 6; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 5 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - 16; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 5 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - 16; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 1 Mounting Hole (Right side) + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 16, boardY + boardH - 20, 14); + + // BME280 Metal Sensor Can with vent hole (Center) + double sx = boardX + 35; + double sy = boardY + boardH - 35; + g2d.setColor(SENSOR_METAL); + g2d.fill(new RoundRectangle2D.Double(sx, sy, 24, 20, 2, 2)); + g2d.setColor(Color.DARK_GRAY); + g2d.draw(new RoundRectangle2D.Double(sx, sy, 24, 20, 2, 2)); + // Vent hole + g2d.setColor(Color.BLACK); + g2d.fill(new Ellipse2D.Double(sx + 5, sy + 5, 4, 4)); + + // LDO Voltage Regulator (Left) + MakerBoardPainter.drawChip(g2d, boardX + 12, boardY + boardH - 32, 16, 16, ""); + + // Silk Screen Text + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "BME280", boardX + boardW / 2.0, boardY + 18, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + // Draw 6 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(BME_PURPLE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(SENSOR_METAL); + g2d.fill(new RoundRectangle2D.Double(width / 2 - 5, height / 2 - 4, 10, 8, 2, 2)); + g2d.setColor(Color.BLACK); + g2d.fill(new Ellipse2D.Double(width / 2 - 3, height / 2 - 2, 2, 2)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/DHTSensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/DHTSensor.java new file mode 100644 index 000000000..dfd3142f6 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/DHTSensor.java @@ -0,0 +1,204 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.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 = "Temp/Humidity Sensor (DHT11 / DHT22)", category = "Sensors", +// author = "Branislav Stojkovic", description = "DHT11 / DHT22 Digital Temperature and Humidity Sensor Module", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class DHTSensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public enum DHTModel { + DHT11("DHT11 (Blue)"), DHT22("DHT22 (White)"); + + private final String label; + DHTModel(String label) { this.label = label; } + @Override public String toString() { return label; } + } + + public static Color DHT11_BLUE = Color.decode("#1E88E5"); + public static Color DHT22_WHITE = Color.decode("#ECEFF1"); + + public static Size BOARD_WIDTH = new Size(15.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(28.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES_3PIN = new String[] {"VCC", "DATA", "GND"}; + + private DHTModel model = DHTModel.DHT11; + + public DHTSensor() { + super(); + this.bodyColor = DHT11_BLUE; + updateControlPoints(); + } + + @EditableProperty(name = "Model") + public DHTModel getModel() { + return model; + } + + public void setModel(DHTModel model) { + this.model = model; + this.bodyColor = (model == DHTModel.DHT11) ? DHT11_BLUE : DHT22_WHITE; + invalidateCache(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES_3PIN.length) { + return PIN_NAMES_3PIN[index]; + } + return "Pin " + (index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + double[][] relativeOffsets = new double[3][2]; + for (int i = 0; i < 3; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : (model == DHTModel.DHT11 ? DHT11_BLUE : DHT22_WHITE)); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Slotted grille lines + Color slotColor = (model == DHTModel.DHT11) ? DHT11_BLUE.darker() : Color.decode("#B0BEC5"); + g2d.setColor(slotColor); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(2f)); + for (double sy = boardY + 25; sy < boardY + boardH - 45; sy += 14) { + g2d.drawLine((int) (boardX + 15), (int) sy, (int) (boardX + boardW - 15), (int) sy); + } + + // Label text + g2d.setColor((model == DHTModel.DHT11) ? Color.WHITE : Color.DARK_GRAY); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, model.name(), boardX + boardW / 2.0, boardY + boardH - 25, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(DHT11_BLUE); + g2d.fill(new RoundRectangle2D.Double(7, 3, width - 14, height - 6, 3, 3)); + g2d.setColor(DHT11_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(7, 3, width - 14, height - 6, 3, 3)); + + // Grille lines + g2d.setColor(Color.WHITE); + g2d.drawLine(10, 8, width - 10, 8); + g2d.drawLine(10, 13, width - 10, 13); + g2d.drawLine(10, 18, width - 10, 18); + + // Pins + g2d.setColor(PIN_COLOR); + g2d.fillRect(width / 2 - 4, height - 5, 2, 2); + g2d.fillRect(width / 2 - 1, height - 5, 2, 2); + g2d.fillRect(width / 2 + 2, height - 5, 2, 2); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java new file mode 100644 index 000000000..6f31e55bb --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/IRReceiverModule.java @@ -0,0 +1,214 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "IR Receiver Module (KY-022)", category = "Sensors", +// author = "Branislav Stojkovic", description = "38kHz Infrared Receiver Module (VS1838B) for remote control", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class IRReceiverModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color PCB_GREEN = Color.decode("#1A6B1A"); + public static Color SENSOR_METAL = Color.decode("#85929E"); + public static Color SENSOR_DOME = Color.decode("#1C2833"); + + public static Size BOARD_WIDTH = new Size(18.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(15.0d, SizeUnit.mm); + + // Standard Keyes KY-022 3-pin layout from left to right: - (GND), + (VCC), S (Signal) + private static final String[] PIN_NAMES = {"- (GND)", "+ (VCC)", "S (Signal)"}; + + public IRReceiverModule() { + super(); + this.bodyColor = PCB_GREEN; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 3 pins along the bottom edge: - (GND), + (VCC), S (Signal) + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { spacing, 0 }, + { spacing * 2, 0 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + double spacing = PIN_SPACING.convertToPixels(); + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // VS1838B sensor element — metal-shielded rectangular body with central black IR dome + double sensorW = new Size(7.5d, SizeUnit.mm).convertToPixels(); + double sensorH = new Size(6.0d, SizeUnit.mm).convertToPixels(); + double sensorX = boardX + (boardW - sensorW) / 2.0; + double sensorY = boardY + 6; + + // Metal shielded box + g2d.setColor(SENSOR_METAL); + g2d.fill(new RoundRectangle2D.Double(sensorX, sensorY, sensorW, sensorH, 3, 3)); + g2d.setColor(SENSOR_METAL.darker()); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1f)); + g2d.draw(new RoundRectangle2D.Double(sensorX, sensorY, sensorW, sensorH, 3, 3)); + + // Central dark IR lens dome + double domeD = sensorH * 0.75; + g2d.setColor(SENSOR_DOME); + g2d.fill(new Ellipse2D.Double(sensorX + (sensorW - domeD) / 2.0, sensorY + (sensorH - domeD) / 2.0, domeD, domeD)); + g2d.setColor(Color.BLACK); + g2d.draw(new Ellipse2D.Double(sensorX + (sensorW - domeD) / 2.0, sensorY + (sensorH - domeD) / 2.0, domeD, domeD)); + + // Red status LED (top right) + g2d.setColor(Color.RED); + g2d.fill(new Ellipse2D.Double(boardX + boardW - 12, boardY + 6, 6, 6)); + + // "KY-022" silkscreen text in the open space between the sensor and pin labels + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + StringUtils.drawCenteredText(g2d, "KY-022", boardX + boardW / 2.0, boardY + 36, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Silkscreen pin labels (-, +, S) directly above the 3 header pins + g2d.setColor(Color.WHITE); + g2d.setFont(new Font("SansSerif", Font.BOLD, 9)); + double labelY = y - 9; + StringUtils.drawCenteredText(g2d, "-", x, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "+", x + spacing, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "S", x + spacing * 2, labelY, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + // Green PCB + g2d.setColor(PCB_GREEN); + g2d.fill(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + g2d.setColor(PCB_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(3, 5, width - 6, height - 10, 3, 3)); + + // Metal shield + dome + g2d.setColor(SENSOR_METAL); + g2d.fillRoundRect(width / 2 - 5, 6, 10, 8, 2, 2); + g2d.setColor(SENSOR_DOME); + g2d.fillOval(width / 2 - 3, 7, 6, 6); + + // Red LED + g2d.setColor(Color.RED); + g2d.fillOval(width - 7, 7, 4, 4); + + // 3 pin dots + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 3; i++) { + g2d.fillRect(6 + i * 4, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/LDRSensorModule.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/LDRSensorModule.java new file mode 100644 index 000000000..d1f19be4a --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/LDRSensorModule.java @@ -0,0 +1,198 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "LDR / Photoresistor Module", category = "Sensors", +// author = "Branislav Stojkovic", description = "Light Dependent Resistor / Photoresistor Sensor Module with Digital/Analog Output", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class LDRSensorModule extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color MODULE_BLUE = Color.decode("#006699"); + public static Size BOARD_WIDTH = new Size(14.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(32.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] {"VCC", "GND", "DO", "AO"}; + + public LDRSensorModule() { + super(); + this.bodyColor = MODULE_BLUE; + 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[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Photoresistor (LDR) bulb at top + double ldrX = boardX + boardW / 2.0; + double ldrY = boardY - 6; + g2d.setColor(Color.decode("#CC6600")); + g2d.fill(new Ellipse2D.Double(ldrX - 12, ldrY - 12, 24, 24)); + g2d.setColor(Color.decode("#993300")); + g2d.draw(new Ellipse2D.Double(ldrX - 12, ldrY - 12, 24, 24)); + // Zig-zag trace on LDR + g2d.setColor(Color.decode("#FF9900")); + g2d.drawLine((int) ldrX - 6, (int) ldrY - 4, (int) ldrX + 6, (int) ldrY - 4); + g2d.drawLine((int) ldrX - 6, (int) ldrY, (int) ldrX + 6, (int) ldrY); + g2d.drawLine((int) ldrX - 6, (int) ldrY + 4, (int) ldrX + 6, (int) ldrY + 4); + + // LM393 comparator chip + MakerBoardPainter.drawChip(g2d, boardX + 15, boardY + 40, boardW - 30, 45, "LM393"); + + // Blue sensitivity trimpot + g2d.setColor(Color.decode("#0088CC")); + g2d.fill(new RoundRectangle2D.Double(boardX + 15, boardY + 100, boardW - 30, 35, 3, 3)); + g2d.setColor(Color.decode("#DCDCDC")); + g2d.fill(new Ellipse2D.Double(boardX + boardW / 2.0 - 6, boardY + 112, 12, 12)); + + // Silkscreen + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "LDR", boardX + boardW / 2.0, boardY + boardH - 22, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(MODULE_BLUE); + g2d.fill(new RoundRectangle2D.Double(8, 4, width - 16, height - 8, 3, 3)); + g2d.setColor(MODULE_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(8, 4, width - 16, height - 8, 3, 3)); + + // LDR sensor top + g2d.setColor(Color.decode("#CC6600")); + g2d.fillOval(width / 2 - 4, 3, 8, 8); + + // Trimpot + g2d.setColor(Color.decode("#0088CC")); + g2d.fillRect(10, 14, width - 20, 6); + + // Pins + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 4; i++) { + g2d.fillRect(10 + i * 3, height - 5, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/MPU6050.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/MPU6050.java new file mode 100644 index 000000000..35a4e9758 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/MPU6050.java @@ -0,0 +1,179 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Gyro/IMU (MPU-6050 / GY-521)", category = "Sensors", +// author = "Branislav Stojkovic", description = "MPU-6050 6-Axis Gyroscope and Accelerometer I2C Breakout Board", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class MPU6050 extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color MPU_BLUE = Color.decode("#0055A5"); + public static Size BOARD_WIDTH = new Size(20.5d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(15.5d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] { + "VCC", "GND", "SCL", "SDA", "XDA", "XCL", "AD0", "INT" + }; + + public MPU6050() { + super(); + this.bodyColor = MPU_BLUE; + 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[][] relativeOffsets = new double[8][2]; + for (int i = 0; i < 8; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 12; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 6, 6); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 12; + double boardY = y - boardH + 12; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Corner mounting hole + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 20, boardY + 20, 16); + + // Central QFN MPU-6050 chip + MakerBoardPainter.drawChip(g2d, boardX + 35, boardY + 20, 45, 45, "6050"); + + // Silkscreen + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "GY-521", boardX + boardW - 35, boardY + 45, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(MPU_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(MPU_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + + // Chip + g2d.setColor(IC_BODY_COLOR); + g2d.fillRect(10, 9, 8, 8); + + // 8 pins at bottom + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 8; i++) { + g2d.fillRect(4 + i * 3, height - 8, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/PIRMotionSensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/PIRMotionSensor.java new file mode 100644 index 000000000..4034b9270 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/PIRMotionSensor.java @@ -0,0 +1,201 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "PIR Motion Sensor (HC-SR501)", category = "Sensors", +// author = "Branislav Stojkovic", description = "HC-SR501 Passive Infrared (PIR) Motion Detector Module", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class PIRMotionSensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SENSOR_GREEN = Color.decode("#2E7D32"); + public static Color DOME_COLOR = Color.decode("#F5F5F5"); + public static Color TRIMPOT_COLOR = Color.decode("#E67E22"); + + public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(24.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] {"VCC", "OUT", "GND"}; + + public PIRMotionSensor() { + super(); + this.bodyColor = SENSOR_GREEN; + 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[][] relativeOffsets = new double[3][2]; + for (int i = 0; i < 3; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 2 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // 2 Corner mounting holes + MakerBoardPainter.drawMountingHole(g2d, boardX + 15, boardY + 15, 16); + MakerBoardPainter.drawMountingHole(g2d, boardX + boardW - 15, boardY + 15, 16); + + // Large central white Fresnel lens dome + double domeD = new Size(20.0d, SizeUnit.mm).convertToPixels(); + double domeX = boardX + (boardW - domeD) / 2.0; + double domeY = boardY + (boardH - domeD) / 2.0 - 10; + + g2d.setColor(DOME_COLOR); + g2d.fill(new Ellipse2D.Double(domeX, domeY, domeD, domeD)); + g2d.setColor(Color.decode("#CCCCCC")); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(domeX, domeY, domeD, domeD)); + + // Fresnel lens ring pattern + for (double r = domeD - 20; r > 10; r -= 20) { + g2d.draw(new Ellipse2D.Double(domeX + (domeD - r) / 2.0, domeY + (domeD - r) / 2.0, r, r)); + } + + // Two orange trimpots (Sensitivity & Delay) + g2d.setColor(TRIMPOT_COLOR); + g2d.fill(new RoundRectangle2D.Double(boardX + 15, boardY + boardH - 35, 25, 20, 3, 3)); + g2d.fill(new RoundRectangle2D.Double(boardX + boardW - 40, boardY + boardH - 35, 25, 20, 3, 3)); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "SENS", boardX + 27, boardY + boardH - 42, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + StringUtils.drawCenteredText(g2d, "TIME", boardX + boardW - 28, boardY + boardH - 42, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SENSOR_GREEN); + g2d.fill(new RoundRectangle2D.Double(3, 4, width - 6, height - 8, 3, 3)); + g2d.setColor(SENSOR_GREEN.darker()); + g2d.draw(new RoundRectangle2D.Double(3, 4, width - 6, height - 8, 3, 3)); + + // White Dome + g2d.setColor(DOME_COLOR); + g2d.fillOval(width / 2 - 7, height / 2 - 8, 14, 14); + g2d.setColor(Color.GRAY); + g2d.drawOval(width / 2 - 7, height / 2 - 8, 14, 14); + + // 3 Pins + g2d.setColor(PIN_COLOR); + g2d.fillRect(width / 2 - 4, height - 6, 2, 2); + g2d.fillRect(width / 2 - 1, height - 6, 2, 2); + g2d.fillRect(width / 2 + 2, height - 6, 2, 2); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/SoilMoistureSensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/SoilMoistureSensor.java new file mode 100644 index 000000000..142359280 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/SoilMoistureSensor.java @@ -0,0 +1,207 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Soil Moisture Sensor (Capacitive)", category = "Sensors", +// author = "Branislav Stojkovic", description = "Capacitive Corrosion-Resistant Soil Moisture Sensor Module", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class SoilMoistureSensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SOIL_PCB = Color.decode("#17202A"); + public static Color PROBE_TRACE = Color.decode("#273746"); + + public static Size BOARD_WIDTH = new Size(60.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(20.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"VCC", "GND", "AOUT"}; + + public SoilMoistureSensor() { + super(); + this.bodyColor = SOIL_PCB; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 3-pin 0.1" header on handle: VCC, GND, AOUT + double[][] relativeOffsets = new double[][] { + { 0, 0 }, + { 0, spacing }, + { 0, spacing * 2 } + }; + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 15; + double boardY = y - (boardH - 2 * PIN_SPACING.convertToPixels()) / 2.0; + + Path2D path = new Path2D.Double(); + path.moveTo(boardX, boardY); + path.lineTo(boardX + boardW * 0.35, boardY); + path.lineTo(boardX + boardW - 15, boardY + 4); + path.lineTo(boardX + boardW, boardY + boardH / 2.0); + path.lineTo(boardX + boardW - 15, boardY + boardH - 4); + path.lineTo(boardX + boardW * 0.35, boardY + boardH); + path.lineTo(boardX, boardY + boardH); + path.closePath(); + return path; + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - 15; + double boardY = y - (boardH - 2 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Capacitive Sensing Copper Area (Right blade section) + double bladeX = boardX + boardW * 0.38; + double bladeW = boardW * 0.55; + g2d.setColor(PROBE_TRACE); + g2d.fill(new RoundRectangle2D.Double(bladeX, boardY + 12, bladeW, boardH - 24, 6, 6)); + + // White silk dividing line & warning + g2d.setColor(Color.WHITE); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1)); + g2d.drawLine((int)(boardX + boardW * 0.35), (int)boardY + 4, (int)(boardX + boardW * 0.35), (int)(boardY + boardH - 4)); + + // NE555 / TLC555 Oscillator Timer IC (Handle area) + MakerBoardPainter.drawChip(g2d, boardX + 45, boardY + boardH / 2.0 - 14, 28, 28, "555"); + + // 3.3V Voltage Regulator + MakerBoardPainter.drawChip(g2d, boardX + 85, boardY + boardH / 2.0 - 10, 18, 20, ""); + + // Silk Screen Text + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "Capacitive Soil Sensor v1.2", bladeX + bladeW / 2.0, boardY + boardH / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Pin labels + String[] labels = new String[] { "VCC", "GND", "AOUT" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 3; i++) { + StringUtils.drawCenteredText(g2d, labels[i], x + 16, y + i * spacing, HorizontalAlignment.LEFT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw 3 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SOIL_PCB); + Path2D p = new Path2D.Double(); + p.moveTo(2, 6); + p.lineTo(width - 10, 6); + p.lineTo(width - 2, height / 2.0); + p.lineTo(width - 10, height - 6); + p.lineTo(2, height - 6); + p.closePath(); + g2d.fill(p); + g2d.setColor(PROBE_TRACE); + g2d.fillRect(width / 2, 8, width / 2 - 8, height - 16); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/TCRT5000Sensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/TCRT5000Sensor.java new file mode 100644 index 000000000..d6509877d --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/TCRT5000Sensor.java @@ -0,0 +1,199 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Line / Obstacle Sensor (TCRT5000)", category = "Sensors", +// author = "Branislav Stojkovic", description = "TCRT5000 Infrared Line Tracking and Optical Obstacle Avoidance Sensor", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class TCRT5000Sensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SENSOR_BLUE = Color.decode("#1B4F72"); + public static Color POT_BLUE = Color.decode("#0055AA"); + + public static Size BOARD_WIDTH = new Size(32.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(14.0d, SizeUnit.mm); + + private static final String[] PIN_NAMES = {"VCC", "GND", "DO", "AO"}; + + public TCRT5000Sensor() { + super(); + this.bodyColor = SENSOR_BLUE; + updateControlPoints(); + } + + @Override + public String getControlPointNodeName(int index) { + if (index >= 0 && index < PIN_NAMES.length) { + return PIN_NAMES[index]; + } + return Integer.toString(index + 1); + } + + @Override + protected void updateControlPoints() { + Point2D firstPoint = controlPoints[0]; + double spacing = PIN_SPACING.convertToPixels(); + + // 4-pin 0.1" header: VCC, GND, DO, AO + double[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; i++) { + relativeOffsets[i][0] = 0; + relativeOffsets[i][1] = i * spacing; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 3 * PIN_SPACING.convertToPixels()) / 2.0; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 4, 4); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - boardW + 20; + double boardY = y - (boardH - 3 * PIN_SPACING.convertToPixels()) / 2.0; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // TCRT5000 Optical Package (Left Edge) + g2d.setColor(Color.decode("#2C3E50")); + g2d.fill(new RoundRectangle2D.Double(boardX - 10, boardY + boardH / 2.0 - 20, 36, 40, 4, 4)); + // Blue IR LED + g2d.setColor(Color.decode("#3498DB")); + g2d.fill(new Ellipse2D.Double(boardX - 6, boardY + boardH / 2.0 - 15, 14, 14)); + // Black Phototransistor + g2d.setColor(Color.BLACK); + g2d.fill(new Ellipse2D.Double(boardX - 6, boardY + boardH / 2.0 + 1, 14, 14)); + + // LM393 Comparator IC (Center) + MakerBoardPainter.drawChip(g2d, boardX + 45, boardY + boardH / 2.0 - 14, 30, 28, "393"); + + // Sensitivity Trimmer Potentiometer (Right Center) + g2d.setColor(POT_BLUE); + g2d.fill(new RoundRectangle2D.Double(boardX + 90, boardY + boardH / 2.0 - 16, 32, 32, 3, 3)); + g2d.setColor(Color.decode("#BDC3C7")); + g2d.fill(new Ellipse2D.Double(boardX + 96, boardY + boardH / 2.0 - 10, 20, 20)); + g2d.setColor(Color.BLACK); + g2d.drawLine((int)(boardX + 99), (int)(boardY + boardH / 2.0), (int)(boardX + 113), (int)(boardY + boardH / 2.0)); + + // Status LEDs (Power & Signal) + g2d.setColor(Color.RED); + g2d.fill(new RoundRectangle2D.Double(boardX + 135, boardY + 12, 10, 8, 2, 2)); + g2d.setColor(Color.GREEN); + g2d.fill(new RoundRectangle2D.Double(boardX + 135, boardY + boardH - 20, 10, 8, 2, 2)); + + // Silk Screen Pin Labels + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + String[] labels = new String[] { "VCC", "GND", "DO", "AO" }; + double spacing = PIN_SPACING.convertToPixels(); + for (int i = 0; i < 4; i++) { + StringUtils.drawCenteredText(g2d, labels[i], boardX + boardW - 24, y + i * spacing, HorizontalAlignment.RIGHT, VerticalAlignment.CENTER); + } + } + + g2d.setTransform(oldTx); + + // Draw 4 header pins + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SENSOR_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 4, width - 4, height - 8, 3, 3)); + g2d.setColor(Color.CYAN); + g2d.fill(new Ellipse2D.Double(4, height / 2 - 6, 6, 6)); + g2d.setColor(Color.BLACK); + g2d.fill(new Ellipse2D.Double(4, height / 2, 6, 6)); + } +} diff --git a/diylc/diylc-library/src/main/java/org/diylc/components/sensors/UltrasonicSensor.java b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/UltrasonicSensor.java new file mode 100644 index 000000000..cf6b02ad8 --- /dev/null +++ b/diylc/diylc-library/src/main/java/org/diylc/components/sensors/UltrasonicSensor.java @@ -0,0 +1,209 @@ +/* + * + * DIY Layout Creator (DIYLC). + * Copyright (c) 2009-2025 held jointly by the individual authors. + * + * This file is part of DIYLC. + * + * DIYLC is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * DIYLC is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with DIYLC. If not, see . + * + */ +package org.diylc.components.sensors; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Point2D; +import java.awt.geom.RoundRectangle2D; + +import org.diylc.awt.StringUtils; +import org.diylc.common.HorizontalAlignment; +import org.diylc.common.ObjectCache; +import org.diylc.common.Orientation; +import org.diylc.common.VerticalAlignment; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.ComponentState; +import org.diylc.core.IDIYComponent; +import org.diylc.core.IDrawingObserver; +import org.diylc.core.Project; +import org.diylc.core.annotations.BomPolicy; +import org.diylc.core.annotations.ComponentDescriptor; +import org.diylc.core.annotations.KeywordPolicy; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.diylc.utils.Constants; + +// @ComponentDescriptor(name = "Ultrasonic Sensor (HC-SR04)", category = "Sensors", +// author = "Branislav Stojkovic", description = "HC-SR04 Ultrasonic Distance Sensor Module", +// instanceNamePrefix = "SEN", zOrder = IDIYComponent.COMPONENT, +// bomPolicy = BomPolicy.SHOW_ONLY_TYPE_NAME, keywordPolicy = KeywordPolicy.SHOW_TYPE_NAME, +// enableCache = true) +public class UltrasonicSensor extends AbstractMakerBoard { + + private static final long serialVersionUID = 1L; + + public static Color SENSOR_BLUE = Color.decode("#006699"); + public static Size BOARD_WIDTH = new Size(45.0d, SizeUnit.mm); + public static Size BOARD_HEIGHT = new Size(20.0d, SizeUnit.mm); + + public static final String[] PIN_NAMES = new String[] {"VCC", "Trig", "Echo", "GND"}; + + public UltrasonicSensor() { + super(); + this.bodyColor = SENSOR_BLUE; + 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[][] relativeOffsets = new double[4][2]; + for (int i = 0; i < 4; i++) { + relativeOffsets[i][0] = i * spacing; + relativeOffsets[i][1] = 0; + } + + rotatePoints(firstPoint, relativeOffsets); + } + + @Override + public Shape getBodyShape() { + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + return new RoundRectangle2D.Double(boardX, boardY, boardW, boardH, 8, 8); + } + + @Override + public void draw(Graphics2D g2d, ComponentState componentState, boolean outlineMode, Project project, + IDrawingObserver drawingObserver) { + if (checkPointsClipped(g2d.getClip())) { + return; + } + + Point2D p0 = controlPoints[0]; + double x = p0.getX(); + double y = p0.getY(); + + AffineTransform oldTx = g2d.getTransform(); + if (orientation != Orientation.DEFAULT) { + g2d.rotate(orientation.toRadians(), x, y); + } + + double boardW = BOARD_WIDTH.convertToPixels(); + double boardH = BOARD_HEIGHT.convertToPixels(); + double boardX = x - (boardW - 3 * PIN_SPACING.convertToPixels()) / 2.0; + double boardY = y - boardH + 12; + + Shape boardShape = getBodyShape(); + + Composite oldComposite = applyAlpha(g2d, componentState); + + drawingObserver.startTracking(); + g2d.setColor(outlineMode ? Constants.TRANSPARENT_COLOR : bodyColor); + g2d.fill(boardShape); + drawingObserver.stopTracking(); + + g2d.setColor(getFinalBorderColor(componentState, outlineMode)); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(boardShape); + + if (!outlineMode) { + // Two cylindrical silver transducers (16mm diameter ~ 126px) + double cylD = new Size(16.0d, SizeUnit.mm).convertToPixels(); + double cyl1X = boardX + 25; + double cyl2X = boardX + boardW - 25 - cylD; + double cylY = boardY + (boardH - cylD) / 2.0 - 5; + + // Transducer 1 (T - Transmitter) + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fill(new Ellipse2D.Double(cyl1X, cylY, cylD, cylD)); + g2d.setColor(METAL_SHIELD_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(cyl1X, cylY, cylD, cylD)); + // Inner mesh ring + g2d.setColor(Color.decode("#999999")); + g2d.draw(new Ellipse2D.Double(cyl1X + 15, cylY + 15, cylD - 30, cylD - 30)); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_LARGE); + StringUtils.drawCenteredText(g2d, "T", cyl1X + cylD / 2.0, cylY + cylD / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Transducer 2 (R - Receiver) + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fill(new Ellipse2D.Double(cyl2X, cylY, cylD, cylD)); + g2d.setColor(METAL_SHIELD_BORDER); + g2d.setStroke(ObjectCache.getInstance().fetchBasicStroke(1.5f)); + g2d.draw(new Ellipse2D.Double(cyl2X, cylY, cylD, cylD)); + g2d.setColor(Color.decode("#999999")); + g2d.draw(new Ellipse2D.Double(cyl2X + 15, cylY + 15, cylD - 30, cylD - 30)); + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_LARGE); + StringUtils.drawCenteredText(g2d, "R", cyl2X + cylD / 2.0, cylY + cylD / 2.0, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + + // Center crystal oscillator & text + MakerBoardPainter.drawMetalConnector(g2d, boardX + boardW / 2.0 - 20, boardY + 25, 40, 20, "4MHz"); + + g2d.setColor(Color.WHITE); + g2d.setFont(SILK_FONT_SMALL); + StringUtils.drawCenteredText(g2d, "HC-SR04", boardX + boardW / 2.0, boardY + boardH - 22, HorizontalAlignment.CENTER, VerticalAlignment.CENTER); + } + + g2d.setTransform(oldTx); + + drawPinHeader(g2d, 0, controlPoints.length, outlineMode, drawingObserver); + + g2d.setComposite(oldComposite); + } + + @Override + public void drawIcon(Graphics2D g2d, int width, int height) { + g2d.setColor(SENSOR_BLUE); + g2d.fill(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + g2d.setColor(SENSOR_BLUE.darker()); + g2d.draw(new RoundRectangle2D.Double(2, 6, width - 4, height - 12, 3, 3)); + + // Two transducer circles + g2d.setColor(METAL_SHIELD_COLOR); + g2d.fillOval(5, 9, 9, 9); + g2d.fillOval(width - 14, 9, 9, 9); + g2d.setColor(Color.GRAY); + g2d.drawOval(5, 9, 9, 9); + g2d.drawOval(width - 14, 9, 9, 9); + + // 4 pins at bottom + g2d.setColor(PIN_COLOR); + for (int i = 0; i < 4; i++) { + g2d.fillRect(10 + i * 3, height - 7, 2, 2); + } + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java new file mode 100644 index 000000000..e6d574f50 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/maker/MakerComponentsTest.java @@ -0,0 +1,259 @@ +/* + * + * 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.maker; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; +import java.awt.image.BufferedImage; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.diylc.common.ComponentType; +import org.diylc.common.DefaultComponentTransformer; +import org.diylc.common.Orientation; +import org.diylc.components.displays.*; +import org.diylc.components.electromechanical.*; +import org.diylc.components.micro.*; +import org.diylc.components.modules.*; +import org.diylc.components.robotics.*; +import org.diylc.components.sensors.*; +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; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.diylc.appframework.miscutils.ConfigurationManager; + +public class MakerComponentsTest { + + @BeforeClass + public static void setUp() { + try { + ConfigurationManager.getInstance().initialize("diylc"); + } catch (Exception ignored) { + } + } + + private final List>> releasedMakerComponentClasses = Arrays.>>asList( + // Controllers + ArduinoUno.class, + ArduinoNano.class, + ArduinoMega.class, + RaspberryPi.class, + RaspberryPiZero.class, + RaspberryPiPico.class, + ESP32DevKit.class, + ESP8266NodeMCU.class, + WemosD1Mini.class, + + // Electro-Mechanical + BatteryHolder18650.class + ); + + // Staged for a later release: their @ComponentDescriptor annotations are commented out, so + // ComponentProcessor does not discover them and they have no ComponentType yet. + private final List>> unreleasedMakerComponentClasses = Arrays.>>asList( + // Sensors + UltrasonicSensor.class, + PIRMotionSensor.class, + DHTSensor.class, + MPU6050.class, + LDRSensorModule.class, + BME280Sensor.class, + SoilMoistureSensor.class, + TCRT5000Sensor.class, + IRReceiverModule.class, + + // Displays & Outputs + CharacterLCD.class, + OLEDDisplay.class, + SevenSegmentDisplay.class, + LEDMatrix8x8.class, + TFTDisplay.class, + WS2812BStick.class, + WS2812BRing.class, + + // Modules & Breakouts + LogicLevelConverter.class, + L298NMotorDriver.class, + A4988StepperDriver.class, + RelayModule.class, + SDCardModule.class, + RTCModule.class, + MB102PowerSupply.class, + TP4056Charger.class, + LM2596BuckConverter.class, + MT3608BoostConverter.class, + AnalogJoystickKY023.class, + RotaryEncoderKY040.class, + NRF24L01Transceiver.class, + HC05Bluetooth.class, + RFIDRC522.class, + ULN2003Driver.class, + PCA9685ServoDriver.class, + MOSFETSwitchModule.class, + ActiveBuzzerModule.class, + GPSModuleNEO6M.class, + INA219CurrentSensor.class, + + // Robotics + MicroServoSG90.class, + NEMA17Stepper.class, + StepperMotor28BYJ48.class, + DCHobbyMotor.class + ); + + private final List>> allMakerComponentClasses = + Stream.concat(releasedMakerComponentClasses.stream(), unreleasedMakerComponentClasses.stream()) + .collect(Collectors.toList()); + + @Test + public void testAllComponentDiscoveryAndCategories() { + Map> categories = ComponentProcessor.getInstance().getComponentTypes(); + + Assert.assertTrue("Categories should contain 'Controllers'", categories.containsKey("Controllers")); + + for (Class> clazz : releasedMakerComponentClasses) { + ComponentType type = ComponentProcessor.getInstance().extractComponentTypeFrom(clazz); + Assert.assertNotNull("Component type should be extracted for " + clazz.getSimpleName(), type); + Assert.assertNotNull("Icon should be non-null for " + clazz.getSimpleName(), type.getIcon()); + Assert.assertNotNull("Category should be non-null for " + clazz.getSimpleName(), type.getCategory()); + Assert.assertNotNull("Name should be non-null for " + clazz.getSimpleName(), type.getName()); + } + } + + @Test + public void testComponentInstantiationAndControlPoints() throws Exception { + for (Class> clazz : allMakerComponentClasses) { + IDIYComponent component = clazz.getDeclaredConstructor().newInstance(); + Assert.assertNotNull("Component instance must not be null", component); + int count = component.getControlPointCount(); + Assert.assertTrue("Component " + clazz.getSimpleName() + " must have at least 1 control point", count > 0); + + for (int i = 0; i < count; i++) { + Point2D p = component.getControlPoint(i); + Assert.assertNotNull("Control point " + i + " must not be null for " + clazz.getSimpleName(), p); + String name = component.getControlPointNodeName(i); + Assert.assertNotNull("Node name for point " + i + " must not be null for " + clazz.getSimpleName(), name); + Assert.assertFalse("Node name for point " + i + " must not be empty for " + clazz.getSimpleName(), name.trim().isEmpty()); + } + } + } + + @Test + public void testComponentDrawingAndOutlineMode() throws Exception { + 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(); + IDrawingObserver observer = new IDrawingObserver() { + @Override public void startTracking() {} + @Override public void stopTracking() {} + @Override public void startTrackingContinuityArea(boolean positive) {} + @Override public void stopTrackingContinuityArea() {} + @Override public boolean isTrackingContinuityArea() { return false; } + @Override public void setContinuityMarker(String marker) {} + }; + + for (Class> clazz : allMakerComponentClasses) { + IDIYComponent component = clazz.getDeclaredConstructor().newInstance(); + component.setControlPoint(new Point2D.Double(200, 200), 0); + + // Normal mode + component.draw(g2d, ComponentState.NORMAL, false, project, observer); + // Selected mode + component.draw(g2d, ComponentState.SELECTED, false, project, observer); + // Outline mode + component.draw(g2d, ComponentState.NORMAL, true, project, observer); + // Icon drawing + component.drawIcon(g2d, 32, 32); + } + g2d.dispose(); + } + + @Test + public void testCachingBounds() throws Exception { + for (Class> clazz : allMakerComponentClasses) { + IDIYComponent component = clazz.getDeclaredConstructor().newInstance(); + component.setControlPoint(new Point2D.Double(200, 200), 0); + + java.awt.geom.Rectangle2D bounds = component.getCachingBounds(); + Assert.assertNotNull("Caching bounds must not be null for " + clazz.getSimpleName(), bounds); + Assert.assertTrue("Caching bounds width must be > 0 for " + clazz.getSimpleName(), bounds.getWidth() > 0); + Assert.assertTrue("Caching bounds height must be > 0 for " + clazz.getSimpleName(), bounds.getHeight() > 0); + } + } + + @Test + public void testComponentRotation() throws Exception { + DefaultComponentTransformer transformer = new DefaultComponentTransformer(); + Point2D center = new Point2D.Double(200, 200); + + for (Class> clazz : allMakerComponentClasses) { + IDIYComponent component = clazz.getDeclaredConstructor().newInstance(); + component.setControlPoint(new Point2D.Double(200, 200), 0); + + Assert.assertTrue("Component " + clazz.getSimpleName() + " should be rotatable", transformer.canRotate(component)); + + // Rotate 4 times clockwise (360 degrees back to original) + for (int r = 0; r < 4; r++) { + transformer.rotate(component, center, 1); + } + } + } + + @Test + public void testCanPointMoveFreelyReturnsFalse() throws Exception { + for (Class> clazz : allMakerComponentClasses) { + IDIYComponent component = clazz.getDeclaredConstructor().newInstance(); + int count = component.getControlPointCount(); + for (int i = 0; i < count; i++) { + Assert.assertFalse("canPointMoveFreely(" + i + ") must return false for " + clazz.getSimpleName(), + component.canPointMoveFreely(i)); + } + } + } + + @Test + public void testControlPointsAreDistinct() throws Exception { + for (Class> clazz : allMakerComponentClasses) { + IDIYComponent component = clazz.getDeclaredConstructor().newInstance(); + int count = component.getControlPointCount(); + for (int i = 0; i < count; i++) { + Point2D p1 = component.getControlPoint(i); + for (int j = i + 1; j < count; j++) { + Point2D p2 = component.getControlPoint(j); + Assert.assertTrue("Duplicate control point coordinates detected between point " + i + " and point " + j + " in " + clazz.getSimpleName(), + p1.distance(p2) > 0.001); + } + } + } + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/maker/UsbPortTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/maker/UsbPortTest.java new file mode 100644 index 000000000..1ccd7891b --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/maker/UsbPortTest.java @@ -0,0 +1,85 @@ +package org.diylc.components.maker; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.AbstractMakerBoard.UsbPortType; +import org.diylc.components.MakerBoardPainter; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class UsbPortTest { + + @Test + public void testUsbConstants() { + // Micro-USB + Assert.assertEquals(7.5d, AbstractMakerBoard.USB_MICRO_WIDTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_MICRO_WIDTH.getUnit()); + Assert.assertEquals(5.6d, AbstractMakerBoard.USB_MICRO_LENGTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_MICRO_LENGTH.getUnit()); + Assert.assertEquals(0.5d, AbstractMakerBoard.USB_MICRO_OVERHANG.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_MICRO_OVERHANG.getUnit()); + + // USB Type-C + Assert.assertEquals(8.94d, AbstractMakerBoard.USB_C_WIDTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_C_WIDTH.getUnit()); + Assert.assertEquals(7.5d, AbstractMakerBoard.USB_C_LENGTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_C_LENGTH.getUnit()); + Assert.assertEquals(0.5d, AbstractMakerBoard.USB_C_OVERHANG.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_C_OVERHANG.getUnit()); + + // USB Type-A + Assert.assertEquals(14.5d, AbstractMakerBoard.USB_A_WIDTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_A_WIDTH.getUnit()); + Assert.assertEquals(14.0d, AbstractMakerBoard.USB_A_LENGTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_A_LENGTH.getUnit()); + Assert.assertEquals(17.5d, AbstractMakerBoard.USB_A_DUAL_LENGTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.mm, AbstractMakerBoard.USB_A_DUAL_LENGTH.getUnit()); + + // USB Type-B + Assert.assertEquals(0.45d, AbstractMakerBoard.USB_B_WIDTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.in, AbstractMakerBoard.USB_B_WIDTH.getUnit()); + Assert.assertEquals(0.51d, AbstractMakerBoard.USB_B_LENGTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.in, AbstractMakerBoard.USB_B_LENGTH.getUnit()); + Assert.assertEquals(0.14d, AbstractMakerBoard.USB_B_OVERHANG.getValue(), 0.001); + Assert.assertEquals(SizeUnit.in, AbstractMakerBoard.USB_B_OVERHANG.getUnit()); + + // Mini-USB + Assert.assertEquals(0.30d, AbstractMakerBoard.USB_MINI_WIDTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.in, AbstractMakerBoard.USB_MINI_WIDTH.getUnit()); + Assert.assertEquals(0.36d, AbstractMakerBoard.USB_MINI_LENGTH.getValue(), 0.001); + Assert.assertEquals(SizeUnit.in, AbstractMakerBoard.USB_MINI_LENGTH.getUnit()); + Assert.assertEquals(0.05d, AbstractMakerBoard.USB_MINI_OVERHANG.getValue(), 0.001); + Assert.assertEquals(SizeUnit.in, AbstractMakerBoard.USB_MINI_OVERHANG.getUnit()); + } + + @Test + public void testUsbPortTypeEnum() { + Assert.assertEquals(5, UsbPortType.values().length); + Assert.assertNotNull(UsbPortType.valueOf("MICRO")); + Assert.assertNotNull(UsbPortType.valueOf("TYPE_C")); + Assert.assertNotNull(UsbPortType.valueOf("TYPE_A")); + Assert.assertNotNull(UsbPortType.valueOf("TYPE_B")); + Assert.assertNotNull(UsbPortType.valueOf("MINI")); + } + + @Test + public void testUsbDrawingMethods() { + BufferedImage img = new BufferedImage(400, 400, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = img.createGraphics(); + + MakerBoardPainter.drawMicroUsb(g2d, 10, 200, 60, 40, "USB"); + MakerBoardPainter.drawMicroUsb(g2d, 100, 200, 40, 60, "USB"); + MakerBoardPainter.drawUsbC(g2d, 10, 260, 60, 40, "Type-C"); + MakerBoardPainter.drawUsbC(g2d, 100, 260, 40, 60, "Type-C"); + MakerBoardPainter.drawUsbA(g2d, 10, 320, 60, 40, "USB 3.0"); + MakerBoardPainter.drawUsbA(g2d, 100, 320, 40, 60, "USB 2.0"); + MakerBoardPainter.drawUsbB(g2d, 200, 10, 60, 40, "USB"); + MakerBoardPainter.drawMetalConnector(g2d, 200, 150, 60, 40, "SHIELD"); + MakerBoardPainter.drawMetalConnector(g2d, 200, 220, 60, 40, null); + MakerBoardPainter.drawMetalConnector(g2d, 250, 220, 60, 40, ""); + + g2d.dispose(); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java new file mode 100644 index 000000000..3eca17e65 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoMegaTest.java @@ -0,0 +1,110 @@ +/* + * + * 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 java.awt.geom.Point2D; + +import org.junit.Assert; +import org.junit.Test; + +public class ArduinoMegaTest { + + @Test + public void testPinout() { + ArduinoMega mega = new ArduinoMega(); + Assert.assertEquals(98, mega.getControlPointCount()); + + // power, analog low, analog high, digital low, digital high, communication, the 2x18 block, + // then the two 2x3 ICSP blocks + Assert.assertEquals("NC", mega.getControlPointNodeName(0)); + Assert.assertEquals("VIN", mega.getControlPointNodeName(7)); + Assert.assertEquals("A0", mega.getControlPointNodeName(8)); + Assert.assertEquals("A7", mega.getControlPointNodeName(15)); + Assert.assertEquals("A8", mega.getControlPointNodeName(16)); + Assert.assertEquals("A15", mega.getControlPointNodeName(23)); + Assert.assertEquals("D0 (RX0)", mega.getControlPointNodeName(24)); + Assert.assertEquals("D7 (~)", mega.getControlPointNodeName(31)); + Assert.assertEquals("D8 (~)", mega.getControlPointNodeName(32)); + Assert.assertEquals("SCL", mega.getControlPointNodeName(41)); + Assert.assertEquals("D14 (TX3)", mega.getControlPointNodeName(42)); + Assert.assertEquals("D21 (SCL)", mega.getControlPointNodeName(49)); + Assert.assertEquals("D22", mega.getControlPointNodeName(50)); + Assert.assertEquals("D53 (SS)", mega.getControlPointNodeName(81)); + Assert.assertEquals("GND_EXT1", mega.getControlPointNodeName(82)); + Assert.assertEquals("5V_EXT2", mega.getControlPointNodeName(85)); + Assert.assertEquals("MISO", mega.getControlPointNodeName(86)); + Assert.assertEquals("GND_ICSP", mega.getControlPointNodeName(91)); + Assert.assertEquals("MISO_16U2", mega.getControlPointNodeName(92)); + Assert.assertEquals("GND_16U2", mega.getControlPointNodeName(97)); + + // the silkscreen prints what is on the board rather than the node name: every ground says + // "GND", the reserved first pin says nothing, and the 2x18 block's supply rows are printed + // once for the pair, on the outer column + Assert.assertEquals("GND", mega.getSilkPinLabel(5)); + Assert.assertEquals("GND", mega.getSilkPinLabel(6)); + Assert.assertEquals("", mega.getSilkPinLabel(0)); + Assert.assertEquals("", mega.getSilkPinLabel(82)); + Assert.assertEquals("GND", mega.getSilkPinLabel(83)); + Assert.assertEquals("", mega.getSilkPinLabel(84)); + Assert.assertEquals("5V", mega.getSilkPinLabel(85)); + } + + @Test + public void testShieldFootprintAndDoubleDigitalBlock() { + ArduinoMega mega = new ArduinoMega(); + double spacing = MakerBoardTestSupport.PIN_SPACING; + + MakerBoardTestSupport.assertRow(mega, 0, 7); + MakerBoardTestSupport.assertRow(mega, 8, 15); + MakerBoardTestSupport.assertRow(mega, 16, 23); + MakerBoardTestSupport.assertRow(mega, 24, 31); + MakerBoardTestSupport.assertRow(mega, 32, 41); + MakerBoardTestSupport.assertRow(mega, 42, 49); + + // the Mega keeps the Uno's shield geometry: 0.2" between the power and analog headers and the + // 0.16" offset between the two digital headers + Assert.assertEquals("Gap between VIN and A0", 2 * spacing, + mega.getControlPoint(7).distance(mega.getControlPoint(8)), 0.01); + Assert.assertEquals("Offset between D7 and D8", 32.0, + mega.getControlPoint(31).distance(mega.getControlPoint(32)), 0.01); + + // the 2x18 block is powered from its top row, level with the digital header, and grounded at + // the bottom one, with D22..D53 filling the sixteen rows in between; even numbers run down the + // inner column and odd ones down the outer, power and ground included + Point2D d0 = mega.getControlPoint(24); + Point2D fiveVoltInner = mega.getControlPoint(84); + Point2D fiveVoltOuter = mega.getControlPoint(85); + Point2D d22 = mega.getControlPoint(50); + Point2D groundInner = mega.getControlPoint(82); + Point2D groundOuter = mega.getControlPoint(83); + + Assert.assertEquals(d0.getY(), fiveVoltInner.getY(), 0.01); + Assert.assertEquals(d0.getY(), fiveVoltOuter.getY(), 0.01); + Assert.assertEquals(spacing, d22.getY() - fiveVoltInner.getY(), 0.01); + Assert.assertEquals(17 * spacing, groundInner.getY() - fiveVoltInner.getY(), 0.01); + Assert.assertEquals(groundInner.getY(), groundOuter.getY(), 0.01); + Assert.assertEquals(fiveVoltInner.getX(), d22.getX(), 0.01); + Assert.assertEquals(fiveVoltInner.getX(), groundInner.getX(), 0.01); + Assert.assertEquals(fiveVoltOuter.getX(), groundOuter.getX(), 0.01); + Assert.assertEquals(spacing, fiveVoltOuter.getX() - fiveVoltInner.getX(), 0.01); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java new file mode 100644 index 000000000..b5bfd1649 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoNanoTest.java @@ -0,0 +1,131 @@ +/* + * + * 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.components.micro.ArduinoNano.NanoVersion; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class ArduinoNanoTest { + + @Test + public void testPinoutPerVersion() { + ArduinoNano classic = new ArduinoNano(); + Assert.assertEquals(NanoVersion.CLASSIC, classic.getVersion()); + Assert.assertEquals(36, classic.getControlPointCount()); + + // JP1 down the left, JP2 down the right, then the 2x3 ICSP block + Assert.assertEquals("D1 (TX)", classic.getControlPointNodeName(0)); + Assert.assertEquals("D12", classic.getControlPointNodeName(14)); + Assert.assertEquals("VIN", classic.getControlPointNodeName(15)); + Assert.assertEquals("RST2", classic.getControlPointNodeName(17)); + Assert.assertEquals("5V", classic.getControlPointNodeName(18)); + Assert.assertEquals("AREF", classic.getControlPointNodeName(27)); + Assert.assertEquals("D13", classic.getControlPointNodeName(29)); + 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", classic.getSilkPinLabel(3)); + Assert.assertEquals("GND", classic.getSilkPinLabel(16)); + Assert.assertEquals("RST", classic.getSilkPinLabel(2)); + Assert.assertEquals("RST", classic.getSilkPinLabel(17)); + + // the 2x3 block belongs to the classic alone; the later boards use that end of the PCB for the + // radio module or the USB bridge, and the R4 adds a point at the centre of its Qwiic socket + for (NanoVersion version : NanoVersion.values()) { + Assert.assertEquals(version + " pin count", expectedPinCount(version), + version(version).getControlPointCount()); + } + + // the classic leaves the bus functions off A4 / A5, every later board annotates them, and the + // Nano ESP32 carries the Espressif GPIO number as well + Assert.assertEquals("A5", classic.getControlPointNodeName(21)); + Assert.assertEquals("A5 (SCL)", version(NanoVersion.EVERY).getControlPointNodeName(21)); + Assert.assertEquals("A4 (~, SDA/GPIO11)", + version(NanoVersion.NANO_ESP32).getControlPointNodeName(22)); + + // the pad the classic repeats RESET on carries the boot mode selector on the later boards + Assert.assertEquals("REC", version(NanoVersion.NANO_RP2040_CONNECT).getSilkPinLabel(17)); + Assert.assertEquals("BOOT", version(NanoVersion.NANO_R4).getSilkPinLabel(17)); + Assert.assertEquals("B1", version(NanoVersion.NANO_ESP32).getSilkPinLabel(17)); + Assert.assertEquals("B0", version(NanoVersion.NANO_ESP32).getSilkPinLabel(27)); + + // the R4 reverses JP1 and doubles D4 / D5 as the CAN pins + ArduinoNano r4 = version(NanoVersion.NANO_R4); + Assert.assertEquals("D4 (CAN TX)", r4.getControlPointNodeName(6)); + Assert.assertEquals("A0 (DAC)", r4.getControlPointNodeName(26)); + Assert.assertEquals("QWIIC", r4.getControlPointNodeName(30)); + } + + @Test + public void testGeometryIsVersionIndependent() { + ArduinoNano classic = new ArduinoNano(); + + MakerBoardTestSupport.assertRow(classic, 0, 14); + MakerBoardTestSupport.assertRow(classic, 15, 29); + MakerBoardTestSupport.assertRowSpacing(classic, 0, 15, new Size(0.60d, SizeUnit.in)); + MakerBoardTestSupport.assertBoardSize(classic, new Size(0.73d, SizeUnit.in), + new Size(1.70d, SizeUnit.in)); + Assert.assertEquals("Margin above the first pin", + new Size(0.15d, SizeUnit.in).convertToPixels(), + classic.getControlPoint(0).getY() - classic.getBodyShape().getBounds2D().getY(), 0.1); + + // every Nano keeps the same outline and the same 2x15 pin field; anything past it is + // board-specific + for (NanoVersion version : NanoVersion.values()) { + MakerBoardTestSupport.assertSharedFootprint(classic, version(version), 30); + } + } + + @Test + public void testEveryVersionDraws() { + for (NanoVersion version : NanoVersion.values()) { + ArduinoNano nano = version(version); + for (boolean headers : new boolean[] {false, true}) { + nano.setHeaders(headers); + MakerBoardTestSupport.assertDrawsCleanly(nano); + } + } + } + + private static ArduinoNano version(NanoVersion version) { + ArduinoNano nano = new ArduinoNano(); + nano.setVersion(version); + return nano; + } + + /** 36 for the classic with its ICSP block, 31 for the R4 with its Qwiic point, 30 otherwise. */ + private static int expectedPinCount(NanoVersion version) { + switch (version) { + case CLASSIC: + return 36; + case NANO_R4: + return 31; + default: + return 30; + } + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java new file mode 100644 index 000000000..7ee3c6a3d --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ArduinoUnoTest.java @@ -0,0 +1,127 @@ +/* + * + * 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 java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.components.micro.ArduinoUno.ArduinoUnoVersion; +import org.junit.Assert; +import org.junit.Test; + +public class ArduinoUnoTest { + + @Test + public void testPinoutPerVersion() { + ArduinoUno r3 = new ArduinoUno(); + Assert.assertEquals(ArduinoUnoVersion.REV3, r3.getVersion()); + Assert.assertEquals(44, r3.getControlPointCount()); + + // power, analog, digital low, digital high, main ICSP, then the 16U2 ICSP block + Assert.assertEquals("NC", r3.getControlPointNodeName(0)); + Assert.assertEquals("VIN", r3.getControlPointNodeName(7)); + Assert.assertEquals("A0", r3.getControlPointNodeName(8)); + Assert.assertEquals("A5", r3.getControlPointNodeName(13)); + Assert.assertEquals("D0 (RX)", r3.getControlPointNodeName(14)); + Assert.assertEquals("D7", r3.getControlPointNodeName(21)); + Assert.assertEquals("D8", r3.getControlPointNodeName(22)); + Assert.assertEquals("SCL", r3.getControlPointNodeName(31)); + Assert.assertEquals("MISO", r3.getControlPointNodeName(32)); + Assert.assertEquals("GND_ICSP", r3.getControlPointNodeName(37)); + Assert.assertEquals("MISO_16U2", r3.getControlPointNodeName(38)); + Assert.assertEquals("GND_16U2", r3.getControlPointNodeName(43)); + + // 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", r3.getSilkPinLabel(5)); + Assert.assertEquals("GND", r3.getSilkPinLabel(6)); + Assert.assertEquals("", r3.getSilkPinLabel(0)); + + // the Leonardo has no ATmega16U2, so its second 2x3 block is gone, and I2C moves to D2 / D3 + ArduinoUno leonardo = version(ArduinoUnoVersion.LEONARDO); + Assert.assertEquals(38, leonardo.getControlPointCount()); + Assert.assertEquals("NC", leonardo.getControlPointNodeName(0)); + Assert.assertEquals("D2 (SDA)", leonardo.getControlPointNodeName(16)); + Assert.assertEquals("D3 (~, SCL)", leonardo.getControlPointNodeName(17)); + + // both R4 boards use the R3's reserved power header pin for boot mode selection; the WiFi adds + // OFF / GND / VRTC on top of the 44, while the Minima drops the second ICSP block for a single + // SWD connector: 44 - 6 + 1 = 39 + ArduinoUno wifi = version(ArduinoUnoVersion.R4_WIFI); + ArduinoUno minima = version(ArduinoUnoVersion.R4_MINIMA); + Assert.assertEquals(47, wifi.getControlPointCount()); + Assert.assertEquals(39, minima.getControlPointCount()); + Assert.assertEquals("BOOT", wifi.getControlPointNodeName(0)); + Assert.assertEquals("BOOT", minima.getControlPointNodeName(0)); + Assert.assertEquals("OFF", wifi.getControlPointNodeName(44)); + Assert.assertEquals("VRTC", wifi.getSilkPinLabel(46)); + Assert.assertEquals("SWD", minima.getControlPointNodeName(38)); + } + + @Test + public void testShieldFootprint() { + ArduinoUno r3 = new ArduinoUno(); + double spacing = MakerBoardTestSupport.PIN_SPACING; + + MakerBoardTestSupport.assertRow(r3, 0, 7); + MakerBoardTestSupport.assertRow(r3, 8, 13); + MakerBoardTestSupport.assertRow(r3, 14, 21); + MakerBoardTestSupport.assertRow(r3, 22, 31); + + // the two gaps the shield standard fixes: 0.2" between the power and analog headers, and the + // notorious 0.16" offset between the two digital headers + Assert.assertEquals("Gap between VIN and A0", 2 * spacing, + r3.getControlPoint(7).distance(r3.getControlPoint(8)), 0.01); + Assert.assertEquals("Offset between D7 and D8", 32.0, + r3.getControlPoint(21).distance(r3.getControlPoint(22)), 0.01); + + // every version drops onto the same shield, so the 38 header pins they share never move + for (ArduinoUnoVersion version : ArduinoUnoVersion.values()) { + MakerBoardTestSupport.assertSharedFootprint(r3, version(version), 38); + } + + // the Minima's SWD connector sits on the board, in the top right quadrant below the digital + // header where the second ICSP block used to be + ArduinoUno minima = version(ArduinoUnoVersion.R4_MINIMA); + Point2D swd = minima.getControlPoint(38); + Rectangle2D bounds = minima.getBodyShape().getBounds2D(); + Assert.assertTrue("SWD should be on the board", bounds.contains(swd)); + Assert.assertTrue("SWD should be on the right half", swd.getX() > bounds.getCenterX()); + Assert.assertTrue("SWD should be below the digital header", + swd.getY() > minima.getControlPoint(14).getY() && swd.getY() < bounds.getCenterY()); + } + + @Test + public void testEveryVersionDraws() { + for (ArduinoUnoVersion version : ArduinoUnoVersion.values()) { + MakerBoardTestSupport.assertDrawsCleanly(version(version)); + } + } + + private static ArduinoUno version(ArduinoUnoVersion version) { + ArduinoUno uno = new ArduinoUno(); + uno.setVersion(version); + return uno; + } +} 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 000000000..66f3429ce --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP32DevKitTest.java @@ -0,0 +1,154 @@ +/* + * + * 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 java.awt.geom.Rectangle2D; + +import org.diylc.components.AbstractMakerBoard; +import org.diylc.components.micro.ESP32DevKit.DevKitVersion; +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 testPinoutPerVersion() { + ESP32DevKit devKit = new ESP32DevKit(); + Assert.assertEquals(DevKitVersion.DevKit_V1_30Pin, devKit.getVersion()); + Assert.assertEquals(30, devKit.getControlPointCount()); + Assert.assertEquals(38, version(DevKitVersion.DevKitC_V4_38Pin).getControlPointCount()); + Assert.assertEquals(44, version(DevKitVersion.ESP32_S3_DevKitC_44Pin).getControlPointCount()); + Assert.assertEquals(30, version(DevKitVersion.ESP32_C3_DevKitM_1).getControlPointCount()); + Assert.assertEquals(32, version(DevKitVersion.ESP32_C6_DevKitC_1).getControlPointCount()); + + // the silkscreen carries the bare board label while the node name carries the GPIO mapping, + // which on these boards is the thing people most often get wrong + Assert.assertEquals("EN", devKit.getSilkPinLabel(0)); + Assert.assertEquals("D34", devKit.getSilkPinLabel(3)); + Assert.assertEquals("GPIO34 (ADC1_CH6, VDET_1)", devKit.getControlPointNodeName(3)); + Assert.assertEquals("TX0 (GPIO1, U0TXD)", devKit.getControlPointNodeName(17)); + Assert.assertEquals("VIN", devKit.getControlPointNodeName(14)); + Assert.assertEquals("3V3", devKit.getControlPointNodeName(29)); + + // the two grounds of the 30-pin board are one net apiece, so they cannot share a node name + Assert.assertEquals("GND_1", devKit.getControlPointNodeName(13)); + Assert.assertEquals("GND_2", devKit.getControlPointNodeName(28)); + Assert.assertEquals("GND", devKit.getSilkPinLabel(13)); + Assert.assertEquals("GND", devKit.getSilkPinLabel(28)); + + // the row ends of the remaining versions, taken from the Espressif user guides' header tables + ESP32DevKit devKitC = version(DevKitVersion.DevKitC_V4_38Pin); + Assert.assertEquals("3V3", devKitC.getControlPointNodeName(0)); + Assert.assertEquals("5V", devKitC.getSilkPinLabel(18)); + Assert.assertEquals("D0 (GPIO7, flash)", devKitC.getControlPointNodeName(36)); + Assert.assertEquals("CLK", devKitC.getSilkPinLabel(37)); + + ESP32DevKit s3 = version(DevKitVersion.ESP32_S3_DevKitC_44Pin); + Assert.assertEquals("3V3_1", s3.getControlPointNodeName(0)); + Assert.assertEquals("3V3", s3.getSilkPinLabel(0)); + Assert.assertEquals("GND_1", s3.getControlPointNodeName(21)); + Assert.assertEquals("GPIO20 (U1CTS, ADC2_CH9, USB_D+)", s3.getControlPointNodeName(40)); + Assert.assertEquals("GND_4", s3.getControlPointNodeName(43)); + + ESP32DevKit c3 = version(DevKitVersion.ESP32_C3_DevKitM_1); + Assert.assertEquals("GND_1", c3.getControlPointNodeName(0)); + Assert.assertEquals("GPIO8 (RGB LED)", c3.getControlPointNodeName(20)); + Assert.assertEquals("GND_10", c3.getControlPointNodeName(29)); + + ESP32DevKit c6 = version(DevKitVersion.ESP32_C6_DevKitC_1); + Assert.assertEquals("3V3", c6.getControlPointNodeName(0)); + Assert.assertEquals("GPIO13 (USB_D+)", c6.getControlPointNodeName(28)); + Assert.assertEquals("NC_2", c6.getControlPointNodeName(31)); + } + + @Test + public void testGeometryPerVersion() { + // the classic DevKits are 1.0" wide over the pin rows, the RISC-V and S3 boards 0.9" + Size classicRowSpacing = new Size(1.0d, SizeUnit.in); + Size narrowRowSpacing = new Size(22.86d, SizeUnit.mm); + assertTwoRows(new ESP32DevKit(), classicRowSpacing); + assertTwoRows(version(DevKitVersion.DevKitC_V4_38Pin), classicRowSpacing); + assertTwoRows(version(DevKitVersion.ESP32_S3_DevKitC_44Pin), narrowRowSpacing); + assertTwoRows(version(DevKitVersion.ESP32_C3_DevKitM_1), narrowRowSpacing); + assertTwoRows(version(DevKitVersion.ESP32_C6_DevKitC_1), narrowRowSpacing); + + MakerBoardTestSupport.assertBoardSize(new ESP32DevKit(), new Size(28.33d, SizeUnit.mm), + new Size(51.45d, SizeUnit.mm)); + MakerBoardTestSupport.assertBoardSize(version(DevKitVersion.DevKitC_V4_38Pin), + new Size(27.9d, SizeUnit.mm), new Size(54.4d, SizeUnit.mm)); + + // the S3's antenna sticks out past the end of the board + ESP32DevKit s3 = version(DevKitVersion.ESP32_S3_DevKitC_44Pin); + Assert.assertEquals("S3 outline including the antenna", + new Size(62.74d + 6.0d, SizeUnit.mm).convertToPixels(), + s3.getBodyShape().getBounds2D().getHeight(), 1.0); + + // the RISC-V boards share the DevKitC form factor and hang their pin field centred on it + for (DevKitVersion version : new DevKitVersion[] {DevKitVersion.ESP32_C3_DevKitM_1, + DevKitVersion.ESP32_C6_DevKitC_1}) { + ESP32DevKit riscV = version(version); + Rectangle2D bounds = riscV.getBodyShape().getBounds2D(); + Assert.assertEquals(version + " board width", + new Size(25.40d, SizeUnit.mm).convertToPixels(), bounds.getWidth(), 0.1); + Assert.assertEquals(version + " pin field should be centred", + riscV.getControlPoint(0).getX() + narrowRowSpacing.convertToPixels() / 2.0, + bounds.getCenterX(), 0.1); + } + } + + @Test + public void testEveryVersionDraws() { + for (DevKitVersion version : DevKitVersion.values()) { + ESP32DevKit devKit = version(version); + for (boolean headers : new boolean[] {false, true}) { + devKit.setHeaders(headers); + MakerBoardTestSupport.assertDrawsCleanly(devKit); + } + } + } + + @Test + public void testDisplayPinLabelStripsAnnotations() { + // the silkscreen defaults to the node name without the parenthesized function list and + // without the trailing disambiguator that keeps two grounds apart in the netlist + Assert.assertEquals("GPIO34", AbstractMakerBoard.getDisplayPinLabel("GPIO34 (ADC1_CH6)")); + Assert.assertEquals("3V3", AbstractMakerBoard.getDisplayPinLabel("3V3_1")); + Assert.assertEquals("EN", AbstractMakerBoard.getDisplayPinLabel("EN")); + Assert.assertEquals("", AbstractMakerBoard.getDisplayPinLabel(null)); + Assert.assertEquals("", AbstractMakerBoard.getDisplayPinLabel("")); + } + + private static void assertTwoRows(ESP32DevKit devKit, Size rowSpacing) { + int pinsPerRow = devKit.getControlPointCount() / 2; + MakerBoardTestSupport.assertRow(devKit, 0, pinsPerRow - 1); + MakerBoardTestSupport.assertRow(devKit, pinsPerRow, devKit.getControlPointCount() - 1); + MakerBoardTestSupport.assertRowSpacing(devKit, 0, pinsPerRow, rowSpacing); + } + + private static ESP32DevKit version(DevKitVersion version) { + ESP32DevKit devKit = new ESP32DevKit(); + devKit.setVersion(version); + return devKit; + } +} 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 000000000..ec7ab4d07 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/ESP8266NodeMCUTest.java @@ -0,0 +1,79 @@ +/* + * + * 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 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 testPinout() { + ESP8266NodeMCU mcu = new ESP8266NodeMCU(); + Assert.assertEquals(30, mcu.getControlPointCount()); + + // left row, top to bottom + Assert.assertEquals("A0 (ADC0)", mcu.getControlPointNodeName(0)); + Assert.assertEquals("CLK (GPIO6, flash)", mcu.getControlPointNodeName(8)); + Assert.assertEquals("VIN", mcu.getControlPointNodeName(14)); + + // right row, top to bottom + Assert.assertEquals("D0 (GPIO16, WAKE)", mcu.getControlPointNodeName(15)); + Assert.assertEquals("3V3_3", mcu.getControlPointNodeName(29)); + + // the silkscreen prints the bare board label while the node name carries the GPIO mapping, + // which on this board is the thing people most often get wrong + Assert.assertEquals("D1 (GPIO5, SCL)", mcu.getControlPointNodeName(16)); + Assert.assertEquals("D2 (GPIO4, SDA)", mcu.getControlPointNodeName(17)); + Assert.assertEquals("D7 (GPIO13, HSPI MOSI)", mcu.getControlPointNodeName(24)); + Assert.assertEquals("D1", mcu.getSilkPinLabel(16)); + Assert.assertEquals("RSV", mcu.getSilkPinLabel(1)); + + // the four grounds and three supply pads are separate nets, so they cannot share a node name + Assert.assertEquals("GND_1", mcu.getControlPointNodeName(9)); + Assert.assertEquals("GND_4", mcu.getControlPointNodeName(28)); + Assert.assertEquals("3V3_1", mcu.getControlPointNodeName(10)); + Assert.assertEquals("GND", mcu.getSilkPinLabel(9)); + Assert.assertEquals("3V3", mcu.getSilkPinLabel(10)); + } + + @Test + public void testGeometry() { + ESP8266NodeMCU mcu = new ESP8266NodeMCU(); + + MakerBoardTestSupport.assertRow(mcu, 0, 14); + MakerBoardTestSupport.assertRow(mcu, 15, 29); + MakerBoardTestSupport.assertRowSpacing(mcu, 0, 15, new Size(0.9d, SizeUnit.in)); + MakerBoardTestSupport.assertBoardSize(mcu, new Size(25.7d, SizeUnit.mm), + new Size(48.0d, SizeUnit.mm)); + + Rectangle2D bounds = mcu.getBodyShape().getBounds2D(); + Assert.assertEquals("Margin above the first pin", + new Size(6.22d, SizeUnit.mm).convertToPixels(), + mcu.getControlPoint(0).getY() - bounds.getY(), 0.1); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java new file mode 100644 index 000000000..6378b72f7 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/MakerBoardTestSupport.java @@ -0,0 +1,145 @@ +/* + * + * 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 java.awt.Graphics2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; + +import org.diylc.common.Orientation; +import org.diylc.components.AbstractMakerBoard; +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; + +/** + * Shared checks for the maker board tests. + * + *

What every board has in common -- naming all of its pins, keeping them apart, drawing in each + * state, rotating -- is asserted once for all of them in {@code MakerComponentsTest}, but only for + * the version each board starts with. The tests in this package cover what is specific to a board: + * its pinout, its geometry and the way those change from one version to the next. + */ +class MakerBoardTestSupport { + + static final double PIN_SPACING = new Size(0.1d, SizeUnit.in).convertToPixels(); + + private static final Project PROJECT = new Project(); + + private static final IDrawingObserver OBSERVER = new IDrawingObserver() { + + @Override + public void startTracking() {} + + @Override + public void stopTracking() {} + + @Override + public void startTrackingContinuityArea(boolean positive) {} + + @Override + public void stopTrackingContinuityArea() {} + + @Override + public boolean isTrackingContinuityArea() { + return false; + } + + @Override + public void setContinuityMarker(String marker) {} + }; + + /** Asserts that pins {@code from} through {@code to} form a straight row on 0.1" pitch. */ + static void assertRow(AbstractMakerBoard board, int from, int to) { + assertRow(board, from, to, PIN_SPACING); + } + + /** + * Asserts that pins {@code from} through {@code to} form a straight row on the given pitch. The + * axis comes from the first pair, so a row that bends part way along fails. + */ + static void assertRow(AbstractMakerBoard board, int from, int to, double pitch) { + boolean vertical = Math.abs( + board.getControlPoint(from).getX() - board.getControlPoint(from + 1).getX()) < 0.01; + for (int i = from; i < to; i++) { + Point2D p1 = board.getControlPoint(i); + Point2D p2 = board.getControlPoint(i + 1); + Assert.assertEquals("Pitch between pins " + i + " and " + (i + 1), pitch, p1.distance(p2), + 0.01); + Assert.assertEquals("Pins " + i + " and " + (i + 1) + " should line up", + vertical ? p1.getX() : p1.getY(), vertical ? p2.getX() : p2.getY(), 0.01); + } + } + + /** Asserts the distance between the first pins of two parallel rows. */ + static void assertRowSpacing(AbstractMakerBoard board, int first, int second, Size spacing) { + Point2D p1 = board.getControlPoint(first); + Point2D p2 = board.getControlPoint(second); + Assert.assertEquals("Row spacing", spacing.convertToPixels(), p1.distance(p2), 0.01); + Assert.assertEquals("Rows should start level", p1.getY(), p2.getY(), 0.01); + } + + /** Asserts the outline size, which is what a shield, a case or an enclosure has to fit. */ + static void assertBoardSize(AbstractMakerBoard board, Size width, Size length) { + Rectangle2D bounds = board.getBodyShape().getBounds2D(); + Assert.assertEquals("Board width", width.convertToPixels(), bounds.getWidth(), 0.1); + Assert.assertEquals("Board length", length.convertToPixels(), bounds.getHeight(), 0.1); + } + + /** + * Asserts that two versions of a board share an outline and put their first {@code sharedPins} + * control points in the same place. A revision that keeps its footprint has to keep its pin + * positions too, or switching the version moves every connection in an existing file. + */ + static void assertSharedFootprint(AbstractMakerBoard reference, AbstractMakerBoard variant, + int sharedPins) { + Assert.assertEquals("Outline", reference.getBodyShape().getBounds2D(), + variant.getBodyShape().getBounds2D()); + for (int i = 0; i < sharedPins; i++) { + Assert.assertEquals("Pin " + i, reference.getControlPoint(i), variant.getControlPoint(i)); + } + } + + /** Draws the board in every state and orientation; anything that throws fails the test. */ + static void assertDrawsCleanly(AbstractMakerBoard board) { + BufferedImage image = new BufferedImage(1000, 1000, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + Orientation original = board.getOrientation(); + board.setControlPoint(new Point2D.Double(300, 300), 0); + try { + for (Orientation orientation : Orientation.values()) { + board.setOrientation(orientation); + board.draw(g2d, ComponentState.NORMAL, false, PROJECT, OBSERVER); + board.draw(g2d, ComponentState.SELECTED, false, PROJECT, OBSERVER); + board.draw(g2d, ComponentState.NORMAL, true, PROJECT, OBSERVER); + } + board.drawIcon(g2d, 32, 32); + } finally { + board.setOrientation(original); + g2d.dispose(); + } + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java new file mode 100644 index 000000000..4e49f7139 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiPicoTest.java @@ -0,0 +1,125 @@ +/* + * + * 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 java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.components.micro.RaspberryPiPico.PicoVersion; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class RaspberryPiPicoTest { + + @Test + public void testPinout() { + RaspberryPiPico pico = new RaspberryPiPico(); + Assert.assertEquals(PicoVersion.PICO, pico.getVersion()); + + // 2x20 castellated pads plus the three debug pads + Assert.assertEquals(43, pico.getControlPointCount()); + Assert.assertEquals("GP0", pico.getControlPointNodeName(0)); + Assert.assertEquals("GP15", pico.getControlPointNodeName(19)); + Assert.assertEquals("VBUS", pico.getControlPointNodeName(20)); + Assert.assertEquals("VSYS", pico.getControlPointNodeName(21)); + Assert.assertEquals("GP16", pico.getControlPointNodeName(39)); + Assert.assertEquals("SWCLK", pico.getControlPointNodeName(40)); + Assert.assertEquals("GND_SWD", pico.getControlPointNodeName(41)); + Assert.assertEquals("SWDIO", pico.getControlPointNodeName(42)); + + // the RP2350 boards are pin- and size-identical to their RP2040 counterparts, so only the chip + // marking tells them apart + assertSameBoard(PicoVersion.PICO, PicoVersion.PICO_2); + assertSameBoard(PicoVersion.PICO_W, PicoVersion.PICO_2_W); + } + + @Test + public void testGeometry() { + RaspberryPiPico pico = new RaspberryPiPico(); + Rectangle2D bounds = pico.getBodyShape().getBounds2D(); + + MakerBoardTestSupport.assertBoardSize(pico, new Size(21.0d, SizeUnit.mm), + new Size(51.0d, SizeUnit.mm)); + MakerBoardTestSupport.assertRow(pico, 0, 19); + MakerBoardTestSupport.assertRow(pico, 20, 39); + MakerBoardTestSupport.assertRowSpacing(pico, 0, 20, new Size(0.7d, SizeUnit.in)); + + // the pad field is symmetrical: 1.61 mm in from each long edge and 1.37 mm from each end + Point2D gp0 = pico.getControlPoint(0); + Point2D gp15 = pico.getControlPoint(19); + Point2D vbus = pico.getControlPoint(20); + double insetX = new Size(1.61d, SizeUnit.mm).convertToPixels(); + double insetY = new Size(1.37d, SizeUnit.mm).convertToPixels(); + Assert.assertEquals(insetX, gp0.getX() - bounds.getX(), 0.1); + Assert.assertEquals(insetY, gp0.getY() - bounds.getY(), 0.1); + Assert.assertEquals(insetX, bounds.getMaxX() - vbus.getX(), 0.1); + Assert.assertEquals(insetY, bounds.getMaxY() - gp15.getY(), 0.1); + + // the debug pads run along the bottom edge of the original, level with the last row of pads + // and centred on the board + MakerBoardTestSupport.assertRow(pico, 40, 42); + Assert.assertEquals(gp15.getY(), pico.getControlPoint(41).getY(), 0.01); + Assert.assertEquals(bounds.getCenterX(), pico.getControlPoint(41).getX(), 0.1); + + // the wireless boards move them inboard to make room for the radio module, 19.8 mm up from the + // bottom edge and 7.38 mm in from the right one + RaspberryPiPico picoW = version(PicoVersion.PICO_W); + Rectangle2D wirelessBounds = picoW.getBodyShape().getBounds2D(); + Point2D debugCentre = picoW.getControlPoint(41); + MakerBoardTestSupport.assertRow(picoW, 40, 42); + Assert.assertEquals(new Size(19.8d, SizeUnit.mm).convertToPixels(), + wirelessBounds.getMaxY() - debugCentre.getY(), 0.1); + Assert.assertEquals(new Size(7.38d, SizeUnit.mm).convertToPixels(), + wirelessBounds.getMaxX() - debugCentre.getX(), 0.1); + } + + @Test + public void testEveryVersionDraws() { + for (PicoVersion version : PicoVersion.values()) { + RaspberryPiPico pico = version(version); + for (boolean headers : new boolean[] {false, true}) { + pico.setHeaders(headers); + MakerBoardTestSupport.assertDrawsCleanly(pico); + } + } + } + + private static void assertSameBoard(PicoVersion a, PicoVersion b) { + RaspberryPiPico first = version(a); + RaspberryPiPico second = version(b); + Assert.assertEquals(a + " and " + b + " should have the same pin count", + first.getControlPointCount(), second.getControlPointCount()); + MakerBoardTestSupport.assertSharedFootprint(first, second, first.getControlPointCount()); + for (int i = 0; i < first.getControlPointCount(); i++) { + Assert.assertEquals("Pin " + i + " on " + a + " and " + b, + first.getControlPointNodeName(i), second.getControlPointNodeName(i)); + } + } + + private static RaspberryPiPico version(PicoVersion version) { + RaspberryPiPico pico = new RaspberryPiPico(); + pico.setVersion(version); + return pico; + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java new file mode 100644 index 000000000..7c92ce0af --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiTest.java @@ -0,0 +1,126 @@ +/* + * + * 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 java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.components.micro.RaspberryPi.RaspberryPiVersion; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class RaspberryPiTest { + + @Test + public void testPinoutPerVersion() { + RaspberryPi pi = new RaspberryPi(); + Assert.assertEquals(RaspberryPiVersion.PI_5, pi.getVersion()); + + // the 40-pin header, then the PoE header and one point per flat-flex connector + Assert.assertEquals(48, pi.getControlPointCount()); + Assert.assertEquals("3.3V (Pin 1)", pi.getControlPointNodeName(0)); + Assert.assertEquals("5V (Pin 2)", pi.getControlPointNodeName(1)); + Assert.assertEquals("GPIO2/SDA (Pin 3)", pi.getControlPointNodeName(2)); + Assert.assertEquals("GND (Pin 39)", pi.getControlPointNodeName(38)); + Assert.assertEquals("GPIO21 (Pin 40)", pi.getControlPointNodeName(39)); + Assert.assertEquals("PoE TR0 (Pin 1)", pi.getControlPointNodeName(40)); + Assert.assertEquals("PoE TR3 (Pin 4)", pi.getControlPointNodeName(43)); + Assert.assertEquals("PCIe", pi.getControlPointNodeName(44)); + Assert.assertEquals("MIPI 1", pi.getControlPointNodeName(45)); + Assert.assertEquals("UART", pi.getControlPointNodeName(47)); + + // the PCIe socket and the second camera port arrived with the Pi 5, so the earlier boards are + // two points short + for (RaspberryPiVersion version : new RaspberryPiVersion[] {RaspberryPiVersion.PI_3_B, + RaspberryPiVersion.PI_4_B}) { + RaspberryPi earlier = version(version); + Assert.assertEquals(version + " pin count", 46, earlier.getControlPointCount()); + Assert.assertEquals("GPIO21 (Pin 40)", earlier.getControlPointNodeName(39)); + Assert.assertEquals("PoE TR3 (Pin 4)", earlier.getControlPointNodeName(43)); + } + } + + @Test + public void testHeaderGeometry() { + RaspberryPi pi = new RaspberryPi(); + Rectangle2D bounds = pi.getBodyShape().getBounds2D(); + double spacing = MakerBoardTestSupport.PIN_SPACING; + + MakerBoardTestSupport.assertBoardSize(pi, new Size(85.0d, SizeUnit.mm), + new Size(56.0d, SizeUnit.mm)); + + // the header is numbered down the columns, odd pins in the inner row and even in the outer, + // so consecutive pins of a row are two indices apart + Point2D pin1 = pi.getControlPoint(0); + Point2D pin2 = pi.getControlPoint(1); + for (int column = 0; column < 19; column++) { + Point2D odd = pi.getControlPoint(column * 2); + Point2D nextOdd = pi.getControlPoint((column + 1) * 2); + Point2D even = pi.getControlPoint(column * 2 + 1); + Point2D nextEven = pi.getControlPoint((column + 1) * 2 + 1); + Assert.assertEquals(spacing, nextOdd.getX() - odd.getX(), 0.01); + Assert.assertEquals(odd.getY(), nextOdd.getY(), 0.01); + Assert.assertEquals(spacing, nextEven.getX() - even.getX(), 0.01); + Assert.assertEquals(even.getY(), nextEven.getY(), 0.01); + } + + // the header straddles the mounting hole line 3.5 mm down, and is centred between the holes at + // 3.5 mm and 61.5 mm from the left edge + Assert.assertEquals(pin1.getX(), pin2.getX(), 0.01); + Assert.assertEquals(mm(3.5d), (pin1.getY() + pin2.getY()) / 2.0 - bounds.getY(), 0.1); + Assert.assertEquals(bounds.getX() + mm(32.5d), + (pin1.getX() + pi.getControlPoint(38).getX()) / 2.0, 0.1); + + // the PoE header is a 2x2 block centred on the bottom right mounting hole + double poeCenterX = bounds.getX() + mm(61.5d); + double poeCenterY = bounds.getY() + mm(46.5d); + Assert.assertEquals(poeCenterX - spacing / 2.0, pi.getControlPoint(40).getX(), 0.1); + Assert.assertEquals(poeCenterY - spacing / 2.0, pi.getControlPoint(40).getY(), 0.1); + Assert.assertEquals(poeCenterX + spacing / 2.0, pi.getControlPoint(43).getX(), 0.1); + Assert.assertEquals(poeCenterY + spacing / 2.0, pi.getControlPoint(43).getY(), 0.1); + + // the flat-flex connectors are single points at the centre of the socket they stand for + for (int i = 44; i < pi.getControlPointCount(); i++) { + Assert.assertTrue("Connector " + pi.getControlPointNodeName(i) + " should be on the board", + bounds.contains(pi.getControlPoint(i))); + } + } + + @Test + public void testEveryVersionDraws() { + for (RaspberryPiVersion version : RaspberryPiVersion.values()) { + MakerBoardTestSupport.assertDrawsCleanly(version(version)); + } + } + + private static double mm(double value) { + return new Size(value, SizeUnit.mm).convertToPixels(); + } + + private static RaspberryPi version(RaspberryPiVersion version) { + RaspberryPi pi = new RaspberryPi(); + pi.setVersion(version); + return pi; + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java new file mode 100644 index 000000000..8a946f046 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/RaspberryPiZeroTest.java @@ -0,0 +1,91 @@ +/* + * + * 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 java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import org.diylc.components.micro.RaspberryPiZero.ZeroVersion; +import org.diylc.core.measures.Size; +import org.diylc.core.measures.SizeUnit; +import org.junit.Assert; +import org.junit.Test; + +public class RaspberryPiZeroTest { + + @Test + public void testPinout() { + RaspberryPiZero pi = new RaspberryPiZero(); + Assert.assertEquals(ZeroVersion.PI_ZERO, pi.getVersion()); + + // the 40-pin header, then the camera port and the two test point pairs + Assert.assertEquals(45, pi.getControlPointCount()); + Assert.assertEquals("3.3V (Pin 1)", pi.getControlPointNodeName(0)); + Assert.assertEquals("5V (Pin 2)", pi.getControlPointNodeName(1)); + Assert.assertEquals("GND (Pin 39)", pi.getControlPointNodeName(38)); + Assert.assertEquals("GPIO21 (Pin 40)", pi.getControlPointNodeName(39)); + Assert.assertEquals("MIPI (CSI)", pi.getControlPointNodeName(40)); + Assert.assertEquals("RUN 1", pi.getControlPointNodeName(41)); + Assert.assertEquals("TV 2", pi.getControlPointNodeName(44)); + } + + @Test + public void testGeometryIsVersionIndependent() { + RaspberryPiZero pi = new RaspberryPiZero(); + Rectangle2D bounds = pi.getBodyShape().getBounds2D(); + double spacing = MakerBoardTestSupport.PIN_SPACING; + + MakerBoardTestSupport.assertBoardSize(pi, new Size(65.0d, SizeUnit.mm), + new Size(30.0d, SizeUnit.mm)); + + // the header is numbered down the columns, odd pins in the inner row and even in the outer, + // so consecutive pins of a row are two indices apart + for (int column = 0; column < 19; column++) { + Point2D odd = pi.getControlPoint(column * 2); + Point2D nextOdd = pi.getControlPoint((column + 1) * 2); + Point2D even = pi.getControlPoint(column * 2 + 1); + Point2D nextEven = pi.getControlPoint((column + 1) * 2 + 1); + Assert.assertEquals(spacing, nextOdd.getX() - odd.getX(), 0.01); + Assert.assertEquals(odd.getY(), nextOdd.getY(), 0.01); + Assert.assertEquals(spacing, nextEven.getX() - even.getX(), 0.01); + Assert.assertEquals(even.getY(), nextEven.getY(), 0.01); + } + + // the header straddles the mounting hole line 3.5 mm down and is centred between the holes at + // 3.5 mm and 61.5 mm from the left edge + Point2D pin1 = pi.getControlPoint(0); + Point2D pin2 = pi.getControlPoint(1); + double millimetre = new Size(1.0d, SizeUnit.mm).convertToPixels(); + Assert.assertEquals(pin1.getX(), pin2.getX(), 0.01); + Assert.assertEquals(3.5d * millimetre, (pin1.getY() + pin2.getY()) / 2.0 - bounds.getY(), 0.1); + Assert.assertEquals(bounds.getX() + 32.5d * millimetre, + (pin1.getX() + pi.getControlPoint(38).getX()) / 2.0, 0.1); + + // the Zero W and the Zero 2 W keep the outline, the header position and every connector of the + // original; only the SoC package and the silkscreen name change + for (ZeroVersion version : ZeroVersion.values()) { + RaspberryPiZero variant = new RaspberryPiZero(); + variant.setVersion(version); + MakerBoardTestSupport.assertSharedFootprint(pi, variant, pi.getControlPointCount()); + } + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java new file mode 100644 index 000000000..d1f0f36d3 --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/TeensyTest.java @@ -0,0 +1,143 @@ +/* + * + * 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.components.micro.Teensy.TeensyVersion; +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 testPinoutPerVersion() { + // 14 left + 14 right + a five-hole end cluster + the VUSB pad + 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("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)); + Assert.assertEquals("13 (SCK/CRX1/LED)", teensy40.getControlPointNodeName(27)); + Assert.assertEquals("VBAT", teensy40.getControlPointNodeName(28)); + Assert.assertEquals("On/Off", teensy40.getControlPointNodeName(32)); + Assert.assertEquals("VUSB", teensy40.getControlPointNodeName(33)); + + // the 3.2 has the same pin field but its own functions, starting with the analog ground + Teensy teensy32 = version(TeensyVersion.Teensy_3_2); + Assert.assertEquals(34, teensy32.getControlPointCount()); + Assert.assertEquals("Vin (3.6-6.0V)", teensy32.getControlPointNodeName(14)); + Assert.assertEquals("AGND", teensy32.getControlPointNodeName(15)); + Assert.assertEquals("VUSB", teensy32.getControlPointNodeName(33)); + + // 24 left + 24 right + the five-hole cluster + the 3x2 Ethernet and 5-pin USB host headers + // + the VUSB pad + Teensy teensy41 = version(TeensyVersion.Teensy_4_1); + Assert.assertEquals(65, teensy41.getControlPointCount()); + Assert.assertEquals("32 (OUT1B)", teensy41.getControlPointNodeName(23)); + Assert.assertEquals("VIN (3.6-5.5V)", teensy41.getControlPointNodeName(24)); + Assert.assertEquals("33 (MCLK2)", teensy41.getControlPointNodeName(47)); + Assert.assertEquals("VBAT", teensy41.getControlPointNodeName(48)); + Assert.assertEquals("ETH_TX-", teensy41.getControlPointNodeName(53)); + Assert.assertEquals("USB_5V", teensy41.getControlPointNodeName(59)); + Assert.assertEquals("VUSB", teensy41.getControlPointNodeName(64)); + + // the silkscreen covers the two pin rows only; the end cluster and the VUSB pad are printed on + // the underside. PJRC abbreviates the supply pads and drops the alternate functions, and the + // compact boards have no room for the three pads level with the cluster and the VUSB pad + Assert.assertEquals("0", teensy40.getSilkPinLabel(1)); + Assert.assertEquals("G", teensy40.getSilkPinLabel(0)); + Assert.assertEquals("5V", teensy40.getSilkPinLabel(14)); + Assert.assertEquals("3V", teensy40.getSilkPinLabel(16)); + Assert.assertEquals("", teensy40.getSilkPinLabel(13)); + Assert.assertEquals("", teensy40.getSilkPinLabel(27)); + Assert.assertEquals("", teensy32.getSilkPinLabel(15)); + + // printing the names cornerwise leaves the 4.1 room for every pad of both rows, including the + // ones the compact boards have to drop; only the ground level with the VUSB pad stays blank + Assert.assertEquals("12", teensy41.getSilkPinLabel(13)); + Assert.assertEquals("33", teensy41.getSilkPinLabel(47)); + Assert.assertEquals("", teensy41.getSilkPinLabel(25)); + } + + @Test + public void testGeometryPerVersion() { + // the 3.2 shares the 4.0's outline and pin field; the 4.1 is the same width and longer + for (TeensyVersion version : new TeensyVersion[] {TeensyVersion.Teensy_4_0, + TeensyVersion.Teensy_3_2}) { + Teensy teensy = version(version); + MakerBoardTestSupport.assertRow(teensy, 0, 13); + MakerBoardTestSupport.assertRow(teensy, 14, 27); + MakerBoardTestSupport.assertRowSpacing(teensy, 0, 14, new Size(15.24d, SizeUnit.mm)); + MakerBoardTestSupport.assertRow(teensy, 28, 32); + MakerBoardTestSupport.assertBoardSize(teensy, new Size(17.78d, SizeUnit.mm), + new Size(35.56d, SizeUnit.mm)); + assertPin1Inset(teensy); + } + + Teensy teensy41 = version(TeensyVersion.Teensy_4_1); + MakerBoardTestSupport.assertRow(teensy41, 0, 23); + MakerBoardTestSupport.assertRow(teensy41, 24, 47); + MakerBoardTestSupport.assertRowSpacing(teensy41, 0, 24, new Size(15.24d, SizeUnit.mm)); + MakerBoardTestSupport.assertRow(teensy41, 48, 52); + MakerBoardTestSupport.assertRow(teensy41, 59, 63); + MakerBoardTestSupport.assertBoardSize(teensy41, new Size(17.78d, SizeUnit.mm), + new Size(60.96d, SizeUnit.mm)); + assertPin1Inset(teensy41); + + // the Ethernet header is a 3x2 block on 2 mm pitch rather than the 0.1" the rest of the board + // uses, so it gets its own rows + double ethPitch = new Size(2.0d, SizeUnit.mm).convertToPixels(); + MakerBoardTestSupport.assertRow(teensy41, 53, 55, ethPitch); + MakerBoardTestSupport.assertRow(teensy41, 56, 58, ethPitch); + Assert.assertEquals("Ethernet row spacing", ethPitch, + teensy41.getControlPoint(56).getY() - teensy41.getControlPoint(53).getY(), 0.01); + } + + @Test + public void testEveryVersionDraws() { + for (TeensyVersion version : TeensyVersion.values()) { + Teensy teensy = version(version); + for (boolean headers : new boolean[] {false, true}) { + teensy.setHeaders(headers); + MakerBoardTestSupport.assertDrawsCleanly(teensy); + } + } + } + + /** Every Teensy sets its first pad 0.05" in from the left and top edges. */ + private static void assertPin1Inset(Teensy teensy) { + double inset = new Size(1.27d, SizeUnit.mm).convertToPixels(); + Assert.assertEquals(teensy.getVersion() + " pin 1 inset from the left edge", inset, + teensy.getControlPoint(0).getX() - teensy.getBodyShape().getBounds2D().getX(), 0.1); + Assert.assertEquals(teensy.getVersion() + " pin 1 inset from the top edge", inset, + teensy.getControlPoint(0).getY() - teensy.getBodyShape().getBounds2D().getY(), 0.1); + } + + private static Teensy version(TeensyVersion version) { + Teensy teensy = new Teensy(); + teensy.setVersion(version); + return teensy; + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java b/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java new file mode 100644 index 000000000..374a67ceb --- /dev/null +++ b/diylc/diylc-library/src/test/java/org/diylc/components/micro/WemosD1MiniTest.java @@ -0,0 +1,70 @@ +/* + * + * 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 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 WemosD1MiniTest { + + @Test + public void testPinout() { + WemosD1Mini mcu = new WemosD1Mini(); + Assert.assertEquals(16, mcu.getControlPointCount()); + + // left row, top to bottom + Assert.assertEquals("RST", mcu.getControlPointNodeName(0)); + Assert.assertEquals("A0", mcu.getControlPointNodeName(1)); + Assert.assertEquals("D0 (GPIO16)", mcu.getControlPointNodeName(2)); + Assert.assertEquals("3V3", mcu.getControlPointNodeName(7)); + + // right row, top to bottom; the board prints the ground pad as "G" + Assert.assertEquals("5V", mcu.getControlPointNodeName(8)); + Assert.assertEquals("G (GND)", mcu.getControlPointNodeName(9)); + Assert.assertEquals("D1 (GPIO5)", mcu.getControlPointNodeName(13)); + Assert.assertEquals("TX (GPIO1)", mcu.getControlPointNodeName(15)); + } + + @Test + public void testGeometry() { + WemosD1Mini mcu = new WemosD1Mini(); + + MakerBoardTestSupport.assertRow(mcu, 0, 7); + MakerBoardTestSupport.assertRow(mcu, 8, 15); + MakerBoardTestSupport.assertBoardSize(mcu, new Size(1.0d, SizeUnit.in), + new Size(1.34d, SizeUnit.in)); + + // the right row runs bottom to top, so it is the last pin that lines up with the first + Assert.assertEquals("Row spacing", new Size(0.9d, SizeUnit.in).convertToPixels(), + mcu.getControlPoint(0).distance(mcu.getControlPoint(15)), 0.01); + + Rectangle2D bounds = mcu.getBodyShape().getBounds2D(); + Assert.assertEquals("Margin above the first pin", + new Size(0.275d, SizeUnit.in).convertToPixels(), + mcu.getControlPoint(0).getY() - bounds.getY(), 0.1); + } +} diff --git a/diylc/diylc-library/src/test/java/org/diylc/netlist/GuitarDiagramAnalyzerTests.java b/diylc/diylc-library/src/test/java/org/diylc/netlist/GuitarDiagramAnalyzerTests.java index c389ff8db..438e19a6b 100644 --- a/diylc/diylc-library/src/test/java/org/diylc/netlist/GuitarDiagramAnalyzerTests.java +++ b/diylc/diylc-library/src/test/java/org/diylc/netlist/GuitarDiagramAnalyzerTests.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.List; +import org.diylc.core.IDIYComponent; import org.diylc.components.passive.AxialFilmCapacitor; import org.diylc.components.passive.Resistor; @@ -377,7 +378,7 @@ public class GuitarDiagramAnalyzerTests { SingleCoilPickup pickup = new SingleCoilPickup(); pickup.setName("Pickup"); pickup.setPolarity(Polarity.StackedNorth); - Netlist netlist = new Netlist(Arrays.asList(jack, pickup)); + Netlist netlist = new Netlist(Arrays.>asList(jack, pickup)); Group tipGroup = new Group().connect(jack, 0).connect(pickup, 2); Group sleeveGroup = new Group().connect(jack, 1).connect(pickup, 1); netlist.add(tipGroup).add(sleeveGroup); @@ -399,7 +400,7 @@ public class GuitarDiagramAnalyzerTests { SingleCoilPickup pickup2 = new SingleCoilPickup(); pickup2.setName("Pickup2"); pickup2.setPolarity(Polarity.StackedSouth); - Netlist netlist = new Netlist(Arrays.asList(jack, pickup1, pickup2)); + Netlist netlist = new Netlist(Arrays.>asList(jack, pickup1, pickup2)); Group tipGroup = new Group().connect(jack, 0).connect(pickup1, 1).connect(pickup2, 1); Group sleeveGroup = new Group().connect(jack, 1).connect(pickup1, 2).connect(pickup2, 2); netlist.add(tipGroup).add(sleeveGroup); @@ -422,7 +423,7 @@ public class GuitarDiagramAnalyzerTests { SingleCoilPickup pickup2 = new SingleCoilPickup(); pickup2.setName("Pickup2"); pickup2.setPolarity(Polarity.StackedSouth); - Netlist netlist = new Netlist(Arrays.asList(jack, pickup1, pickup2)); + Netlist netlist = new Netlist(Arrays.>asList(jack, pickup1, pickup2)); Group tipGroup = new Group().connect(jack, 0).connect(pickup1, 1).connect(pickup2, 2); Group sleeveGroup = new Group().connect(jack, 1).connect(pickup1, 2).connect(pickup2, 1); netlist.add(tipGroup).add(sleeveGroup); @@ -445,7 +446,7 @@ public class GuitarDiagramAnalyzerTests { SingleCoilPickup pickup2 = new SingleCoilPickup(); pickup2.setName("Pickup2"); pickup2.setPolarity(Polarity.North); - Netlist netlist = new Netlist(Arrays.asList(jack, pickup1, pickup2)); + Netlist netlist = new Netlist(Arrays.>asList(jack, pickup1, pickup2)); Group tipGroup = new Group().connect(jack, 0).connect(pickup1, 1).connect(pickup2, 1); Group sleeveGroup = new Group().connect(jack, 1).connect(pickup1, 2).connect(pickup2, 2); netlist.add(tipGroup).add(sleeveGroup); @@ -467,7 +468,7 @@ public class GuitarDiagramAnalyzerTests { pickup1.setPolarity(Polarity.StackedNorth); HumbuckerPickup pickup2 = new HumbuckerPickup(); pickup2.setName("Pickup2"); - Netlist netlist = new Netlist(Arrays.asList(jack, pickup1, pickup2)); + Netlist netlist = new Netlist(Arrays.>asList(jack, pickup1, pickup2)); Group tipGroup = new Group().connect(jack, 0).connect(pickup1, 1).connect(pickup2, 0); Group coilTapGroup = new Group().connect(pickup2, 1).connect(pickup2, 2); Group sleeveGroup = new Group().connect(jack, 1).connect(pickup1, 2).connect(pickup2, 3); diff --git a/diylc/diylc-swing/pom.xml b/diylc/diylc-swing/pom.xml index 03f062040..738d5693f 100644 --- a/diylc/diylc-swing/pom.xml +++ b/diylc/diylc-swing/pom.xml @@ -7,7 +7,7 @@ org.diylc diylc-parent - 6.4.0 + 6.5.0 diylc-swing diff --git a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/TreePanel.java b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/TreePanel.java index f8b1687c8..e77b84c64 100644 --- a/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/TreePanel.java +++ b/diylc/diylc-swing/src/main/java/org/diylc/swing/plugins/tree/TreePanel.java @@ -468,6 +468,20 @@ public class TreePanel extends JPanel { private static final long serialVersionUID = 1L; + public ComponentCellRenderer() { + setVerticalAlignment(SwingConstants.CENTER); + setVerticalTextPosition(SwingConstants.CENTER); + } + + @Override + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + if (d != null) { + d.height = Math.max(d.height, 20); + } + return d; + } + @SuppressWarnings("unchecked") @Override public Component getTreeCellRendererComponent(final JTree tree, final Object value, @@ -479,19 +493,17 @@ public class TreePanel extends JPanel { Object obj = value; if (obj != null && obj.getClass().equals(TreeNode.class)) { TreeNode payload = (TreeNode) obj; + setPreferredSize(null); if (payload.getComponentType() == null) { setToolTipText(null); if (leaf) setIcon(IconLoader.Component.getIcon()); - - setPreferredSize(new Dimension(250, 20)); } else { setToolTipText("" + payload.getComponentType().getName() + "
" + payload.getComponentType().getDescription() + "
Author: " + payload.getComponentType().getAuthor() + "

" + CLICK_TO_INSTANTIATE + ""); setIcon(payload.getComponentType().getIcon()); - setPreferredSize(new Dimension(250, 32)); } String shortCutHtml = ""; @@ -528,11 +540,25 @@ public class TreePanel extends JPanel { } } + int viewportWidth = tree.getVisibleRect().width; + if (viewportWidth <= 0 && tree.getParent() != null) { + viewportWidth = tree.getParent().getWidth(); + } + if (viewportWidth <= 0 && tree.getWidth() > 0) { + viewportWidth = tree.getWidth(); + } + if (viewportWidth <= 0) { + viewportWidth = 240; + } + + int overhead = (payload.getComponentType() != null) ? 95 : 45; + int wrapWidth = Math.max(100, viewportWidth - overhead); + // translate categories - setText("" + setText("

" + (payload.getComponentType() == null ? LangUtil.translate(payload.forDisplay()) : payload.forDisplay()) - + shortCutHtml + variantsHtml + ""); + + shortCutHtml + variantsHtml + "
"); } return this; diff --git a/diylc/docs/plans/maker-board-roadmap.md b/diylc/docs/plans/maker-board-roadmap.md new file mode 100644 index 000000000..e952ba7ee --- /dev/null +++ b/diylc/docs/plans/maker-board-roadmap.md @@ -0,0 +1,456 @@ +clau# Maker Board Roadmap + +Closing the gap between `org.diylc.components.micro` and what people building with +microcontroller boards today actually reach for. + +Status: proposed +Scope: `diylc-library`, package `org.diylc.components.micro` +Supersedes: `tier-1-modern-boards.md` + +## 1. Goal + +The `micro` package models 10 board classes across roughly 16 selectable variants. The +scaffolding in `AbstractMakerBoard` — USB connector types, PCB meander antenna, castellated +and through-hole pads, terminal blocks, FPC connectors, mounting holes, vendor logos — is +complete enough that **adding a board is mostly pin arrays and a draw branch**. The expensive +part is already paid for. + +This document inventories what is modelled, lists what a modern maker cannot draw today, and +sorts the additions into three tiers by how often their absence would actually block someone. + +## 2. Decisions that govern this roadmap + +| # | Decision | Choice | Rationale | +|---|---|---|---| +| D1 | How coverage grows | **Named boards, drawn faithfully.** No generic/parametric "configurable dev board" component. | A parametric board would cover more of the long tail per hour, but the palette entry a maker searches for is the product name, and accurate silk/connectors/logos are the point of these components. One was built and reverted; the question is settled. | +| D2 | New class vs. new `Version` constant | **Form factor decides.** Same outline and pin positions → append a `Version` enum constant. Different footprint → new class. | Already the established paradigm (`ArduinoUno`, `RaspberryPiPico`, `RaspberryPiZero`, `ESP32DevKit`). Keeps the palette short without forcing unrelated geometry into one draw method. | +| D3 | Where pin data lives | **Hardcoded `String[]` arrays in the component class.** | Matches every existing board. An external pin-definition resource and per-pin type metadata (`POWER_5V`, `GND`, `ADC`, `I2C_SDA`…) were both considered and declined; no pin-data infrastructure is introduced here. | +| D4 | Package boundary | **`micro` only.** | Adjacent gaps (Qwiic/STEMMA QT/Grove connectors, shields and HATs, stackable headers) are real but belong to `connectivity` / `electromechanical` and are out of scope for this document. | +| D5 | Backward compatibility | Append-only enums, unchanged defaults, unchanged control-point counts for existing variants. | `.diy` files are a public contract; see §4.3. No file-format migration is needed anywhere in this roadmap. | + +### Explicitly out of scope + +- A configurable catch-all board component (D1). +- Pin-type metadata and any electrical rule check built on it (D3). +- Shields, HATs, stacking, and the Qwiic/STEMMA QT/Grove connector family (D4). +- Bumping the project version, packaging, and the deployment build. + +## 3. Current coverage + +| Class | Variants | Notes | +|---|---|---| +| `ArduinoUno` | R3, R4 WiFi, **R4 Minima**, **Leonardo** | | +| `ArduinoMega` | — | | +| `ArduinoNano` | **classic, Every, 33 IoT, 33 BLE, 33 BLE Sense, RP2040 Connect, Nano ESP32** | | +| `ESP32DevKit` | V1 30-pin, DevKitC V4 38-pin, S3 DevKitC-1 44-pin, **C3 DevKitM-1 30-pin**, **C6 DevKitC-1 32-pin** | per-version pin arrays, geometry and draw branches | +| `ESP8266NodeMCU` | — | | +| `WemosD1Mini` | — | hardcodes `SILK_PIN_NAMES_LEFT/RIGHT` instead of `drawPinLabels` | +| `RaspberryPi` | 3B+, 4B, 5 | | +| `RaspberryPiZero` | Zero, Zero W, **Zero 2 W** | | +| `RaspberryPiPico` | Pico, Pico W, **Pico 2**, **Pico 2 W** | castellated pad rendering | +| `Teensy` | 4.0, 4.1, **Teensy 3.2** | | + +Variants in bold landed with this roadmap; everything in §5.1 and §5.2 is now **done**. All ten are +`category = "Controllers"`, `bomPolicy = SHOW_ONLY_TYPE_NAME`, `enableCache = true`, and extend +`AbstractMakerBoard`. + +## 4. Shared mechanics + +Applies to every item in every tier. + +### 4.1 Component discovery + +Components are found by classpath annotation scan in `ComponentProcessor.getComponentTypes()` +(Reflections, `TypesAnnotated`, `includePackage("org.diylc.components")`). **Any class carrying +`@ComponentDescriptor` is picked up automatically** — there is no manifest or registry to edit. +Adding enum constants to an existing class needs nothing. Adding a *new* class needs nothing for +discovery, but should be added to the hand-maintained list in +`org.diylc.components.maker.MakerComponentsTest#allMakerComponentClasses`. + +### 4.2 Version property pattern + +```java +public enum XxxVersion { A("Label A"), B("Label B"); ... toString() returns label; } + +protected XxxVersion version = XxxVersion.A; // default initializer + +@EditableProperty(name = "Version") +public XxxVersion getVersion() { + if (version == null) version = XxxVersion.A; // null-guard for old files + return version; +} +public void setVersion(XxxVersion version) { + this.version = version; + updateControlPoints(); // only if geometry changes + invalidateCache(); // required: enableCache = true +} +``` + +XStream serializes enum constants **by name** — `.diy` files contain `_90` +and the like, and no custom enum converter is registered — so adding constants and reordering them +are both backward compatible. **Removing or renaming one is not.** Ordinals are used only by the +rotation transformers (`Orientation`, `OrientationHV`, `Orientation45`), never by a `Version` enum, +so a version enum may be sorted for a tidy drop-down; `ArduinoNano.NanoVersion` is ordered by label +for exactly that reason. Keep the default constant and the null-guard so existing files deserialize +unchanged. + +### 4.3 Serialization safety checklist (per changed class) + +- `serialVersionUID` stays `1L`. +- The default `version` field value is unchanged, so old files keep their look. +- **Control-point count must not change for existing variants.** If a new variant has a + different pin count, `updateControlPoints()` must size the array from the per-version pin list + (as `ESP32DevKit` already does), and any code assuming a fixed `PIN_NAMES.length` must switch + on version. +- `drawIcon()` reflects only the default variant — leave it alone unless the default changes. + +### 4.4 Test pattern + +Each board has a `*Test.java` under `diylc-library/src/test/java/org/diylc/components/micro/`: + +- `testControlPointCountAndNames()` — count, every name non-empty, spot checks +- `testDimensionsAndPinGeometry()` — `getBodyShape().getBounds2D()` against expected mm, and + control-point offsets from board edges +- `testVersionProperty()` — default, `toString()` labels, round-trip through the setter +- a `*Drawing` smoke test — `draw()` in NORMAL / SELECTED / outline, headers on and off, against + a `BufferedImage` with a no-op `IDrawingObserver` + +Extend these in place; add one geometry test and one draw smoke test per new variant. + +### 4.5 Sourcing dimensions and pin names + +Every dimension constant and every pin array below must be cross-checked against vendor +documentation before it is committed. Where this document states a size or a pin count without +naming a source, it is an estimate to scope the work, not a datasheet citation. + +What actually works as a source: + +- **Arduino** — `docs.arduino.cc/resources/pinouts/-full-pinout.pdf` is canonical for PWM + tildes and silkscreen names; `docs.arduino.cc/resources/datasheets/-datasheet.pdf` carries + the numbered header tables. The HTML product and cheat-sheet pages render client-side and yield + nothing to a fetch. +- **Espressif** — the `esp-dev-kits` user guides carry complete J1/J3 header tables as HTML. +- **PJRC** — pinout cards, not yet used for the Teensy 3.2 bottom cluster (see §5.1 item 4). + +Note that these PDFs need downloading and text extraction; a plain fetch returns binary. Do not +assume variants of one board share a pinout — across the Nano family the PWM sets, the bus +annotations and the power pins all differ per variant. + +### 4.6 Build and verify + +```bash +cd diylc +mvn -pl diylc-library -am test +``` + +Rendering-regression fixtures live in `../diylc-regression-data/` but are not wired into the +library test run, so new variants do not break it. When those baselines are next regenerated, +add sample placements for the new variants. + +--- + +## 5. Tier 1 + +Boards a maker is very likely to reach for and cannot draw today. + +### 5.1 Enum appends to existing classes — **done** + +The cheapest fidelity wins in the whole roadmap — the geometry is already correct, so each is a +pin array, a silk label and a few draw branches. + +| # | Board | Host class | Change | +|---|---|---|---| +| 1 | Raspberry Pi Pico 2 / Pico 2 W (RP2350) | `RaspberryPiPico` | 2 constants | +| 2 | Raspberry Pi Zero 2 W | `RaspberryPiZero` | 1 constant | +| 3 | Arduino Uno R4 Minima | `ArduinoUno` | 1 constant | +| 4 | Teensy 3.2 | `Teensy` | 1 constant + pin array | +| 5 | Arduino Leonardo | `ArduinoUno` | 1 constant + pin array | + +**Item 1 — Pico 2 / Pico 2 W.** The Pico 2 is pin-, size- and connector-identical to the Pico +(21 × 51 mm, 2×20 castellated plus through-hole, 3-pad SWD, micro-USB, BOOTSEL). The visible +differences: the SoC silk reads `RP2350` instead of `RP2040`, and Pico 2 W carries the same +Infineon CYW43439 radio in the same position as Pico W, so its 3-pad SWD block sits at the +Pico W offset. + +1. `PicoVersion`: append `PICO_2("Pi Pico 2")`, `PICO_2_W("Pi Pico 2 W")`. +2. Add helpers so the existing checks stay readable: + ```java + private boolean isW() { return version == PicoVersion.PICO_W || version == PicoVersion.PICO_2_W; } + private boolean isV2() { return version == PicoVersion.PICO_2 || version == PicoVersion.PICO_2_W; } + ``` +3. In `updateControlPoints()`, `getBodyShape()`, `drawCastellatedPads()` and `draw()`, replace + `getVersion() == PicoVersion.PICO_W` with `isW()`. +4. The chip label becomes `isV2() ? "RP2350" : "RP2040"`. +5. `PIN_NAMES` is unchanged and shared across all four variants. +6. `drawIcon()` unchanged (default is still `PICO`). + +Tests: extend `testVersionProperty()`; add `testPico2GeometryMatchesPico()` asserting identical +body bounds and control-point count for `PICO`/`PICO_2` and `PICO_W`/`PICO_2_W`; draw smoke test +for `PICO_2_W`. **~2–3 h, minimal risk.** + +**Item 2 — Pi Zero 2 W.** Identical 65 × 30 mm outline, identical 40-pin GPIO position, identical +connector layout (mini-HDMI, 2× micro-USB, CSI FPC on the right edge, 4 mounting holes). The SoC +is the RP3A0 package-on-package with integrated radio — physically a ~12 mm square where the +drawing already places a 12 mm chip. Silk and label only. + +1. `ZeroVersion`: append `PI_ZERO_2_W("Pi Zero 2 W")`. +2. No geometry change; pin arrays untouched. +3. `draw()` already prints `"Raspberry " + getVersion()` — verify the longer string still fits + inside `hdrBoxW`; if not, drop to `SILK_FONT` or print `getVersion().toString()` alone for + this case. +4. Optional cosmetic touch: draw the SoC as a plain dark PoP square with no Broadcom split. + +Tests: extend `testVersionProperty()`; geometry-equivalence assertion against `PI_ZERO_W`; draw +smoke test. **~1–2 h, minimal risk — only watch the label width.** + +**Item 3 — Uno R4 Minima.** `ArduinoUno` models R3 and R4 WiFi but not the Minima, which shares +the R4 WiFi board outline and header positions and differs in silk (no ESP32-S3 radio module, no +LED matrix) and in the absence of the WiFi can. Append `R4_MINIMA("UNO R4 Minima")` and branch the +draw code that places the radio module and matrix. + +**Item 4 — Teensy 3.2.** Same 1.4 × 0.7" outline and 2×14 header positions as Teensy 4.0, so this +is a pin array plus silk. Implemented with 34 control points — the two 1×14 edge rows, the +five-hole bottom cluster and the severable VUSB pad — and an `isCompact()` helper that groups the +3.2 with the 4.0 against the longer 4.1. **The bottom-cluster labels (`VBAT`, `3.3V`, `GND`, +`Program`, `A14/DAC`) are the one part not taken from a primary source and are worth checking +against the PJRC pinout card.** + +**Item 5 — Arduino Leonardo.** Shares the Uno R3 shield footprint and header positions exactly. +Differences are the ATmega32U4 silk, micro-USB in place of USB-B (`drawMicroUsb` instead of +`drawUsbB`), and no separate USB-serial chip. Pin names are the R3 map with the native-USB +annotations. + +### 5.2 Version retrofit and new pin arrays — **done** + +**Item 6 — Modern Arduino Nano family.** `ArduinoNano` has no version property; Tier 1 introduces +one and adds the current lineup. All share the classic 0.73 × 1.70" outline and the 2×15 pin +positions, but **only the classic Nano populates the 2×3 ICSP block** — on every later board that +end of the PCB carries the radio module or USB bridge instead. So the classic keeps its 36 control +points and the modern variants have 30. Serialization is safe because the classic is the default +and is untouched; the shorter counts belong to enum constants no shipped file references. + +```java +public enum NanoVersion { + CLASSIC("Nano (ATmega328)"), // default; current behavior + EVERY("Nano Every"), + NANO_33_IOT("Nano 33 IoT"), + NANO_33_BLE("Nano 33 BLE"), + NANO_33_BLE_SENSE("Nano 33 BLE Sense"), + NANO_RP2040_CONNECT("Nano RP2040 Connect"), + NANO_ESP32("Nano ESP32"); +} +``` + +What varies by version: + +| Aspect | CLASSIC | EVERY | 33 IoT | 33 BLE / Sense | RP2040 Connect | Nano ESP32 | +|---|---|---|---|---|---|---| +| Separate MCU package | `m328P` (rot. 45°) | `m4809` | `SAMD21` | — (in module) | `RP2040` | — (in module) | +| Part at the far end | ICSP 2×3 header | `SAMD11` bridge | `NINA-W102` can | `NINA-B306` can | `NINA-W102` can | `NORA-W106` can | +| USB | Mini-B | Micro-B | Micro-B | Micro-B | Micro-B | **USB-C** | +| Pin-name array | classic | modern | modern | modern | modern | modern + `GPIOxx` | +| Control points | 36 | 30 | 30 | 30 | 30 | 30 | + +The enum carries the silk label, the MCU marking and the end-module marking, so `draw()` branches +only on USB type, whether the MCU package is drawn separately (it is not when the die sits inside +the module) and whether the end part is a shielded can or a bare QFN. + +`drawMiniUsb`, `drawMicroUsb`, `drawUsbC`, `drawMetalConnector` and `drawChip` all already exist +in `AbstractMakerBoard`. + +Pin arrays: `PIN_NAMES` keeps the classic 36 entries. Each later board gets **its own 30-entry +array**, transcribed from its official Arduino pinout diagram (`docs.arduino.cc/resources/pinouts/ +-full-pinout.pdf`) rather than assumed from the classic. A single shared "modern" array was +wrong: the PWM sets and the power pins genuinely differ. + +| Version | PWM pins | Position 17 | Position 18 | Position 27 | +|---|---|---|---|---| +| CLASSIC | D3 D5 D6 D9 D10 D11 | RST2 | 5V | AREF | +| EVERY | D3 D5 D6 D9 D10 | RST2 | 5V | AREF | +| NANO_33_IOT | D2 D3 D5 D6 D9 D10 | RST2 | 5V | AREF | +| NANO_33_BLE / _SENSE | D2–D13 (all) | RST2 | 5V | AREF | +| NANO_RP2040_CONNECT | D2–D13 (all) | **REC** (BOOTSEL) | 5V | AREF | +| NANO_ESP32 | D0–D13 and A0–A7 | **B1** | **VUSB** | **B0** | +| NANO_R4 | D3 D5 D6 D9 D10 D11 | **BOOT** | 5V | AREF | + +The Nano R4 (ABX00142, Renesas RA4M1) additionally annotates D4/D5 as the CAN pins, A0 as the DAC +and A1–A3 as the on-chip OPAMP inputs and output, and its end-of-board part is the **Qwiic** +JST-SH socket rather than a radio module — drawn with `drawFpcConnector`, since it is a connector +and not a shielded can. + +`PIN_NAMES_ESP32` additionally dual-labels with the Espressif GPIO number, e.g. `D2 (~, GPIO5)`; +those numbers are taken from the ABX00083 pinout diagram. + +`draw()` gains version switches for the MCU chip label (and whether it is drawn rotated 45° like +the classic TQFP or axis-aligned like a QFN), the end module, and the USB connector helper. Board +outline and mounting holes are unchanged. Because the pin count is now version-dependent, +`setVersion()` must call `updateControlPoints()` — it previously only invalidated the cache, which +was harmless while the geometry was fixed. + +The `5V` / `3V3` logic-level silk from the original plan was dropped: the Nano draws no pin labels +at all, so a lone voltage annotation would be invented silkscreen rather than what is on the board. + +The modern Nanos also have **castellated edges** rather than plain through-holes: the outline is +notched at every pin and the pads run out to the board edge, exactly as on the Pico. That rendering +now lives in `AbstractMakerBoard` as `drawCastellatedPads` (one column of pads toward a given edge) +and `subtractCastellationNotches` (the matching bites out of the outline), with +`getCastellatedPadSize` / `HoleSize` / `NotchSize` / `PadColor` as the per-board overrides. +`RaspberryPiPico` was refactored onto the same pair — its own method survives only as the wrapper +that places the two columns and draws the SWD pads, so its rendering is unchanged. Any future +castellated board (the ESP32 SuperMini and XIAO in §5.3 / §6) should reuse these rather than copy +the geometry a third time. + +Tests: `testVersionProperty()` (default `CLASSIC`, labels, round-trip); +`testPinNamesPerVersion()` over all seven versions asserting no empty names, the `A4 (SDA)` +annotation on a modern version and the GPIO dual-label on `NANO_ESP32`; +`testGeometryIsVersionIndependent()` and `testModernVersionsHaveNoIcspBlock()` pinning the 36/30 +split and the outline; draw smoke test looping all versions × headers on/off. **The value is +entirely in getting the labels and per-board silk right; the rendering scaffold is a light touch.** + +**Item 7 — ESP32-C3 DevKitM-1 and ESP32-C6 DevKitC-1.** The heaviest enum-style item, because +`ESP32DevKit` gives every variant its own pin array and its own branch in `getRelativeOffsets()`, +`getBodyShape()` and `draw()`. The C3 is a genuinely different board (single RISC-V core, +ESP32-C3-MINI-1 module, one addressable RGB LED), not a re-label. + +Both pin arrays were transcribed from the Espressif `esp-dev-kits` user guides (the J1/J3 header +tables), not from memory — see D3's note in §4.5. + +- **ESP32-C3-DevKitM-1**: two 1×15 headers (**30 pins**) on 0.1" pitch, row spacing 22.86 mm + (0.9"), ESP32-C3-MINI-1 module with PCB antenna at the top, a **single micro-USB** port at the + bottom, `BOOT` + `RST` buttons. +- **ESP32-C6-DevKitC-1**: two 1×16 headers (**32 pins**, the last position on each is `NC`) on the + same 0.9" body, ESP32-C6-WROOM-1 module, **two USB-C** connectors (USB-to-UART and native). + +Espressif publishes these outlines only as dimension drawings, so `BOARD_WIDTH_RISCV` (25.4 mm) and +the row spacing are taken from the shared DevKitC form factor and the body length is *derived* as +top margin + pin field + bottom margin using the same margins as the S3 DevKitC-1. That derivation +is called out in a comment on the constants and is the thing to check if a board ever measures +wrong. + +1. `DevKitVersion`: append `ESP32_C3_DevKitM_1("ESP32-C3 DevKitM-1 (32-Pin)")` and + `ESP32_C6_DevKitC_1("ESP32-C6 DevKitC-1 (32-Pin)")`. +2. Add `PIN_NAMES_C3` and `PIN_NAMES_C6`. +3. Add `BOARD_WIDTH_C3` / `BOARD_LENGTH_C3` (and `_C6`) plus module and antenna sizes; the C3 + module is smaller than WROOM-32, so `SHIELD_*_S3` is the better starting point. +4. `getControlPointNodeName(int)`: two new branches. +5. `getRelativeOffsets()`: two branches. Both are the simple two-straight-columns layout — copy + the 30-pin branch and change the loop bounds and row-spacing constant. +6. `getBodyShape()`: rounded rectangle plus PCB-antenna notch at the top, as the 30-pin and S3 + cases already do. +7. `draw()`: module shield labelled `ESP32-C3-MINI-1` / `ESP32-C6-WROOM-1` via + `drawMetalConnector`, PCB antenna via `drawPcbAntenna`, `BOOT`/`RST` via `drawButton`, USB via + `drawMicroUsb` ×2 (C3) or `drawUsbC` ×2 (C6), one RGB LED dot. +8. `drawIcon()` unchanged (default stays 30-pin V1). + +Tests mirror the existing per-version blocks. **~1 day, moderate risk** — geometry constants need +datasheet cross-checking, but the change is contained entirely within `ESP32DevKit`. + +### 5.3 New classes + +Genuinely different footprints, so per D2 each gets its own class. + +| # | Board | Shape | Why Tier 1 | +|---|---|---|---| +| 8 | **ESP32-CAM** | ~40 × 27 mm, 2×8 headers, OV2640 camera FPC, microSD slot | After the DevKit, the most-used ESP32 board in hobby projects. Nothing close exists. | +| 9 | **ESP32 SuperMini** | ~22.5 × 18 mm, 2×8 castellated, PCB antenna. One class, `Version` {C3, S3} | Now the default cheap ESP32 board. Model on `RaspberryPiPico`'s castellated pad rendering. | +| 10 | **Arduino Pro Micro** | ~33 × 18 mm, 2×12, ATmega32U4, micro-USB | The HID and custom-keyboard staple; no equivalent in the set. | +| 11 | **Arduino Pro Mini** | ~33 × 18 mm, 2×12 + 6-pin FTDI header. `Version` {5V 16 MHz, 3.3V 8 MHz} | Still ubiquitous on breadboards. Identical outline between the two variants; differs in silk and regulator. | +| 12 | **STM32 Pill** | ~53 × 22 mm, 2×20 on 0.1". `Version` {Blue Pill F103, Black Pill F401, Black Pill F411} | Cheapest ARM path, still everywhere. Shared outline makes this one class. | + +Item 9 replaces the "Tier 2, note only" treatment the SuperMini had in the superseded document: +it is a different footprint from the DevKit (so a separate class, per D2) and is common enough now +to belong in Tier 1. + +--- + +## 6. Tier 2 + +Whole ecosystems worth a deliberate push. Each is one class plus a `Version` enum where the vendor +standardised a footprint, and separate classes where they did not. + +| Board / family | Structure | Notes | +|---|---|---| +| **Seeed XIAO** | one class, `Version` {ESP32-C3, ESP32-S3, RP2040, nRF52840, SAMD21} | Seeed standardised a ~21 × 17.5 mm castellated footprint across the family — a textbook single-class case under D2. | +| **Adafruit Feather** | one class, `Version` {M0 Express, M4 Express, ESP32-S3, RP2040} | Standardised 0.9 × 2.0" outline. | +| **Adafruit QT Py / ItsyBitsy** | one class each | Different footprints from Feather and from each other. | +| **BBC micro:bit** | one class, `Version` {v1, v2} | Enormous in education. The gold edge connector is genuinely new drawing work, which is why it is not Tier 1. | +| **Arduino MKR** | one class, `Version` {WiFi 1010, Zero, NB 1500} | Shared ~25 × 61.5 mm outline. | +| **Heltec WiFi LoRa 32** | one class, `Version` {V2, V3} | LoRa + OLED. The Meshtastic community is large and entirely unserved today. | +| **LilyGO T-Display-S3** | one class | ESP32-S3 with an integrated TFT. | +| **TTGO LoRa32** | one class | Distinct footprint from the Heltec boards. | +| **M5Stack** | `M5StackCore` (Core2), `M5StickC` (Plus / Plus2), `M5Atom` (Atom / StampS3) | Four distinct form factors; group by footprint, not by brand. | +| **Waveshare RP2040-Zero** | one class | ~18 × 23.5 mm castellated. | +| **Pimoroni Tiny 2040** | one class | Sibling footprint to RP2040-Zero but not identical. | +| **Pi-compatible SBCs** | `OrangePiZero`, `RadxaZero`, `LePotato` | Same 40-pin header, different outlines, so named classes rather than one shared one (D1, D2). | + +--- + +## 7. Tier 3 + +Long tail and specialty. Add on request or when a specific project needs one. + +- **Pro / industrial Arduino** — Giga R1, Portenta H7, Nicla Sense, Due, Zero, Micro. +- **Raspberry Pi variants** — CM4 / CM5 plus IO board, Pi 400 / 500. +- **AI accelerators** — Jetson Orin Nano, Google Coral Dev Board. +- **Other SBCs and RISC-V** — BeagleBone Black, Milk-V Duo, ESP32-P4, CH32V003. +- **Legacy** — Teensy LC, Teensy 2.0++, Digispark ATtiny85, Wemos D1 R32. +- **Bare modules for custom-PCB work** — ESP-01, ESP-12F, ESP32-WROOM-32, nRF52840 module. These + are footprints rather than dev boards; if they are ever added, check whether they belong in + `micro` at all or alongside the SMD packages. + +--- + +## 8. Cross-cutting fixes + +Small, independent, and worth folding into whichever Tier 1 item is done first. + +1. **`Teensy` z-order.** `Teensy.java:54` declares `zOrder = IDIYComponent.BOARD`, while every + other breadboard-friendly board in the package (`ArduinoNano`, `RaspberryPiPico`, + `ESP32DevKit`, `ESP8266NodeMCU`, `WemosD1Mini`) uses `COMPONENT`. `Breadboard` is also `BOARD`, + so a Teensy dropped on a breadboard renders in placement order rather than reliably on top. + Looks like an oversight; changing it affects only draw order, not serialization. +2. **`Version` never reaches the BOM.** `getValue()` returns `null` and every board uses + `bomPolicy = SHOW_ONLY_TYPE_NAME`, so a Uno R3 and a Uno R4 WiFi both print as "Arduino Uno". + A maker's BOM is a shopping list and the variant is the part they buy. Worth deciding on a + uniform approach before the roadmap roughly triples the number of variants. +3. **Editable-property set drifts across the family.** `Headers` exists on `ArduinoNano` and + `RaspberryPiZero` but not on `ArduinoUno`, `Teensy` or `ESP32DevKit`; there is no "show pin + labels" toggle anywhere; `WemosD1Mini` hardcodes `SILK_PIN_NAMES_LEFT` / `SILK_PIN_NAMES_RIGHT` + instead of using the shared `drawPinLabels`. Settling a uniform Version / Headers / Show Labels + / Board Color set now stops ten more boards inheriting the inconsistency. +4. **Pin-name conventions differ.** `ESP32DevKit` uses `GND_1`, `ArduinoNano` uses `GND1`. And + `AbstractMakerBoard.getDisplayPinLabel` truncates at the first `_` and `(`, so `ArduinoNano`'s + `D3 (~)` silently loses its PWM marker on the silkscreen. Pick one convention before adding + more arrays. + +Items 1 and 4 are behavioural changes small enough to carry their own regression-suite run; +items 2 and 3 are decisions to make before the bulk of the roadmap lands. + +## 9. Suggested sequencing + +1. ~~**§5.1 enum appends**~~ — **done**: Pico 2 / 2 W, Zero 2 W, Uno R4 Minima, Leonardo, + Teensy 3.2. +2. ~~**§5.2 Item 6, modern Nano**~~ and ~~**Item 7, ESP32-C3 / C6**~~ — **done**. +3. **Cross-cutting fixes** (§8) — cheap, and fix 4 is better done before more pin arrays exist. +4. **§5.3 new classes** — ESP32-CAM and SuperMini first; they close the largest remaining gaps. +5. **Tier 2** — one family at a time, each an independent commit. + +Each item is an independent commit; none depends on another except where sequencing is noted. + +## 10. Open decisions + +1. **BOM treatment of `Version`** (§8.2). Options: leave as-is; switch these boards to a BOM + policy that includes the variant; or expose the variant through a value-like property. Affects + every board in the package, so worth deciding once. +2. **Uniform property set** (§8.3). Which of `Headers` and a new "Show Pin Labels" toggle should + exist on all boards, and whether retrofitting `Headers` onto `ArduinoUno` / `Teensy` / + `ESP32DevKit` is wanted. New boolean fields deserialize as `false` in old files, so the default + must be chosen to preserve the current look. +3. **ESP32-C6 in the same pass as C3?** §5.2 Item 7 is written for both since they share a body. + If time-boxing, ship C3 first and add C6 as a one-branch follow-up. +4. **Nano pin-label verbosity** (§5.2 Item 6). Recommendation as written: annotate bus pins on all + modern variants, add the GPIO number only for `NANO_ESP32`. +5. **i18n.** Component display names in `@ComponentDescriptor` are not currently in + `diylc-swing/src/main/resources/lang/*.txt` for these boards, and the enum labels are plain + strings shown in the property editor. No translation work is required by this roadmap; noted + for a future i18n pass. diff --git a/diylc/pom.xml b/diylc/pom.xml index 0494f2a38..9e7722ee1 100644 --- a/diylc/pom.xml +++ b/diylc/pom.xml @@ -6,7 +6,7 @@ org.diylc diylc-parent - 6.4.0 + 6.5.0 pom DIY Layout Creator