Compare commits

..

10 Commits

Author SHA1 Message Date
Bane Stojkovic cd9ac32a2f dot matrix improvements 2026-10-09 14:21:29 +02:00
Bane Stojkovic 9922615de5 release 2026-10-09 13:37:20 +02:00
Bane Stojkovic 44b57f4880 add lcd matrix, pom bump 2026-10-09 12:53:35 +02:00
dependabot[bot] 486e83b6b5 Bump com.fasterxml.jackson.core:jackson-databind in /diylc/diylc-core (#1081)
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson-databind) from 2.22.2 to 2.22.3.
- [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.2...jackson-databind-2.22.3)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 09:38:32 +02:00
Bane Stojkovic eb48ab9664 Merge branch 'master' of https://github.com/bancika/diy-layout-creator 2026-10-09 09:38:16 +02:00
Bane Stojkovic a743de4024 Controllers (#1080)
* 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

* displays tweaks

* display improvements

* neopix

* displays

* improvements

* led matrix

* strip

* led bar graph

* jewel

* tft

* tft

* Update maker-peripherals.md

* nokia

* led bar

* labels

* tft

* cleanup

* oled

* cleanup

* display enhancement

* displays

* neopixel

* displays

* displays

* new tft

* displays

* icon

* variants

* 2-digit 0.56 inch 7 segment

* Added ability to hide component categories in the component tree and toolbar from the Config menu

* led bar

* Update variants.xml

* color selection for bar graph

* Added 1.8" sd

* tft fixes

* Update WS2812BRing.java

* neopixel improvements

* netlist and node name improvements

* oled tweak

* cleanup

* mockito fix

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 09:38:09 +02:00
dependabot[bot] f62b3ddf04 Bump com.fasterxml.jackson.core:jackson-databind in /diylc/diylc-swing (#1078)
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson-databind) from 2.22.2 to 2.22.3.
- [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.2...jackson-databind-2.22.3)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-09 09:19:37 +02:00
Bane Stojkovic f77c821f0a Create audio-coverage-roadmap.md 2026-10-08 09:11:36 +02:00
dependabot[bot] 208c157016 Bump com.fasterxml.jackson.core:jackson-databind in /diylc/diylc-swing (#1076)
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson-databind) from 2.22.1 to 2.22.2.
- [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.1...jackson-databind-2.22.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 13:10:35 +02:00
dependabot[bot] c6d4b9df9c Bump com.fasterxml.jackson.core:jackson-databind in /diylc/diylc-core (#1077)
Bumps [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson-databind) from 2.22.1 to 2.22.2.
- [Commits](https://github.com/FasterXML/jackson-databind/compare/jackson-databind-2.22.1...jackson-databind-2.22.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 13:10:25 +02:00
16 changed files with 1398 additions and 105 deletions
+2 -2
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.6.0</version>
<version>6.7.0</version>
</parent>
<artifactId>diylc-core</artifactId>
@@ -60,7 +60,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.1</version>
<version>2.22.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
@@ -4897,7 +4897,7 @@
<entry><string>Interface</string><diylc.displays.CharacterLCD_-LCDInterface>I2C_Backpack</diylc.displays.CharacterLCD_-LCDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>16x2 Parallel HD44780</name>
@@ -4906,7 +4906,7 @@
<entry><string>Interface</string><diylc.displays.CharacterLCD_-LCDInterface>Parallel_16Pin</diylc.displays.CharacterLCD_-LCDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>20x4 I2C Backpack</name>
@@ -4915,7 +4915,7 @@
<entry><string>Interface</string><diylc.displays.CharacterLCD_-LCDInterface>I2C_Backpack</diylc.displays.CharacterLCD_-LCDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>20x4 Parallel HD44780</name>
@@ -4924,7 +4924,7 @@
<entry><string>Interface</string><diylc.displays.CharacterLCD_-LCDInterface>Parallel_16Pin</diylc.displays.CharacterLCD_-LCDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -4938,7 +4938,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>I2C_4Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.96" SSD1306 SPI (7-Pin)</name>
@@ -4947,7 +4947,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_7Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.96" SSD1306 SPI (6-Pin)</name>
@@ -4956,7 +4956,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_6Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.96" SSD1306 SPI (8-Pin)</name>
@@ -4965,7 +4965,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_8Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.91" SSD1306 I2C</name>
@@ -4974,7 +4974,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>I2C_4Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.91" SSD1306 SPI (7-Pin)</name>
@@ -4983,7 +4983,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_7Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.91" SSD1306 SPI (6-Pin)</name>
@@ -4992,7 +4992,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_6Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.91" SSD1306 SPI (8-Pin)</name>
@@ -5001,7 +5001,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_8Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.3" SH1106 I2C</name>
@@ -5010,7 +5010,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>I2C_4Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.3" SH1106 SPI (7-Pin)</name>
@@ -5019,7 +5019,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_7Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.3" SH1106 SPI (6-Pin)</name>
@@ -5028,7 +5028,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_6Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.3" SH1106 SPI (8-Pin)</name>
@@ -5037,7 +5037,7 @@
<entry><string>Interface</string><diylc.displays.OLEDDisplay_-OLEDInterface>SPI_8Pin</diylc.displays.OLEDDisplay_-OLEDInterface></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -5050,7 +5050,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>ILI9341_2_8</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>2.4" ILI9341 (Touch + SD)</name>
@@ -5058,7 +5058,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>ILI9341_2_4</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>0.96" ST7735 80x160</name>
@@ -5066,7 +5066,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>ST7735_0_96</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.8" ST7735 128x160</name>
@@ -5074,7 +5074,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>ST7735_1_8</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.8" ST7735 128x160 (SD)</name>
@@ -5082,7 +5082,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>ST7735_1_8_SD</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.54" ST7789 240x240</name>
@@ -5090,7 +5090,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>ST7789_1_54</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1.28" GC9A01 Round</name>
@@ -5098,7 +5098,7 @@
<entry><string>Controller</string><diylc.displays.TFTDisplay_-Controller>GC9A01_1_28</diylc.displays.TFTDisplay_-Controller></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -5112,7 +5112,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Cathode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>1-Digit 0.56" Common Anode</name>
@@ -5121,7 +5121,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Anode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>2-Digit 0.56" Common Cathode</name>
@@ -5130,7 +5130,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Cathode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>2-Digit 0.56" Common Anode</name>
@@ -5139,7 +5139,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Anode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>4-Digit 0.36" Common Cathode</name>
@@ -5148,7 +5148,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Cathode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>4-Digit 0.36" Common Anode</name>
@@ -5157,7 +5157,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Anode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>4-Digit 0.56" Common Cathode</name>
@@ -5166,7 +5166,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Cathode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>4-Digit 0.56" Common Anode</name>
@@ -5175,7 +5175,7 @@
<entry><string>Common</string><diylc.displays.SevenSegmentDisplay_-Common>Anode</diylc.displays.SevenSegmentDisplay_-Common></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>4-Digit TM1637 Module</name>
@@ -5183,7 +5183,7 @@
<entry><string>Type</string><diylc.displays.SevenSegmentDisplay_-DisplayType>TM1637_Module_4Pin</diylc.displays.SevenSegmentDisplay_-DisplayType></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -5196,7 +5196,7 @@
<entry><string>Modules</string><diylc.displays.LEDMatrix_-Modules>Single_8x8</diylc.displays.LEDMatrix_-Modules></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>Compact 8x8 (MAX7219)</name>
@@ -5204,7 +5204,7 @@
<entry><string>Modules</string><diylc.displays.LEDMatrix_-Modules>Compact_8x8</diylc.displays.LEDMatrix_-Modules></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>4-in-1 32x8 (MAX7219)</name>
@@ -5212,7 +5212,7 @@
<entry><string>Modules</string><diylc.displays.LEDMatrix_-Modules>FourInOne_32x8</diylc.displays.LEDMatrix_-Modules></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>8-in-1 64x8 (MAX7219)</name>
@@ -5220,7 +5220,7 @@
<entry><string>Modules</string><diylc.displays.LEDMatrix_-Modules>EightInOne_64x8</diylc.displays.LEDMatrix_-Modules></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -5233,7 +5233,7 @@
<entry><string>Segments</string><diylc.displays.LEDBarGraph_-Segments>_4</diylc.displays.LEDBarGraph_-Segments></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>5 Segment</name>
@@ -5241,7 +5241,7 @@
<entry><string>Segments</string><diylc.displays.LEDBarGraph_-Segments>_5</diylc.displays.LEDBarGraph_-Segments></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>6 Segment</name>
@@ -5249,7 +5249,7 @@
<entry><string>Segments</string><diylc.displays.LEDBarGraph_-Segments>_6</diylc.displays.LEDBarGraph_-Segments></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>8 Segment</name>
@@ -5257,7 +5257,7 @@
<entry><string>Segments</string><diylc.displays.LEDBarGraph_-Segments>_8</diylc.displays.LEDBarGraph_-Segments></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>10 Segment</name>
@@ -5265,7 +5265,7 @@
<entry><string>Segments</string><diylc.displays.LEDBarGraph_-Segments>_10</diylc.displays.LEDBarGraph_-Segments></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>12 Segment</name>
@@ -5273,7 +5273,7 @@
<entry><string>Segments</string><diylc.displays.LEDBarGraph_-Segments>_12</diylc.displays.LEDBarGraph_-Segments></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -5286,7 +5286,7 @@
<entry><string>Ring Size</string><diylc.displays.WS2812BRing_-RingSize>_12_LED</diylc.displays.WS2812BRing_-RingSize></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>16 LEDs</name>
@@ -5294,7 +5294,7 @@
<entry><string>Ring Size</string><diylc.displays.WS2812BRing_-RingSize>_16_LED</diylc.displays.WS2812BRing_-RingSize></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>24 LEDs</name>
@@ -5302,7 +5302,7 @@
<entry><string>Ring Size</string><diylc.displays.WS2812BRing_-RingSize>_24_LED</diylc.displays.WS2812BRing_-RingSize></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -5315,7 +5315,7 @@
<entry><string>Density</string><diylc.displays.WS2812BStrip_-Density>_30</diylc.displays.WS2812BStrip_-Density></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>60 LED/m</name>
@@ -5323,7 +5323,7 @@
<entry><string>Density</string><diylc.displays.WS2812BStrip_-Density>_60</diylc.displays.WS2812BStrip_-Density></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
<org.diylc.core.Template>
<name>144 LED/m</name>
@@ -5331,7 +5331,7 @@
<entry><string>Density</string><diylc.displays.WS2812BStrip_-Density>_144</diylc.displays.WS2812BStrip_-Density></entry>
</values>
<defaultFlag>false</defaultFlag>
<createdOn>2026-10-11T00:00:00.0000000</createdOn>
<createdOn>2026-10-09T00:00:00.0000000</createdOn>
</org.diylc.core.Template>
</list>
</entry>
@@ -502,7 +502,7 @@
<minor>7</minor>
<build>0</build>
</versionNumber>
<releaseDate>2026-10-11 00:00:00.000 CET</releaseDate>
<releaseDate>2026-10-09 00:00:00.000 CET</releaseDate>
<name></name>
<changes>
<org.diylc.appframework.update.Change>
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.6.0</version>
<version>6.7.0</version>
</parent>
<artifactId>diylc-library</artifactId>
@@ -343,7 +343,7 @@ public class MakerBoardPainter {
* nothing. The variant string reads "size, interface", and breaking it at the comma gives one
* property per line, which keeps the longest line well inside the glass instead of spanning it.
*/
private static String screenText(Display display, String name, String value) {
public static String screenText(Display display, String name, String value) {
boolean hasValue = value != null && !value.trim().isEmpty();
String variant = hasValue ? value.replace(", ", "\n") : null;
@@ -30,6 +30,7 @@ import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.util.List;
import org.diylc.awt.StringUtils;
import org.diylc.common.Display;
@@ -62,8 +63,6 @@ public class CharacterLCD extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
public static Size HEADER_OFFSET = new Size(2.54d, SizeUnit.mm);
// The parallel row is not centred: its leftmost pin starts this far in from the left edge.
public static Size PARALLEL_HEADER_INSET = new Size(10.0d, SizeUnit.mm);
// The I2C backpack's four pins stand in a column against the left edge, centred on the height.
public static Size I2C_HEADER_INSET = new Size(2.5d, SizeUnit.mm);
// A 3 mm hole centred 2.5 mm in leaves only 1 mm of board outside it, which is what the part
@@ -71,6 +70,15 @@ public class CharacterLCD extends AbstractMakerBoard {
public static Size MOUNTING_HOLE_INSET = new Size(2.5d, SizeUnit.mm);
public static Size MOUNTING_HOLE_SIZE = new Size(3.0d, SizeUnit.mm);
// The HD44780 character box. Square dots on a 0.6 mm pitch make the cell 2.95 x 4.75 mm, the
// figure the datasheet quotes, so the cell is derived from the pitch rather than stored twice.
public static Size DOT_SIZE = new Size(0.55d, SizeUnit.mm);
public static Size DOT_PITCH = new Size(0.6d, SizeUnit.mm);
public static Size CHAR_PITCH_X = new Size(3.55d, SizeUnit.mm);
public static Size CHAR_PITCH_Y = new Size(5.35d, SizeUnit.mm);
public static final int DOT_COLUMNS = 5;
public static final int DOT_ROWS = 8;
public static Color PCB_GREEN = Color.decode("#1B5E20");
public static Color SCREEN_BG = Color.decode("#1E88E5");
public static Color BEZEL_COLOR = Color.decode("#212121");
@@ -80,17 +88,13 @@ public class CharacterLCD extends AbstractMakerBoard {
public static Color SCREEN_INK_DARK = Color.decode("#1B2631");
public static final String[] PIN_NAMES_I2C = new String[] {"GND", "VCC", "SDA", "SCL"};
// The bare designation the module prints, with the function in parentheses where the pin's name
// does not give it away. The silkscreen label is the part before the parenthesis, there being no
// room for more along a 0.1" pitch.
public static final String[] PIN_NAMES_PARALLEL = new String[] {
"VSS (GND)", "VCC (+5V)", "VEE (Contrast)", "RS", "RW", "E",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7",
"A (Backlight +)", "K (Backlight -)"
};
// The bare designations the module prints. The supply and backlight pins carry their function
// in the node name instead, there being no room for it along a 0.1" pitch.
public static final String[] SILK_NAMES_PARALLEL = new String[] {
"VSS", "VCC", "VEE", "RS", "RW", "E",
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7", "A", "K"
"VSS (GND)", "VDD (+5V)", "VO (Contrast)", "RS", "R/W", "E",
"DB0", "DB1", "DB2", "DB3", "DB4", "DB5", "DB6", "DB7",
"LEDA (Backlight +)", "LEDK (Backlight -)"
};
private LCDSize lcdSize = LCDSize._16x2;
@@ -166,15 +170,6 @@ public class CharacterLCD extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
@Override
protected String getSilkPinLabel(int index) {
if (lcdInterface == LCDInterface.Parallel_16Pin && index >= 0
&& index < SILK_NAMES_PARALLEL.length) {
return SILK_NAMES_PARALLEL[index];
}
return super.getSilkPinLabel(index);
}
private int getPinCount() {
return lcdInterface == LCDInterface.I2C_Backpack ? PIN_NAMES_I2C.length
: PIN_NAMES_PARALLEL.length;
@@ -185,6 +180,14 @@ public class CharacterLCD extends AbstractMakerBoard {
return CalcUtils.calculateLuminance(screenColor) < 128d ? SCREEN_INK_LIGHT : SCREEN_INK_DARK;
}
/**
* An unlit dot is the backlight seen through the cell, so it shows as a shade of the backlight
* rather than a colour of its own and follows whatever the user picks.
*/
private Color getDotColor() {
return screenColor.darker();
}
private double getBoardWidth() {
return new Size(lcdSize.getWidthMm(), SizeUnit.mm).convertToPixels();
}
@@ -194,12 +197,12 @@ public class CharacterLCD extends AbstractMakerBoard {
}
/**
* Neither header is centred: the parallel row starts a fixed distance in from the left, and the
* I2C column stands against that same edge.
* Neither header is centred: the parallel row starts a distance in from the left that the module
* itself dictates, and the I2C column stands against that same edge whatever the module.
*/
private double getBoardX(double x) {
Size inset =
lcdInterface == LCDInterface.I2C_Backpack ? I2C_HEADER_INSET : PARALLEL_HEADER_INSET;
Size inset = lcdInterface == LCDInterface.I2C_Backpack ? I2C_HEADER_INSET
: new Size(lcdSize.getParallelHeaderInsetMm(), SizeUnit.mm);
return x - inset.convertToPixels();
}
@@ -302,9 +305,7 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setColor(screenColor);
g2d.fill(new Rectangle2D.Double(screenX, screenY, screenW, screenH));
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(screenX, screenY, screenW, screenH), getScreenInk(), getScreen(),
getName(), getValueForDisplay());
drawCharacterMatrix(g2d, new Rectangle2D.Double(screenX, screenY, screenW, screenH));
// Only the parallel row has anywhere to print. The backpack's column sits 2.5 mm from the
// left edge with the bezel 1.4 mm further in, so a label beside it would lie across the metal
@@ -322,6 +323,71 @@ public class CharacterLCD extends AbstractMakerBoard {
g2d.setComposite(oldComposite);
}
/**
* The character grid, centred in the lit area: every cell's dots in the unlit shade first, then
* the dots the text lights over the top of them. The matrix is narrower than the glass on both
* modules, which is why it is centred rather than inset by a margin.
*/
private void drawCharacterMatrix(Graphics2D g2d, Rectangle2D screenArea) {
double dotSize = DOT_SIZE.convertToPixels();
double dotPitch = DOT_PITCH.convertToPixels();
double charPitchX = CHAR_PITCH_X.convertToPixels();
double charPitchY = CHAR_PITCH_Y.convertToPixels();
double cellWidth = (DOT_COLUMNS - 1) * dotPitch + dotSize;
double cellHeight = (DOT_ROWS - 1) * dotPitch + dotSize;
double matrixWidth = (lcdSize.getColumns() - 1) * charPitchX + cellWidth;
double matrixHeight = (lcdSize.getRows() - 1) * charPitchY + cellHeight;
double matrixX = screenArea.getX() + (screenArea.getWidth() - matrixWidth) / 2.0;
double matrixY = screenArea.getY() + (screenArea.getHeight() - matrixHeight) / 2.0;
// The 20x4 comes to 3200 dots, so the rectangle is reused rather than allocated per dot, and
// the two shades are laid down in one pass each rather than a colour change per dot.
Rectangle2D.Double dot = new Rectangle2D.Double(0, 0, dotSize, dotSize);
g2d.setColor(getDotColor());
for (int row = 0; row < lcdSize.getRows(); row++) {
for (int column = 0; column < lcdSize.getColumns(); column++) {
double cellX = matrixX + column * charPitchX;
double cellY = matrixY + row * charPitchY;
for (int dotRow = 0; dotRow < DOT_ROWS; dotRow++) {
for (int dotColumn = 0; dotColumn < DOT_COLUMNS; dotColumn++) {
dot.x = cellX + dotColumn * dotPitch;
dot.y = cellY + dotRow * dotPitch;
g2d.fill(dot);
}
}
}
}
List<String> lines = DotMatrixScreen.layOutText(getScreen(), getName(),
getValueForDisplay(), lcdSize.getColumns(), lcdSize.getRows());
if (lines.isEmpty()) {
return;
}
g2d.setColor(getScreenInk());
int firstRow = (lcdSize.getRows() - lines.size()) / 2;
for (int line = 0; line < lines.size(); line++) {
String text = lines.get(line);
int firstColumn = (lcdSize.getColumns() - text.length()) / 2;
double cellY = matrixY + (firstRow + line) * charPitchY;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
double cellX = matrixX + (firstColumn + i) * charPitchX;
for (int dotRow = 0; dotRow < DotMatrixFont.GLYPH_HEIGHT; dotRow++) {
for (int dotColumn = 0; dotColumn < DotMatrixFont.GLYPH_WIDTH; dotColumn++) {
if (DotMatrixFont.isDotLit(c, dotRow, dotColumn)) {
dot.x = cellX + dotColumn * dotPitch;
dot.y = cellY + dotRow * dotPitch;
g2d.fill(dot);
}
}
}
}
}
}
@Override
public void drawIcon(Graphics2D g2d, int width, int height) {
g2d.setColor(PCB_GREEN);
@@ -340,36 +406,47 @@ public class CharacterLCD extends AbstractMakerBoard {
public enum LCDSize {
// Labels reach the BOM's value column, so they stay short; the dimensions are carried by the
// fields beside them. The bezel and lit area are measured off the module rather than derived as
// margins off the board, which made the window grow with the board.
_16x2("16x2", 80.0, 35.0, 72.2, 24.1, 64.5, 14.5),
_20x4("20x4", 98.0, 60.0, 96.8, 39.3, 77.0, 25.2);
// margins off the board, which made the window grow with the board. The last figure is where
// the parallel header starts, which the two modules do not agree on.
_16x2("16x2", 16, 2, 80.0, 36.0, 72.2, 24.1, 64.5, 14.5, 7.5),
_20x4("20x4", 20, 4, 98.0, 60.0, 96.8, 39.3, 77.0, 25.2, 10.0);
private final String label;
private final int columns;
private final int rows;
private final double widthMm;
private final double heightMm;
private final double bezelWidthMm;
private final double bezelHeightMm;
private final double displayWidthMm;
private final double displayHeightMm;
private final double parallelHeaderInsetMm;
LCDSize(String label, double widthMm, double heightMm, double bezelWidthMm,
double bezelHeightMm, double displayWidthMm, double displayHeightMm) {
LCDSize(String label, int columns, int rows, double widthMm, double heightMm,
double bezelWidthMm, double bezelHeightMm, double displayWidthMm, double displayHeightMm,
double parallelHeaderInsetMm) {
this.label = label;
this.columns = columns;
this.rows = rows;
this.widthMm = widthMm;
this.heightMm = heightMm;
this.bezelWidthMm = bezelWidthMm;
this.bezelHeightMm = bezelHeightMm;
this.displayWidthMm = displayWidthMm;
this.displayHeightMm = displayHeightMm;
this.parallelHeaderInsetMm = parallelHeaderInsetMm;
}
@Override public String toString() { return label; }
public int getColumns() { return columns; }
public int getRows() { return rows; }
public double getWidthMm() { return widthMm; }
public double getHeightMm() { return heightMm; }
public double getBezelWidthMm() { return bezelWidthMm; }
public double getBezelHeightMm() { return bezelHeightMm; }
public double getDisplayWidthMm() { return displayWidthMm; }
public double getDisplayHeightMm() { return displayHeightMm; }
public double getParallelHeaderInsetMm() { return parallelHeaderInsetMm; }
}
public enum LCDInterface {
@@ -0,0 +1,158 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
/**
* The glyphs an HD44780 character LCD paints in its 5x8 cell, one entry per printable ASCII
* character. A glyph is stored as five columns, one per dot column, with bit 0 the top row -- the
* order the controller itself clocks them out.
*
* <p>Most glyphs sit on a baseline in the seventh row and leave the eighth empty, which is where
* the cursor would sit. The ones that descend below the baseline on a real module -- the lower-case
* g, j, p, q and y, the comma, the semicolon and the underscore -- do use that eighth row, so they
* are drawn to the full eight.
*
* <p>The table is spelled out rather than sampled from a system font because a rendered project has
* to come out identical on every machine, and a font that may or may not be installed cannot
* promise that.
*
* @author Branislav Stojkovic
*/
public class DotMatrixFont {
public static final int GLYPH_WIDTH = 5;
public static final int GLYPH_HEIGHT = 8;
private static final char FIRST_CHAR = ' ';
private static final char LAST_CHAR = '~';
// what stands in for anything the table has no glyph for, as the controller's ROM does
private static final char FALLBACK_CHAR = '?';
private static final int[] GLYPH_COLUMNS = new int[] {
0x00, 0x00, 0x00, 0x00, 0x00, // space
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x14, 0x08, 0x3E, 0x08, 0x14, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x80, 0xC0, 0x40, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x80, 0x76, 0x36, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x41, 0x22, 0x14, 0x08, 0x00, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, // @
0x7E, 0x11, 0x11, 0x11, 0x7E, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x09, 0x01, // F
0x3E, 0x41, 0x49, 0x49, 0x7A, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x0C, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x3F, 0x40, 0x38, 0x40, 0x3F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x07, 0x08, 0x70, 0x08, 0x07, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x7F, 0x41, 0x41, 0x00, // [
0x02, 0x04, 0x08, 0x10, 0x20, // \
0x00, 0x41, 0x41, 0x7F, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x80, 0x80, 0x80, 0x80, 0x80, // _
0x00, 0x01, 0x02, 0x04, 0x00, // `
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x18, 0xA4, 0xA4, 0xA4, 0x7C, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x40, 0x80, 0x80, 0x7D, 0x00, // j
0x7F, 0x10, 0x28, 0x44, 0x00, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0xFC, 0x24, 0x24, 0x24, 0x18, // p
0x18, 0x24, 0x24, 0x24, 0xFC, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x1C, 0xA0, 0xA0, 0xA0, 0x7C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x08, 0x08, 0x2A, 0x1C, 0x08, // ~
};
private DotMatrixFont() {}
/** Whether the glyph for a character lights the dot at the given row and column of its cell. */
public static boolean isDotLit(char c, int row, int column) {
if (row < 0 || row >= GLYPH_HEIGHT || column < 0 || column >= GLYPH_WIDTH) {
return false;
}
char glyph = c < FIRST_CHAR || c > LAST_CHAR ? FALLBACK_CHAR : c;
return (GLYPH_COLUMNS[(glyph - FIRST_CHAR) * GLYPH_WIDTH + column] >> row & 1) != 0;
}
}
@@ -0,0 +1,134 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.diylc.common.Display;
import org.diylc.components.MakerBoardPainter;
/**
* Text as a dot-matrix screen actually shows it: folded into the character grid the panel has room
* for, then painted one lit pixel at a time out of {@link DotMatrixFont}.
*
* <p>Laying the text out is shared by every such screen; painting it is not. A graphic panel like
* the Nokia's addresses one unbroken grid of pixels, and {@link #drawText} serves those. A
* character module instead has gaps between its cells, so it walks its own cells and only borrows
* the layout.
*
* @author Branislav Stojkovic
*/
public class DotMatrixScreen {
/** A glyph and the blank column that separates it from the next, as the panel fonts are drawn. */
public static final int CELL_WIDTH = DotMatrixFont.GLYPH_WIDTH + 1;
/** Rows butt against each other; the glyph's own last row is the gap. */
public static final int CELL_HEIGHT = DotMatrixFont.GLYPH_HEIGHT;
private DotMatrixScreen() {}
/**
* What the screen spells out, broken into grid rows. A line too wide for the grid is folded at a
* space where there is one and cut where there is not, and whatever runs past the last row is
* dropped: a real panel shows what fits and nothing more.
*/
public static List<String> layOutText(Display display, String name, String value, int columns,
int rows) {
String text = MakerBoardPainter.screenText(display, name, value);
if (text == null || columns <= 0 || rows <= 0) {
return Collections.emptyList();
}
List<String> lines = new ArrayList<String>();
for (String paragraph : text.split("\n")) {
String remaining = paragraph.trim();
while (!remaining.isEmpty() && lines.size() < rows) {
if (remaining.length() <= columns) {
lines.add(remaining);
break;
}
int fold = remaining.lastIndexOf(' ', columns);
if (fold <= 0) {
fold = columns;
}
lines.add(remaining.substring(0, fold).trim());
remaining = remaining.substring(fold).trim();
}
}
return lines;
}
/**
* Paints laid-out text onto an unbroken pixel grid, centred in it. The caller has already set the
* ink colour and placed the grid; this only decides which pixels light.
*
* <p>Centring is worked out in pixels rather than whole cells. A panel addresses single pixels,
* and a line that nearly fills the grid has no whole cell to spare, so rounding to one would pin
* it against the left edge and leave the slack -- the trailing cell's blank column, plus whatever
* the cell width does not divide into the panel -- piled up on the right.
*
* @param lines text from {@link #layOutText}
* @param pixelsX pixels the panel is wide
* @param pixelsY pixels the panel is tall
* @param x left edge of the pixel grid
* @param y top edge of the pixel grid
* @param pixelWidth width of one display pixel
* @param pixelHeight height of one display pixel, which need not match its width
*/
public static void drawText(Graphics2D g2d, List<String> lines, int pixelsX, int pixelsY,
double x, double y, double pixelWidth, double pixelHeight) {
if (lines.isEmpty()) {
return;
}
// a panel runs to thousands of pixels, so the rectangle is reused rather than allocated per dot
Rectangle2D.Double pixel = new Rectangle2D.Double(0, 0, pixelWidth, pixelHeight);
// the offsets land on whole pixels because that is all the controller can address
double firstRow = Math.floor((pixelsY - lines.size() * CELL_HEIGHT) / 2.0);
for (int line = 0; line < lines.size(); line++) {
String text = lines.get(line);
// the spacer column trailing the last glyph is not part of the text, so it is not centred
// with it
int lineWidth = text.length() * CELL_WIDTH - (CELL_WIDTH - DotMatrixFont.GLYPH_WIDTH);
double firstColumn = Math.floor((pixelsX - lineWidth) / 2.0);
double cellY = y + (firstRow + line * CELL_HEIGHT) * pixelHeight;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
double cellX = x + (firstColumn + i * CELL_WIDTH) * pixelWidth;
for (int dotRow = 0; dotRow < DotMatrixFont.GLYPH_HEIGHT; dotRow++) {
for (int dotColumn = 0; dotColumn < DotMatrixFont.GLYPH_WIDTH; dotColumn++) {
if (DotMatrixFont.isDotLit(c, dotRow, dotColumn)) {
pixel.x = cellX + dotColumn * pixelWidth;
pixel.y = cellY + dotRow * pixelHeight;
g2d.fill(pixel);
}
}
}
}
}
}
}
@@ -87,6 +87,11 @@ public class Nokia5110LCD extends AbstractMakerBoard {
public static Size CORNER_RADIUS = new Size(1.5d, SizeUnit.mm);
public static Size SILK_TEXT_GAP = new Size(1.0d, SizeUnit.mm);
// The PCD8544's own addressable grid. Its pixels are not square, the glass being wider than it
// is tall by more than 84:48, so the two pitches are worked out separately.
public static final int PIXELS_X = 84;
public static final int PIXELS_Y = 48;
public static final String[] PIN_NAMES =
new String[] {"RST", "CE", "DC", "DIN", "CLK", "VCC", "BL", "GND"};
@@ -253,9 +258,14 @@ public class Nokia5110LCD extends AbstractMakerBoard {
g2d.draw(new RoundRectangle2D.Double(displayX, displayY, displayW, displayH, displayRadius,
displayRadius));
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(displayX, displayY, displayW, displayH), getScreenInk(),
getScreen(), getName(), getValueForDisplay());
// The panel has no other way to show text, so the label is drawn the way the module would
// put it there: lit pixels of its own grid rather than a smooth font laid over the glass.
int columns = PIXELS_X / DotMatrixScreen.CELL_WIDTH;
int rows = PIXELS_Y / DotMatrixScreen.CELL_HEIGHT;
g2d.setColor(getScreenInk());
DotMatrixScreen.drawText(g2d,
DotMatrixScreen.layOutText(getScreen(), getName(), getValueForDisplay(), columns, rows),
PIXELS_X, PIXELS_Y, displayX, displayY, displayW / PIXELS_X, displayH / PIXELS_Y);
// Names lie flat along the row: the strip is 3.5 mm deep and a label stood on end needs
// 4.7 mm.
@@ -29,7 +29,6 @@ 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;
@@ -346,9 +345,16 @@ public class OLEDDisplay extends AbstractMakerBoard {
g2d.setColor(ACTIVE_AREA_COLOR);
g2d.fill(new RoundRectangle2D.Double(activeX, activeY, activeW, activeH, 2, 2));
MakerBoardPainter.drawScreenText(g2d,
new Rectangle2D.Double(activeX, activeY, activeW, activeH), getPixelColor(), getScreen(),
getName(), getValueForDisplay());
// Drawn as lit pixels of the controller's own grid, which is the only way this panel can
// put text on the glass.
int pixelsX = version.getPixelsX();
int pixelsY = version.getPixelsY();
int columns = pixelsX / DotMatrixScreen.CELL_WIDTH;
int rows = pixelsY / DotMatrixScreen.CELL_HEIGHT;
g2d.setColor(getPixelColor());
DotMatrixScreen.drawText(g2d,
DotMatrixScreen.layOutText(getScreen(), getName(), getValueForDisplay(), columns, rows),
pixelsX, pixelsY, activeX, activeY, activeW / pixelsX, activeH / pixelsY);
drawPinNames(g2d, boardX, boardY);
}
@@ -405,15 +411,17 @@ public class OLEDDisplay extends AbstractMakerBoard {
// Panel and lit-area sizes belong to the display part and are shared by both of a size's
// boards. Everything belonging to a PCB is on the Layout, including where the lit area sits
// down the board, which the two 0.91" boards disagree on.
SSD1306_0_91("0.91\" SSD1306 128x32", 30.0d, 12.0d, 22.384d, 5.584d, 2.1d,
SSD1306_0_91("0.91\" SSD1306 128x32", 128, 32, 30.0d, 12.0d, 22.384d, 5.584d, 2.1d,
new Layout(35.8d, 12.0d, 1.5d, true, 0d, 0d, 5.0d, 0d, 2.35d),
new Layout(32.5d, 20.5d, 1.5d, false, 2.5d, 2.0d, 1.25d, 4.0d, 6.6d)),
SSD1306_0_96("0.96\" SSD1306 128x64", 26.7d, 19.26d, 21.74d, 11.0d, 2.48d,
SSD1306_0_96("0.96\" SSD1306 128x64", 128, 64, 26.7d, 19.26d, 21.74d, 11.0d, 2.48d,
new Layout(27.0d, 27.0d, 1.6d, false, 2.0d, 2.0d, 0.15d, 3.87d, 6.14d)),
SH1106_1_3("1.3\" SH1106 128x64", 35.4d, 23.0d, 29.42d, 14.7d, 2.99d,
SH1106_1_3("1.3\" SH1106 128x64", 128, 64, 35.4d, 23.0d, 29.42d, 14.7d, 2.99d,
new Layout(35.4d, 33.5d, 1.5d, false, 2.5d, 3.0d, 0d, 5.25d, 7.35d));
private final String label;
private final int pixelsX;
private final int pixelsY;
private final double panelWidthMm;
private final double panelLengthMm;
private final double activeWidthMm;
@@ -423,15 +431,18 @@ public class OLEDDisplay extends AbstractMakerBoard {
private final Layout spiLayout;
/** For a size sold on one board, which both headers are built onto. */
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout layout) {
this(label, panelWidthMm, panelLengthMm, activeWidthMm, activeLengthMm, activeLeftInPanelMm,
layout, layout);
Version(String label, int pixelsX, int pixelsY, double panelWidthMm, double panelLengthMm,
double activeWidthMm, double activeLengthMm, double activeLeftInPanelMm, Layout layout) {
this(label, pixelsX, pixelsY, panelWidthMm, panelLengthMm, activeWidthMm, activeLengthMm,
activeLeftInPanelMm, layout, layout);
}
Version(String label, double panelWidthMm, double panelLengthMm, double activeWidthMm,
double activeLengthMm, double activeLeftInPanelMm, Layout i2cLayout, Layout spiLayout) {
Version(String label, int pixelsX, int pixelsY, double panelWidthMm, double panelLengthMm,
double activeWidthMm, double activeLengthMm, double activeLeftInPanelMm, Layout i2cLayout,
Layout spiLayout) {
this.label = label;
this.pixelsX = pixelsX;
this.pixelsY = pixelsY;
this.panelWidthMm = panelWidthMm;
this.panelLengthMm = panelLengthMm;
this.activeWidthMm = activeWidthMm;
@@ -444,6 +455,8 @@ public class OLEDDisplay extends AbstractMakerBoard {
@Override public String toString() { return label; }
public double getPanelWidthMm() { return panelWidthMm; }
public double getPanelLengthMm() { return panelLengthMm; }
public int getPixelsX() { return pixelsX; }
public int getPixelsY() { return pixelsY; }
public double getActiveWidthMm() { return activeWidthMm; }
public double getActiveLengthMm() { return activeLengthMm; }
public double getActiveLeftInPanelMm() { return activeLeftInPanelMm; }
@@ -91,6 +91,29 @@ public class CharacterLCDTest {
MakerBoardTestSupport.assertScreenTextIsDrawn(pale, paleBlank);
}
/**
* The dot figures and the lit-area figures are measured off the module independently, so nothing
* but this keeps them consistent: a character matrix wider or taller than the glass it sits on
* means one of the two sets has drifted.
*/
@Test
public void theCharacterMatrixFitsInsideTheLitArea() {
double cellWidth = (CharacterLCD.DOT_COLUMNS - 1) * 0.6d + 0.55d;
double cellHeight = (CharacterLCD.DOT_ROWS - 1) * 0.6d + 0.55d;
for (LCDSize lcdSize : LCDSize.values()) {
double matrixWidth = (lcdSize.getColumns() - 1) * 3.55d + cellWidth;
double matrixHeight = (lcdSize.getRows() - 1) * 5.35d + cellHeight;
Assert.assertTrue(lcdSize + " matrix is " + matrixWidth + " mm wide on a "
+ lcdSize.getDisplayWidthMm() + " mm window",
matrixWidth <= lcdSize.getDisplayWidthMm());
Assert.assertTrue(lcdSize + " matrix is " + matrixHeight + " mm tall on a "
+ lcdSize.getDisplayHeightMm() + " mm window",
matrixHeight <= lcdSize.getDisplayHeightMm());
}
}
@Test
public void drawsCleanlyInEveryState() {
for (LCDSize lcdSize : LCDSize.values()) {
@@ -0,0 +1,119 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import org.junit.Assert;
import org.junit.Test;
/**
* The glyph table is 475 hand-entered figures, and a drawing that renders a character wrong looks
* like a rendering bug rather than a bad byte, so the table is checked here instead.
*/
public class DotMatrixFontTest {
// where a glyph without a descender sits; the row below it is the descender and cursor row
private static final int BASELINE_ROW = DotMatrixFont.GLYPH_HEIGHT - 2;
private static boolean[][] glyphOf(char c) {
boolean[][] dots = new boolean[DotMatrixFont.GLYPH_HEIGHT][DotMatrixFont.GLYPH_WIDTH];
for (int row = 0; row < DotMatrixFont.GLYPH_HEIGHT; row++) {
for (int column = 0; column < DotMatrixFont.GLYPH_WIDTH; column++) {
dots[row][column] = DotMatrixFont.isDotLit(c, row, column);
}
}
return dots;
}
private static boolean isBlank(char c) {
for (boolean[] row : glyphOf(c)) {
for (boolean lit : row) {
if (lit) {
return false;
}
}
}
return true;
}
/** A hole in the table would read as a blank cell on the glass, which is what this catches. */
@Test
public void everyPrintableCharacterHasAGlyph() {
for (char c = '!'; c <= '~'; c++) {
Assert.assertFalse("'" + c + "' has no glyph", isBlank(c));
}
Assert.assertTrue("a space must light nothing", isBlank(' '));
}
/**
* Spot-check against a glyph whose shape is unambiguous: a capital H is two uprights from the top
* row down to the baseline, joined by a bar across the middle. A table shifted by one entry fails
* here.
*/
@Test
public void theGlyphsAreTheRightWayUpAndRound() {
boolean[][] h = glyphOf('H');
for (int row = 0; row <= BASELINE_ROW; row++) {
Assert.assertTrue("left upright, row " + row, h[row][0]);
Assert.assertTrue("right upright, row " + row, h[row][4]);
// the bar joins them across the middle row only
Assert.assertEquals("middle column, row " + row, row == 3, h[row][2]);
}
}
/**
* The eighth row belongs to the descenders and to the cursor. A capital letter or a digit that
* reached into it would collide with the cursor on a real module, and one of the five lower-case
* descenders that did not reach it would be the squashed seven-row font this replaced.
*/
@Test
public void onlyTheDescendersReachTheBottomRow() {
String descenders = "gjpqy,;_";
for (char c = ' '; c <= '~'; c++) {
boolean reaches = false;
for (int column = 0; column < DotMatrixFont.GLYPH_WIDTH; column++) {
reaches |= DotMatrixFont.isDotLit(c, BASELINE_ROW + 1, column);
}
Assert.assertEquals("'" + c + "' in the bottom row", descenders.indexOf(c) >= 0, reaches);
}
}
@Test
public void nothingIsLitOutsideTheGlyph() {
for (char c : new char[] {' ', 'A', 'g', '0', '~'}) {
Assert.assertFalse("row past the cell",
DotMatrixFont.isDotLit(c, DotMatrixFont.GLYPH_HEIGHT, 0));
Assert.assertFalse("row above", DotMatrixFont.isDotLit(c, -1, 0));
Assert.assertFalse("column past the glyph",
DotMatrixFont.isDotLit(c, 0, DotMatrixFont.GLYPH_WIDTH));
Assert.assertFalse("column before the glyph", DotMatrixFont.isDotLit(c, 0, -1));
}
}
/** Anything the table has no glyph for still has to print something. */
@Test
public void charactersOutsideTheTableFallBack() {
for (char c : new char[] {'\u00e9', '\u0416', (char) 0x1F, (char) 0x7F}) {
Assert.assertArrayEquals("'" + c + "' should print as a question mark", glyphOf('?'),
glyphOf(c));
}
}
}
@@ -0,0 +1,140 @@
/*
*
* DIY Layout Creator (DIYLC).
* Copyright (c) 2009-2025 held jointly by the individual authors.
*
* This file is part of DIYLC.
*
* DIYLC is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* DIYLC is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with DIYLC. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.diylc.components.displays;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.util.List;
import org.diylc.common.Display;
import org.junit.Assert;
import org.junit.Test;
/**
* Folding the label into the character grid, which the character LCD and both graphic panels now
* share. A panel shows what fits and nothing else, so the rules about what gets dropped matter.
*/
public class DotMatrixScreenTest {
private static List<String> lines(String name, String value, int columns, int rows) {
return DotMatrixScreen.layOutText(Display.BOTH, name, value, columns, rows);
}
@Test
public void textThatFitsIsLeftAlone() {
Assert.assertEquals(List.of("LCD1", "16x2", "Parallel HD44780"),
lines("LCD1", "16x2, Parallel HD44780", 16, 4));
}
/** The value arrives as one string per property, and each starts its own row. */
@Test
public void everyPropertyStartsItsOwnRow() {
Assert.assertEquals(List.of("0.96\" SSD1306", "128x64", "I2C"),
DotMatrixScreen.layOutText(Display.VALUE, "OLED1", "0.96\" SSD1306, 128x64, I2C", 21, 8));
}
@Test
public void aLongLineFoldsAtASpace() {
Assert.assertEquals(List.of("LCD1", "Parallel", "HD44780"),
lines("LCD1", "Parallel HD44780", 8, 4));
}
/** A word with no space in it has to be cut, or it would run off the glass. */
@Test
public void aWordTooWideIsCut() {
Assert.assertEquals(List.of("ABCDE", "FGHIJ", "KL"),
DotMatrixScreen.layOutText(Display.NAME, "ABCDEFGHIJKL", null, 5, 4));
}
/** What runs past the last row is dropped rather than shrunk or scrolled. */
@Test
public void whatRunsPastTheLastRowIsDropped() {
Assert.assertEquals(List.of("LCD1", "16x2"), lines("LCD1", "16x2, Parallel HD44780", 16, 2));
}
/**
* Renders onto a one-pixel-per-pixel grid and reports the margins the text leaves either side,
* which is the only way to see where it actually landed.
*/
private static int[] margins(List<String> lines, int pixelsX, int pixelsY) {
BufferedImage image = new BufferedImage(pixelsX, pixelsY, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
g2d.setColor(Color.WHITE);
DotMatrixScreen.drawText(g2d, lines, pixelsX, pixelsY, 0, 0, 1, 1);
g2d.dispose();
int first = -1;
int last = -1;
for (int x = 0; x < pixelsX; x++) {
for (int y = 0; y < pixelsY; y++) {
if ((image.getRGB(x, y) & 0xFFFFFF) != 0) {
first = first == -1 ? x : first;
last = x;
break;
}
}
}
Assert.assertNotEquals("nothing was drawn", -1, first);
return new int[] {first, pixelsX - 1 - last};
}
/**
* A line that nearly fills the panel used to be pinned against the left edge: centring rounded to
* whole cells, and a cell is a glyph plus a blank column, so the slack all ended up on the right.
* The 0.96" OLED's own label is exactly that case, 20 characters on a 21-column panel.
*/
@Test
public void aNearlyFullLineIsStillCentred() {
int[] m = margins(List.of("0.96\" SSD1306 128x64"), 128, 64);
Assert.assertTrue("left " + m[0] + " vs right " + m[1], Math.abs(m[0] - m[1]) <= 1);
Assert.assertTrue("should not touch the edge, left margin was " + m[0], m[0] > 0);
}
/** The Nokia's label is 13 characters on a 14-column panel, the same case one column narrower. */
@Test
public void theNokiaLabelIsCentred() {
int[] m = margins(List.of("84x48 PCD8544"), 84, 48);
Assert.assertTrue("left " + m[0] + " vs right " + m[1], Math.abs(m[0] - m[1]) <= 1);
Assert.assertTrue("should not touch the edge, left margin was " + m[0], m[0] > 0);
}
/** A short line has whole cells to spare, and must come out centred just the same. */
@Test
public void aShortLineIsCentred() {
int[] m = margins(List.of("I2C"), 128, 64);
Assert.assertTrue("left " + m[0] + " vs right " + m[1], Math.abs(m[0] - m[1]) <= 1);
}
@Test
public void aScreenTurnedOffPrintsNothing() {
Assert.assertTrue(
DotMatrixScreen.layOutText(Display.NONE, "LCD1", "16x2", 16, 2).isEmpty());
}
/** A grid with no room at all must not throw its way out of a repaint. */
@Test
public void anEmptyGridPrintsNothing() {
Assert.assertTrue(lines("LCD1", "16x2", 0, 2).isEmpty());
Assert.assertTrue(lines("LCD1", "16x2", 16, 0).isEmpty());
}
}
+2 -2
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.6.0</version>
<version>6.7.0</version>
</parent>
<artifactId>diylc-swing</artifactId>
@@ -116,7 +116,7 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.1</version>
<version>2.22.3</version>
</dependency>
<dependency>
<groupId>com.oracle</groupId>
+619
View File
@@ -0,0 +1,619 @@
# Audio Coverage Roadmap
Closing the gap between the component library and what the guitar, amplifier, pedal and hi-fi
communities actually build.
Status: proposed
Scope: `diylc-library` (`components.passive`, `.tube`, `.electromechanical`, `.guitar`,
`.connectivity`), one bug fix in `diylc-core/serialization`
Companion: the sibling roadmaps in this directory, which apply the same method to other packages
## 1. Goal
The audio side of the library has never had a systematic coverage pass, and it is the side the
sample corpus is made of. The headline number: **three components in the `Tubes` category**,
against 50 corpus projects that place a tube socket.
This document inventories what ships, measures what people hand-draw because it does not exist,
and sorts the additions into three tiers by evidence of demand rather than by appetite.
## 2. Decisions that govern this roadmap
| # | Decision | Choice | Rationale |
|---|---|---|---|
| D1 | What counts as a gap | **A part people are drawing by hand today.** Measured from label text in `diylc-regression-data/input`, not from catalogue browsing. | Keeps the roadmap honest. Several things that looked like gaps from the palette listing (pedal enclosures, EMG pickups, Filtertrons, Tele single coils, the switch-type matrix) turned out to ship already; several things that look exotic turn out to be in 25 projects. |
| D2 | Evidence vs. judgement | **Tiers 1 and 2 are measured. Tier 3 is reasoned and labelled as such.** | The corpus is overwhelmingly pedal and amp. It cannot speak to hi-fi demand at all, and saying so is better than inventing a number. See §4.3. |
| D3 | New class vs. new enum constant | **Same footprint family → enum constant; different structure → new class.** | Already the established paradigm, and `diylc/CLAUDE.md` states the preference directly: a `Version` property on one class beats a class per revision. |
| D4 | Where iron lives | **One `Transformer` class with a `Type` property (power / output / choke), in `components.passive`.** Not three classes. | A bell-end power transformer, an output transformer and a choke share an outline, a mounting footprint and a tap-strip; they differ in tap count and label. `AudioTransformer` already sits in `passive` and already extends `AbstractMultiPartComponent`, so the neighbour is right there. |
| D5 | Backward compatibility | Append-only enums, unchanged defaults, unchanged control-point counts for existing variants. | `.diy` files are a public contract. Only §6.1 touches serialization, and it *restores* compatibility rather than risking it. |
| D6 | Pre-set data before components | **Where a variant or a datasheet row closes a gap, it ships before any new class.** | Both are configuration rather than Java, and both are already covered by tests. §4.2 shows which of the most-placed parts have neither. |
| D7 | Composable parts are not components | **If a part can be assembled from existing components and saved as a building block, it ships as a factory building block, not a new class.** | A new class has to be drawn, transformed, tested, BOM-policed and supported forever, and it crowds the toolbox. A building block is data. The test is whether the assembly is electrically and visually honest without a new primitive: a push-pull pot is a pot plus a DPDT switch and passes, a chassis transformer is not a rectangle plus lugs and fails. See §5.4. |
### Explicitly out of scope
- The dormant classes outside the packages in scope above. They were parked deliberately
("comment out future stuff", `27eb2599`) and belong to the roadmaps for their own packages.
- Any electrical simulation: load-line *plotting* is in scope only because the component already
exists (§6.2), impedance and frequency-response maths are not.
- Speaker cabinet and enclosure mechanical drawing beyond what `ChassisPanel` already does.
- Bumping the project version, packaging, and the deployment build.
## 3. Current coverage
**169 components register at runtime.** 208 classes carry a `@ComponentDescriptor`, but 39 have it
commented out and are invisible to the annotation scan.
| Category | Active | | Category | Active |
|---|---|---|---|---|
| Schematic Symbols | 30 | | Semiconductors | 12 |
| Electro-Mechanical | 28 | | Guitar | 12 |
| Passive | 17 | | Boards | 12 |
| Connectivity | 16 | | Misc | 10 |
| Displays & Outputs | 13 | | Controllers | 10 |
| Shapes | 4 | | **Tubes** | **3** |
| SMD | 2 | | | |
What is genuinely strong and should not be touched: the switch matrix (`ToggleSwitchType` runs SPST
through 5PDT with both on/on/on variants; sealed and open rotaries; Lever, LP, Freeway 3x3,
Schaller Megaswitch in six types, S1), the pickup models (`SingleCoilPickup` does Strat and Tele
bodies with rod/rail/dual-rail poles, `HumbuckerPickup` does PAF/Mini/Filtertron with an EMG-style
variant), and resistor coverage (22 variants from 1/4 W carbon comp to 100 W ceramic).
### 3.1 Dormant classes relevant to audio
Five of the 39 fall inside this roadmap's scope. Two are finished code behind a commented
annotation; three are not.
| Class | State | To revive |
|---|---|---|
| `passive/AirCoreInductor` | Body live, 4 commented descriptor lines | Uncomment + restore 4 imports |
| `passive/BoxTrimmer` | Body live, 9 commented lines, `BoxTrimmerTransformer` exists | Uncomment + restore imports |
| `misc/Loadline` | Body live, 4 commented lines, extends `AbstractBoard` | Uncomment + restore imports |
| `passive/AxialAirCoreInductor` | **Stub** — 69 of 70 lines commented, and it extended `AbstractFilmCapacitor` | Rewrite, or delete the file |
| `smd/SOT23Package` | **Stub** — 427 of 449 lines commented | Out of scope here |
## 4. Evidence
### 4.1 Method
`diylc-regression-data/input` holds 709 `.diy` projects (cloud 379, v1 284, user-files 34,
diy-fever 12). The 284 v1-format files carry no class names, so component tallies reflect the 425
modern files; label-text searches cover all 709.
**Searching raw file text does not work.** `Image` components store base64, and three-letter
queries hit it: `XLR` returned 17 "files" of which zero were real. Every figure below comes from
extracting `<text>`, `<title>`, `<name>`, `<value>` and `<description>` element contents first
(118,619 strings) and searching those.
### 4.2 Two coverage mechanisms, and which parts have neither
Pre-set data reaches a component two ways, and they are easy to confuse.
**Variants** (`variants.xml`, 176 of them) are saved property sets, authored per class, listed under
*Apply Variant*.
**Datasheets** are parametrized part catalogues: a `<ClassName>.datasheet` CSV on the classpath, one
row per real orderable part, listed under *Apply Model*. Five capacitor classes have one, and
between them they carry **3,539 rows covering 34 models** — each row a part number, a voltage, a
capacitance, exact body dimensions in mm, and body/border/label/marker colours:
| Class | Rows | Models |
|---|---|---|
| `RadialFilmCapacitor` | 1,523 | 14 (WIMA MKS-4/MKP-4/FKP/FKS, Orange Drop 225P/715P/716P, Epcos MKT) |
| `RadialElectrolytic` | 833 | 7 (Panasonic FC/FM/NHG, Nichicon FW/FG/KZ, Elna Silmic II) |
| `AxialElectrolyticCapacitor` | 455 | 4 (F&T Typ A, Sprague TVA Atom, Illinois TTA, JJ ANH) |
| `RadialMicaCapacitor` | 433 | 7 (Cornell Dubilier CD10 through CD42, CDV19/CDV30) |
| `AxialFilmCapacitor` | 293 | 2 (Solen Fast, Mallory 150) |
**Capacitors are therefore already covered, at a fidelity no variant could reach** — exact
dimensions per value and voltage, not a size estimate. Any roadmap item proposing to "add
Mallory 150 / Orange Drop / WIMA variants" is proposing something that shipped long ago; an
earlier draft of this document made exactly that mistake by counting only `variants.xml`.
The gap is the classes with *neither* mechanism:
| Component | Placements | Variants | Datasheet |
|---|---|---|---|
| `Resistor` | 2,388 | 22 | — |
| `RadialFilmCapacitor` | 754 | 0 | 1,523 rows |
| `RadialElectrolytic` | 602 | 0 | 833 rows |
| `TransistorTO92` | 363 | 0 | **none** |
| `DiodePlastic` | 324 | 0 | **none** |
| `RadialCeramicDiskCapacitor` | 283 | 0 | **none** |
| `PotentiometerPanel` | 195 | 2 | **none** |
`RadialCeramicDiskCapacitor` is the odd one out among the capacitors — the only one of the six with
no datasheet and no variants, despite 283 placements. `TantalumCapacitor` and `MultiSectionCapacitor`
are likewise uncovered.
The opportunity is that **the datasheet mechanism is generic and nothing outside capacitors uses
it.** `IDatasheetSupport` is a one-method interface in `diylc-core` implemented by exactly those five
classes; see §5.3 for what extending it costs.
### 4.3 Hand-written labels for parts that do not exist
Out of 709 projects:
| Part | Files | Mentions | Status in library |
|---|---|---|---|
| Output transformer (`OT`, "output transformer") | 27 / 10 | 84 / 10 | **absent** |
| Power transformer (`PT`, "power transformer") | 15 / 12 | 37 / 25 | **absent** |
| Speaker | 25 | 45 | **absent**, not even a symbol |
| Choke | 21 | 38 | **absent** |
| 3PDT / stomp / footswitch | 12 / 6 / 5 | 12 / 8 / 8 | electrically yes, body no |
| Push-pull pot | 12 | 15 | **absent** |
| Standby | 10 | 31 | covered by toggle |
| LDR | 10 | 15 | **absent** as a discrete part |
| Reverb tank | 6 | — | **absent** |
| 1590-series enclosure | 5 | 5 | covered — `ChassisPanel` variants |
Corroborating signals: 15 files assemble a transformer by hand from `TransformerCoil` pieces, and
88 fall back to `shapes.Rectangle`.
**Zero hits**, and therefore Tier 3 at best: `xlr`, `binding post`, `piezo`, `jazzmaster`,
`lipstick`, `varitone`, `concentric`, `vactrol`. The corpus is pedals and amps; its silence on
hi-fi connectors is a property of the sample, not evidence of no demand.
### 4.4 Why `EyeletBoard` is used 8 times and discrete turrets 40
`EyeletBoard.draw()` paints a full grid of eyelets with `fillOval`. They are decorative: no control
points, no node names, no conductive areas, nothing in the netlist. So builders ignore it and place
individual `Turret` components, which do carry a node name, on a `BlankBoard`.
This kills the obvious-looking fix. A `Type = Turret` flag on `EyeletBoard` would inherit the same
flaw, and a full grid is wrong for turret boards anyway — they are drilled per design. The real fix
is selectable, connected positions on both boards, which is a `composite-building-blocks.md`-scale
design question. Tier 3, §8.6.
## 5. Shared mechanics
### 5.1 Component discovery
Classpath annotation scan in `ComponentProcessor.getComponentTypes()`. **Any class carrying
`@ComponentDescriptor` is picked up automatically** — no manifest, no registry. Uncommenting an
annotation is the entire registration step.
### 5.2 Serialization safety
Per `diylc/CLAUDE.md`: never rename or delete a serialized field or change its type; fields added
to an existing class deserialize as `null` in old files, so default in the getter, not the
constructor; `serialVersionUID` stays `1L`.
Worth knowing before touching `ProjectFileManager`: its anonymous `MapperWrapper` overrides
`shouldSerializeMember` and drops elements that map to no field, so **unknown fields in old files
are already tolerated**. It does *not* override `realClass`, so an **unknown class throws**. That
asymmetry is the whole of §6.1.
### 5.3 Variant and datasheet mechanics
**Variants** live in `diylc-core/src/main/resources/import-defaults/variants.xml`, an
XStream-serialized `Map<String, List<Template>>` keyed by fully-qualified class name.
`FactoryVariantsTest` (`diylc-library/src/test/java/org/diylc/components/`) loads it, resolves every
key, and asserts every template names a live property and applies cleanly — so a stale variant fails
the build rather than failing silently at runtime.
**Datasheets** are cheaper than they look. `DatasheetService.loadDatasheet(Class)` resolves
`/datasheets/<SimpleClassName>.datasheet` off the classpath and splits each line on `\s*,\s*`.
Adding one to a component is therefore two steps and no registration:
1. implement `IDatasheetSupport.applyModel(String[] model)` on the component, mapping columns onto
setters — `RadialFilmCapacitor.applyModel` is 20 lines and the model to copy;
2. drop `<ClassName>.datasheet` into `diylc-library/src/main/resources/datasheets/`.
`ComponentPopupMenu.updateApplyModelMenu` then builds the *Apply Model* submenu from
`ComponentType.getDatasheet()` automatically, and `Presenter.applyModelToSelection` routes the
chosen row back to `applyModel`.
Two cautions, both learned from §6.5:
- **The column format is positional and unforgiving.** `CapacitorDatasheetService` reads
`model[1].split(" ")[0..1]` for voltage and `model[2].split(" ")[0..1]` for value, so a cell
must be `"<number> <unit>"` with exactly one space. `4.7uF` throws
`ArrayIndexOutOfBoundsException`; there is no validation anywhere.
- **`DatasheetService` reads with `new InputStreamReader(in)` and no charset**, so files are decoded
with the platform default. Keep datasheet files ASCII — write `uF`, never `µF`. Per JEP 400 the
default is UTF-8 on the supported JVM, so a latin-1 byte becomes U+FFFD rather than failing loudly.
There is also an unused path worth knowing about: `DatasheetService.lookup` /
`CapacitorDatasheetService.lookup` match a datasheet row by approximate value within a tolerance,
for auto-sizing a component from its value. **It has no production caller** — only
`CapacitorDimensionServiceTests` — so it is latent infrastructure, not live behaviour.
### 5.4 Factory building block mechanics
Building blocks are named groups of components, saved from a selection and re-placed as a unit.
`BuildingBlockManager.importDefaultBlocks()` ships a default set in
`diylc-core/src/main/resources/import-defaults/blocks.xml`, stored as a
`Map<String, List<IDIYComponent<?>>>` using the `diylc` package alias
(`aliasPackage("diylc", "org.diylc.components")`), and merges it into the user's own blocks by name.
Three ship today: `TO-92 w/ 3 Leads`, `TO-92 w/ 2 Leads`, `Title Block`.
Authoring one is a UI job, not a code job: place the components, select them, *Save as Building
Block*, then lift the entry out of the user config into `blocks.xml`. Per D7 this is the default
route for anything composable.
**The blocker (fixed in §6.6).** `importDefaultBlocks()` is wrapped in a one-shot flag:
```java
if (!configManager.readBoolean(IPlugInPort.DEFAULT_BLOCKS_IMPORTED_KEY, false)) {
```
Once `defaultBlocksImported` is true it never runs again, so **a factory block added to `blocks.xml`
reaches new installs only** — never anyone who has already launched the app. Any plan that
distributes composites this way needs §6.6 first.
`VariantManager.importDefaultVariants()` does not have this problem, and the asymmetry is worth
knowing: it keeps a `defaultTemplatesImportDate` watermark, re-reads `variants.xml` on every start,
and imports any variant whose `Template.createdOn` is newer than the watermark and whose name the
user does not already have. New factory *variants* do reach existing users today.
### 5.5 Tests
JUnit 4 under `diylc-library/src/test/java/org/diylc/`, with `components/passive/`,
`components/misc/` and `netlist/` already populated. Switching and netlist behaviour is unit-tested
(`AbstractSwitchTests` and its fifteen subclasses); component rendering is not — that is the
regression suite's job. Any new class with switching belongs in `netlist/`.
### 5.6 Build and verify
```bash
cd diylc
mvn -pl diylc-library -am test
```
Baseline at the time of writing, on `controllers` at `2a08fbe5`: `diylc-core` 148 tests,
`diylc-library` 331 tests, BUILD SUCCESS, project version 6.6.0.
Everything in Tiers 1 and 2 affects rendering or serialization, so each item also wants a
regression run (`.claude/skills/regression-test/SKILL.md`) before it is called done. §6.1
specifically cannot be verified any other way.
### 5.7 Changelog
User-visible additions get a `<Change>` entry in `diylc-core/src/main/resources/update.xml`, in the
block for the unreleased version. `NEW_FEATURE` for components and variants, `BUG_FIX` for §6.1.
---
## 6. Tier 1 — no new components
Enum constants, catalogue data, building blocks and uncommenting. Cheapest fidelity per hour in
the roadmap, and where D6 and D7 send most of the work.
§6.1, §6.5 and §6.6 are correctness fixes and should go first regardless of the rest; §6.6 also
gates §6.7, which is the channel the composable parts travel through.
### 6.1 Restore the `ElectrolyticCanCapacitor` alias — **bug, not a gap**
`diylc-regression-data/input/cloud/diy/282.diy` contains three
`<org.diylc.components.passive.ElectrolyticCanCapacitor>` elements. That class exists nowhere in
the tree, in any module, or in the vendored jars, and `ProjectFileManager.configure()` registers no
alias for it. Per §5.2 the file cannot deserialize. It came from the cloud corpus, so real user
files are affected.
This is the exact failure `diylc/CLAUDE.md` forbids: a shipped component class was renamed or
removed without an XStream alias.
The old element carries `name`, `alpha`, `value` (four nulls), `orientation`, `controlPoints`
(three), `bodyColor`, `baseColor`, `borderColor`, `labelColor`, `display`, `sections` (`_1`) and
`diameter` (1.0 in). `MultiSectionCapacitor` is the successor and matches all of it except
`sections`, which it does not have — it derives section count from `value.length`.
1. In `ProjectFileManager.configure()`, beside the existing aliases:
```java
xStream.alias("org.diylc.components.passive.ElectrolyticCanCapacitor",
MultiSectionCapacitor.class);
```
2. Confirm `ClassAliasingMapper` wins over class lookup for a dotted element name. If it does not,
fall back to restoring a `@Deprecated` `ElectrolyticCanCapacitor` that extends
`MultiSectionCapacitor` and carries no descriptor — invisible in the palette, resolvable by
XStream.
3. `sections` is dropped by the §5.2 mapper, so the file loads but renders four sections where it
said one. Decide whether that drift is acceptable or whether it needs a converter that maps
`sections` onto `value` length. **Recommend accepting it** — the old field disagreed with its own
`value` array, so there is no correct answer to preserve.
4. Verify by loading `282.diy`. A regression run is the only real check.
**~2 h.** Gate the rest of Tier 1 behind this.
### 6.2 Revive the three live dormant classes
Per §3.1, `AirCoreInductor`, `BoxTrimmer` and `Loadline` are finished code behind a commented
annotation. `AirCoreInductor` needs `ComponentDescriptor`, `CreationMethod`, `IDIYComponent` and
`SimpleComponentTransformer` (the last lives in `diylc-core`) re-imported; the other two need their
own commented imports restored the same way.
Each one: uncomment, restore imports, compile, check it appears in the palette and draws. `Loadline`
is the most interesting of the three — tube and transistor load-line plotting is a design feature
no other item here provides, and it is already written.
Then decide `AxialAirCoreInductor`'s fate. It is a 70-line stub that extended `AbstractFilmCapacitor`,
which is wrong for an inductor. **Recommend deleting the file** and treating axial air-core as a
variant of a revived `AirCoreInductor` if it is wanted at all; a commented-out stub inheriting from
the wrong base is worse than nothing.
**~3 h for three, plus a decision on the fourth.**
### 6.3 Tube socket bases
`TubeSocket.Base` covers B9A, OCTAL, B7G and B12C. `Jumbo4PinTubeSocket` is a separate class scoped
by its own description to 211/805/845. Missing bases the amp community uses constantly:
| Base | Pins | Tubes |
|---|---|---|
| UX4 | 4 | 300B, 2A3, 45, 5U4G |
| UX5 | 5 | type 80, 807 |
| B8G Loctal | 8 | 7N7, 7F7 |
| B9D Magnoval | 9 | EL509, PL519 |
UX4 is the valuable one and the jumbo socket does not substitute — wrong diameter, wrong spacing.
The class is clean to extend: `updateControlPoints()` switches on `base` for pin count, spacing and
a `hasEmptySpace` flag, `getBody()` switches for body diameter, and `draw()` carries per-base
keyway branches. **Both switches end in `default: throw new RuntimeException`, so a constant added
without touching both crashes at runtime rather than failing to compile.** Add the constant, the
`Size` constants, and a case in each switch.
One geometry wrinkle: UX4 and UX5 have two large filament pins and two or three small ones, so pin
diameter becomes per-pin where today it is per-socket. Scope that before committing to UX4.
Dimensions must come from datasheets, not from this table. **~6-8 h for all four, UX4 first.**
### 6.4 Datasheets for the uncovered high-usage parts
Per §4.2 and D6, and **not** the capacitor work an earlier draft of this document proposed — that
ships already. The parts with neither mechanism, in placement order:
| Class | Placements | What it needs |
|---|---|---|
| `TransistorTO92` | 363 | BC547/BC557, 2N3904/2N3906, J201, MPF102, BC108, with pinout per part |
| `DiodePlastic` | 324 | 1N4148, 1N400x, 1N5817 |
| `RadialCeramicDiskCapacitor` | 283 | NPO/X7R by value and voltage, with disc diameter |
| `PotentiometerPanel` | 195 | Alpha/Bourns 16 mm and 24 mm bodies by resistance and taper |
`RadialCeramicDiskCapacitor` is the natural first one: it is a capacitor, so
`CapacitorDatasheetService` already knows its column layout, and it is the only capacitor class of
the six most-placed with no coverage at all.
`TransistorTO92` is the most valuable and the most work, because a transistor datasheet needs a
pinout column that the capacitor format has no equivalent for — the existing E-C-B / E-B-C options
would have to become a datasheet column. Scope that before starting.
Per §5.3 each one is an `applyModel` implementation plus a CSV. Keep every cell ASCII and every
value `"<number> <unit>"`.
**~6-10 h each, `RadialCeramicDiskCapacitor` first.**
### 6.5 Fix six malformed datasheet rows — **bug**
Six of the 3,539 rows have a value cell the parser cannot read. Found by validating every row
against `parseCapacitorDatasheet`'s own logic:
| File | Rows | Cell | Failure |
|---|---|---|---|
| `RadialElectrolytic.datasheet` | 4 (Elna Silmic II 4.7 uF at 25/35/50/63 V) | `4.7uF` | no space, so `split(" ")` yields one element and `capacitanceParts[1]` throws `ArrayIndexOutOfBoundsException` |
| `RadialFilmCapacitor.datasheet` | 2 (WIMA MKS-4 400 V) | `0.01µF A`, `0.01µF B` | latin-1 `0xb5` in a file read with the platform charset (§5.3), no space, plus an ` A`/` B` suffix no other row has; `parseCapacitance` throws `IllegalArgumentException` |
User-visible effect: picking any of those six models from *Apply Model* throws instead of applying.
Latent effect: the same cells break `DatasheetService.lookup`, whose `Double.parseDouble` runs over
every row in a `model|voltage` bucket. Three of the five affected buckets have the bad row first, so
the whole bucket would throw — **80 rows across 5 buckets, including all 41 WIMA MKS-4 400 V rows**.
That path has no production caller today (§5.3), so this is a trap set for whoever wires up
value-based auto-sizing, not a live fault.
1. Rewrite the four `4.7uF` cells as `4.7 uF`.
2. Replace the two `0.01µF A` / `0.01µF B` cells with ASCII. The ` A`/` B` suffix looks like an attempt
to give one value two body sizes (10x3 at 7.5 mm pitch, 13x4 at 10 mm pitch) — a real need the
positional format cannot express, since model and voltage are the whole lookup key. **Recommend
splitting them into two models**, `WIMA MKS-4` and `WIMA MKS-4 (10mm)`, matching how
`Epcos MKT (5mm)` / `(7.5mm)` / `(10mm)` and the WIMA 2.5 mm/5 mm entries already encode pitch in
the model name.
3. Add this row validation as a test beside `FactoryVariantsTest` — same shape, same
collect-all-problems-then-assert style — so the next malformed cell fails the build. **This is
the part that matters**; the six fixes are typos, the missing validation is why they survived.
**~3 h including the test.**
### 6.6 Make factory building blocks reach existing users — **bug, and a prerequisite**
Per §5.4, `importDefaultBlocks()` runs once per installation and never again, so every factory
building block this roadmap proposes would land only on fresh installs. D7 routes most composable
parts through that mechanism, so this is a prerequisite for §6.7, §7.5 and §8.9, not a nicety.
Two ways to fix it, in `BuildingBlockManager`:
**Recommended — drop the guard.** Delete the `DEFAULT_BLOCKS_IMPORTED_KEY` check and let the
existing merge run on every start. The merge body already skips any name the user has
(`if (!blocksMap.containsKey(entry.getKey()))`), so user blocks and user edits survive untouched and
new factory names simply appear. Leave the flag being written so a downgrade still sees it.
One behavioural cost, which should be a deliberate choice rather than a surprise: a user who
*deletes* a factory block gets it back on the next launch. Mitigate by recording deleted factory
names in config if that turns out to annoy anyone — not before.
**Thorough — mirror the variant watermark.** Copy `VariantManager.importDefaultVariants()`: a
`defaultBlocksImportDate` watermark plus a per-block `createdOn`. The obstacle is that `blocks.xml`
is a `Map<String, List<IDIYComponent<?>>>` and `BuildingBlock` — which *does* have a name field but
is not used in the stored format at all — carries no timestamp. Adding one means changing the shape
of the `BLOCKS_KEY` config value, which also holds the user's own blocks. That is a config-format
migration for a gain the simpler option already delivers.
Take the recommended option. Extend `BuildingBlockManagerTest` (8 tests, `diylc-core`) with a case
asserting that a second `importDefaultBlocks()` on a config that already has the flag set still
picks up a newly added default name. **~3 h.**
### 6.7 Ship the first factory building blocks
Data, not code, and per D7 this is where composable parts go. Each is authored in the app and lifted
into `blocks.xml`. Gated on §6.6.
| Block | Built from | Why |
|---|---|---|
| Push-pull pot | `PotentiometerPanel` + `MiniToggleSwitch` (DPDT) | 12 files, 15 mentions — the highest-demand composable in the corpus |
| Transformer symbol | `TransformerCoil` x2 + `TransformerCore` | 15 files already hand-assemble exactly this; closes most of §8.1's transformer-symbol gap for free |
| Treble bleed | `Resistor` + `RadialFilmCapacitor` | Named network every guitar builder adds; trivial to author |
| Tube gain stage | `TubeSocket` + grid stopper + cathode R/C + plate R | 50 corpus files place a tube socket and then rebuild this by hand every time |
| Strat / Tele / LP harness | `guitar` pickups + pots + switch + jack | `guitar` components appear in only 8 of 425 files, which may be discoverability as much as demand |
Author the push-pull pot first: it is the one with measured demand, and it is the worked example that
proves D7 before the rest follow. **~2-3 h each once §6.6 lands.**
---
## 7. Tier 2 — new components, ranked by measured demand
Parts that cannot be composed from what already ships, so D7 does not apply to them. §7.6 is
the exception and is kept only to record why it was declined.
### 7.1 `Transformer` — the largest single gap
Per D4: one class in `components.passive` extending `AbstractMultiPartComponent`, as
`AudioTransformer` does, with a `Type` enum of Power / Output / Choke, a bell-end or open-frame
body, and a configurable tap count per winding.
What the absence costs today: no BOM line, no terminals, no netlist presence, and 15 projects
assembling one by hand out of `TransformerCoil` pieces. Every tube amp has two or three.
Needs: `startTrackingContinuityArea` around the tap strip, mirroring a neighbouring multi-part
component exactly; an `IComponentTransformer` for rotation; `drawIcon()` hand-drawn for the
toolbox; `bomPolicy = SHOW_ONLY_TYPE_NAME` with the type and tap count in the variant label.
A matching schematic symbol is §8.1. Ship the physical part first — the corpus demand is for
layouts. **~16-20 h.**
### 7.2 `Speaker`
Physical component plus a schematic symbol, 25 files and 45 hand-written mentions, and the only
item here that serves amp and hi-fi equally. Round frame with a configurable diameter, two
terminals, an impedance property. The symbol is a few hours; the physical part is a day.
**~10-12 h for both.**
### 7.3 Chassis `Choke`
21 files, 38 mentions. Once §7.1 exists this is largely its Choke type plus a single-winding body,
which is the main argument for building it as part of the `Transformer` class rather than beside it.
`RadialInductor` and `ToroidalInductor` are PCB parts and do not substitute. **~4 h if it rides on
§7.1, ~10 h standalone.**
### 7.4 `Footswitch`
`MiniToggleSwitch` already provides 3PDT electrically, but draws as a rounded-rectangle mini toggle.
A stomp switch is a round body with a 3x3 lug grid, and pedal layouts are built around that
geometry. ~20 files reference 3PDT, stomp or footswitch by hand.
New class in `electromechanical`, reusing `ToggleSwitchType` so the netlist behaviour and the
existing `MiniToggleSwitchTests` patterns carry over. Add a momentary SPST variant for
non-latching relay bypass. Needs a `netlist/` test per §5.5. **~10-12 h.**
### 7.5 `Photoresistor`, and `Vactrol` as a building block
10 files, 15 mentions, and nothing discrete in the library — the only LDR class, `LDRSensorModule`,
is a dormant breakout board rather than the bare part. Blocks tremolo, optical compressors and Uni-Vibe-style circuits.
Split by D7:
- **`Photoresistor`** is a new component. It is a two-lead primitive with its own body and no
existing part to compose it from, so it has to be drawn. Extends `AbstractLeadedComponent`.
- **Vactrol** is then an `LED` plus a `Photoresistor`, which is a factory building block
(§6.7), not a class. A real vactrol is exactly those two parts in opaque heatshrink, so the
assembly is honest.
**~5 h for the component, then an hour to author the block.**
### 7.6 Push-pull pot — **declined as a component**
Kept here as a record of the decision, because it is the case D7 exists for.
12 files and 15 mentions make this the best-evidenced composable part in the corpus, and an earlier
draft of this roadmap proposed a `PushPullPotentiometer` class extending `AbstractPotentiometer`.
That is the wrong answer. A push-pull pot is a `PotentiometerPanel` and a DPDT `MiniToggleSwitch`,
both of which already draw correctly, already report the right terminals, and already have netlist
coverage — `MiniToggleSwitchTests` alone is 15 cases. A new class would re-derive all of it and add
a toolbox entry.
It ships as a factory building block instead: §6.7, first in the list.
The same reasoning retires the alternative that was also considered, a `switch` property on
`PotentiometerPanel`. Besides D7, it would change the control-point count of a class placed 195
times in the corpus, which §5.2 forbids for existing files.
---
## 8. Tier 3 — reasoned, not measured
Per D2 these have no corpus evidence. Ordered by how confident the judgement is, not by cost.
**8.1 Schematic symbol holes.** Thirty symbols with specific omissions: no speaker (§7.2), no
momentary/push-button switch (only `SwitchLatchingSymbol`), no single transformer symbol with taps
(only the `TransformerCoil` + `TransformerCore` primitives), no tetrode or beam-power tube and no
dual-triode envelope for a 12AX7, no LDR, opto-isolator, thermistor, varistor, relay or SCR/Triac,
and no crystal symbol although a physical `CrystalOscillator` ships. `GroundSymbol` offers only
Default and Triangle with no chassis-versus-earth distinction, which matters for amp safety
drawings. The `IC` symbol's 3- and 5-contact forms do serve as an op-amp triangle. Each is a few
hours.
Two of these are not component work at all. The **transformer symbol** is `TransformerCoil` twice
plus `TransformerCore`, so per D7 it is a factory building block (§6.7) and needs no class. The
**opto-isolator symbol** is likewise an `LEDSymbol` facing a photoresistor symbol, once the latter
exists. That leaves the **momentary switch** as the one genuinely missing primitive here, and the
first worth drawing.
**8.2 Hi-fi connectors.** XLR (3-pin, male and female, chassis and PCB), binding post, banana,
speakON, 5-pin DIN, BNC. Zero corpus hits, high confidence from domain knowledge, and the weakest
domain in the library — there is no hi-fi category at all.
**8.3 Speaker crossover parts.** Reviving `AirCoreInductor` in §6.2 is the prerequisite and the
cheapest hi-fi win available. Beyond it: a mains toroid (`ToroidalInductor` is an inductor, not a
transformer with taps) and non-polar film caps at crossover values.
**8.4 Modern power packages.** No TO-247, TO-264 or TO-3P, so a current power amp cannot be drawn;
vintage TO-3 ships. Plus a bolt-on heatsink — `HeatSinkResistor` is a resistor, not a heatsink.
**8.5 Pickups not covered.** Jazzmaster (a wide soapbar, geometrically distinct from the P-90),
Danelectro lipstick, Rickenbacker toaster, gold foil, piezo/under-saddle. All zero corpus hits,
which is why they sit below the Tier 2 guitar item despite looking more visible.
**8.6 Connected board positions.** The `EyeletBoard` problem from §4.4: selectable eyelet and
turret positions that carry node names and reach the netlist. Benefits both boards and would
retire the discrete-turret workaround in 40 projects. Design question, not a component.
**8.7 Mains safety parts.** NTC inrush thermistor, MOV, X and Y capacitors. One corpus file
mentions NTC.
**8.8 Reverb tank.** Accutronics-style, 6 files. Simple geometry, narrow audience.
**8.9 Dual-concentric pot.** Jazz Bass, '62 Strat, Rickenbacker. Zero corpus hits, and per D7 not a
component: two `PotentiometerPanel` instances sharing a position is a factory building block, so it
joins the §6.7 list rather than this one once someone asks for it.
---
## 9. Sequencing
| Step | Items | Rough effort |
|---|---|---|
| 1 | §6.5 six malformed datasheet rows, plus the validation test | 3 h |
| 2 | §6.1 alias fix, verified by regression run | 2 h |
| 3 | §6.6 factory building block import guard | 3 h |
| 4 | §6.7 push-pull pot block, as the D7 worked example | 3 h |
| 5 | §6.2 revive three, decide on the fourth | 3 h |
| 6 | §6.3 UX4, then UX5 / B8G / B9D | 6-8 h |
| 7 | §6.4 datasheets, `RadialCeramicDiskCapacitor` first | 6-10 h each |
| 8 | §6.7 remaining blocks: transformer symbol, treble bleed, tube gain stage, harnesses | 2-3 h each |
| 9 | §7.1 `Transformer`, with §7.3 Choke as its third type | 20-24 h |
| 10 | §7.2 Speaker, physical plus symbol | 10-12 h |
| 11 | §7.4 Footswitch | 10-12 h |
| 12 | §7.5 `Photoresistor`, then the Vactrol block | 6 h |
Steps 1-4 are about a day and a half: both known bugs cleared, the building block channel unblocked,
and the highest-demand composable shipped as data rather than as a class. Step 9 is the one that
matters most.
Three items that looked like components when this roadmap started are now data (§6.7, §7.6, §8.9),
and one is split into a primitive plus a block (§7.5). That is D7 working as intended; expect the
same question of anything added to Tier 2 later.
Figures are estimates for scoping, not commitments, and every dimension in this document is an
estimate until it is checked against a datasheet (§6.3, §6.4).
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.diylc</groupId>
<artifactId>diylc-parent</artifactId>
<version>6.6.0</version>
<version>6.7.0</version>
<packaging>pom</packaging>
<name>DIY Layout Creator</name>