This commit is contained in:
Bane Stojkovic
2026-10-04 20:29:56 +02:00
parent 1853cae35c
commit 4ee3335c18
7 changed files with 262 additions and 49 deletions
@@ -204,10 +204,11 @@ public class ArduinoUno extends AbstractMakerBoard {
public void setVersion(ArduinoUnoVersion version) {
this.version = version;
if (isR4()) {
this.bodyColor = ARDUINO_BLUE;
} else {
this.bodyColor = ARDUINO_TEAL;
// The two generations are different colours, but the board only follows the version while its
// colour is still one of ours. A colour the user has chosen is theirs to keep across a version
// switch, which assigning unconditionally used to take away without saying so.
if (ARDUINO_TEAL.equals(bodyColor) || ARDUINO_BLUE.equals(bodyColor)) {
this.bodyColor = isR4() ? ARDUINO_BLUE : ARDUINO_TEAL;
}
updateControlPoints();
invalidateCache();
@@ -59,6 +59,8 @@ public class WemosD1Mini extends AbstractMakerBoard {
private static final long serialVersionUID = 1L;
private static final int PINS_PER_ROW = 8;
public static Size BOARD_WIDTH = new Size(1.0d, SizeUnit.in);
public static Size BOARD_LENGTH = new Size(1.34d, SizeUnit.in);
public static Size ROW_SPACING = new Size(0.9d, SizeUnit.in);
@@ -92,14 +94,6 @@ public class WemosD1Mini extends AbstractMakerBoard {
"5V", "G (GND)", "D4 (GPIO2)", "D3 (GPIO0)", "D2 (GPIO4)", "D1 (GPIO5)", "RX (GPIO3)", "TX (GPIO1)"
};
public static final String[] SILK_PIN_NAMES_LEFT = new String[] {
"RST", "A0", "D0", "D5", "D6", "D7", "D8", "3V3"
};
public static final String[] SILK_PIN_NAMES_RIGHT = new String[] {
"TX", "RX", "D1", "D2", "D3", "D4", "G", "5V"
};
protected boolean headers = false;
public WemosD1Mini() {
@@ -126,23 +120,28 @@ public class WemosD1Mini extends AbstractMakerBoard {
return "Pin " + (index + 1);
}
@Override
protected void updateControlPoints() {
Point2D firstPoint = controlPoints[0];
private double[][] getRelativeOffsets() {
double spacing = PIN_SPACING.convertToPixels();
double rowSpacing = ROW_SPACING.convertToPixels();
double[][] relativeOffsets = new double[16][2];
for (int i = 0; i < 8; i++) {
double[][] relativeOffsets = new double[PIN_NAMES.length][2];
// Left row (pins 0..7, top to bottom)
for (int i = 0; i < PINS_PER_ROW; i++) {
relativeOffsets[i][0] = 0;
relativeOffsets[i][1] = i * spacing;
}
for (int i = 0; i < 8; i++) {
relativeOffsets[8 + i][0] = rowSpacing;
relativeOffsets[8 + i][1] = (7 - i) * spacing;
// Right row (pins 8..15, bottom to top, which is the order the board numbers them in)
for (int i = 0; i < PINS_PER_ROW; i++) {
relativeOffsets[PINS_PER_ROW + i][0] = rowSpacing;
relativeOffsets[PINS_PER_ROW + i][1] = (PINS_PER_ROW - 1 - i) * spacing;
}
rotatePoints(firstPoint, relativeOffsets);
return relativeOffsets;
}
@Override
protected void updateControlPoints() {
rotatePoints(controlPoints[0], getRelativeOffsets());
}
@Override
@@ -287,18 +286,19 @@ public class WemosD1Mini extends AbstractMakerBoard {
StringUtils.drawCenteredText(g2d, "D1 mini", x + rowSpacing / 2.0, labelY,
HorizontalAlignment.CENTER, VerticalAlignment.CENTER);
// Silkscreen: Pin labels
// Silkscreen: pin names, lying flat beside their row as the board prints them rather than
// standing on end like drawPinLabels' -- the 0.9" row spacing leaves room for them. Both the
// text and the position come from the control point, so the right row's bottom-to-top order
// is followed rather than mirrored by a second array kept in step by hand.
g2d.setFont(SILK_FONT_TINY);
double spacing = PIN_SPACING.convertToPixels();
double labelOffset = new Size(1.4d, SizeUnit.mm).convertToPixels();
for (int i = 0; i < 8; i++) {
double pinY = y + i * spacing;
// Left pin labels
StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_LEFT[i], x + labelOffset, pinY,
HorizontalAlignment.LEFT, VerticalAlignment.CENTER);
// Right pin labels
StringUtils.drawCenteredText(g2d, SILK_PIN_NAMES_RIGHT[i], x + rowSpacing - labelOffset, pinY,
HorizontalAlignment.RIGHT, VerticalAlignment.CENTER);
double[][] offsets = getRelativeOffsets();
for (int i = 0; i < offsets.length; i++) {
boolean leftRow = i < PINS_PER_ROW;
StringUtils.drawCenteredText(g2d, getSilkPinLabel(i),
x + offsets[i][0] + (leftRow ? labelOffset : -labelOffset), y + offsets[i][1],
leftRow ? HorizontalAlignment.LEFT : HorizontalAlignment.RIGHT,
VerticalAlignment.CENTER);
}
}
@@ -88,7 +88,10 @@ public class DHTSensor extends AbstractMakerBoard {
public void setModel(DHTModel model) {
this.model = model;
this.bodyColor = (model == DHTModel.DHT11) ? DHT11_BLUE : DHT22_WHITE;
// Follows the model only while the colour is still a default of ours; see ArduinoUno.
if (DHT11_BLUE.equals(bodyColor) || DHT22_WHITE.equals(bodyColor)) {
this.bodyColor = model == DHTModel.DHT11 ? DHT11_BLUE : DHT22_WHITE;
}
invalidateCache();
}
@@ -21,9 +21,11 @@
*/
package org.diylc.components.micro;
import java.awt.Color;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.components.micro.ArduinoUno.ArduinoUnoVersion;
import org.junit.Assert;
@@ -120,6 +122,25 @@ public class ArduinoUnoTest {
}
}
/**
* The board colour follows the version only while it is still a default. The R3 and the R4 are
* genuinely different colours, so switching has to repaint a board the user has not touched --
* but a colour they chose has to survive, which assigning unconditionally did not let it do.
*/
@Test
public void aChosenColourSurvivesAVersionSwitch() {
ArduinoUno uno = version(ArduinoUnoVersion.REV3);
Assert.assertEquals(AbstractMakerBoard.ARDUINO_TEAL, uno.getBodyColor());
uno.setVersion(ArduinoUnoVersion.R4_WIFI);
Assert.assertEquals("an untouched board should follow its version",
AbstractMakerBoard.ARDUINO_BLUE, uno.getBodyColor());
Color chosen = Color.decode("#8A2BE2");
uno.setBodyColor(chosen);
uno.setVersion(ArduinoUnoVersion.REV3);
Assert.assertEquals("a chosen colour should survive the switch", chosen, uno.getBodyColor());
}
private static ArduinoUno version(ArduinoUnoVersion version) {
ArduinoUno uno = new ArduinoUno();
uno.setVersion(version);
@@ -23,6 +23,7 @@ package org.diylc.components.micro;
import java.awt.geom.Rectangle2D;
import org.diylc.components.AbstractMakerBoard;
import org.diylc.components.maker.MakerBoardTestSupport;
import org.diylc.core.measures.Size;
import org.diylc.core.measures.SizeUnit;
@@ -43,7 +44,7 @@ public class WemosD1MiniTest {
Assert.assertEquals("D0 (GPIO16)", mcu.getControlPointNodeName(2));
Assert.assertEquals("3V3", mcu.getControlPointNodeName(7));
// right row, top to bottom; the board prints the ground pad as "G"
// right row in array order, which runs bottom to top on the board; the ground pad prints "G"
Assert.assertEquals("5V", mcu.getControlPointNodeName(8));
Assert.assertEquals("G (GND)", mcu.getControlPointNodeName(9));
Assert.assertEquals("D1 (GPIO5)", mcu.getControlPointNodeName(13));
@@ -68,4 +69,36 @@ public class WemosD1MiniTest {
new Size(0.275d, SizeUnit.in).convertToPixels(),
mcu.getControlPoint(0).getY() - bounds.getY(), 0.1);
}
/**
* The right row runs bottom to top, so its first pin is level with the left row's last. This is
* the fact the silkscreen has to follow, and the reason the labels are taken from each control
* point rather than from a parallel array.
*/
@Test
public void theRightRowRunsBottomToTop() {
WemosD1Mini mcu = new WemosD1Mini();
Assert.assertEquals("the rows should start and end level", mcu.getControlPoint(7).getY(),
mcu.getControlPoint(8).getY(), 0.01);
Assert.assertTrue("pin 8 should sit below pin 15",
mcu.getControlPoint(8).getY() > mcu.getControlPoint(15).getY());
}
/**
* The silkscreen is the node names with their annotations stripped. Two hand-maintained arrays
* used to carry them, the right one written in reverse to compensate for the row direction
* above -- a duplicate of PIN_NAMES that had to be kept in step by hand and would have drifted
* silently if a pin were ever renamed.
*/
@Test
public void silkLabelsComeFromTheNodeNames() {
WemosD1Mini mcu = new WemosD1Mini();
String[] expected = new String[] {"RST", "A0", "D0", "D5", "D6", "D7", "D8", "3V3", "5V", "G",
"D4", "D3", "D2", "D1", "RX", "TX"};
Assert.assertEquals(expected.length, mcu.getControlPointCount());
for (int i = 0; i < expected.length; i++) {
Assert.assertEquals("pin " + i, expected[i],
AbstractMakerBoard.getDisplayPinLabel(mcu.getControlPointNodeName(i)));
}
}
}
+26 -9
View File
@@ -413,11 +413,27 @@ Small, independent, and worth folding into whichever Tier 1 item is done first.
`bomPolicy = SHOW_ONLY_TYPE_NAME`, so a Uno R3 and a Uno R4 WiFi both print as "Arduino Uno".
A maker's BOM is a shopping list and the variant is the part they buy. Worth deciding on a
uniform approach before the roadmap roughly triples the number of variants.
3. **Editable-property set drifts across the family.** `Headers` exists on `ArduinoNano` and
`RaspberryPiZero` but not on `ArduinoUno`, `Teensy` or `ESP32DevKit`; there is no "show pin
labels" toggle anywhere; `WemosD1Mini` hardcodes `SILK_PIN_NAMES_LEFT` / `SILK_PIN_NAMES_RIGHT`
instead of using the shared `drawPinLabels`. Settling a uniform Version / Headers / Show Labels
/ Board Color set now stops ten more boards inheriting the inconsistency.
3. **Editable-property set drifts across the family** -- **mostly withdrawn; see the peripherals
plan's §11.2 for the decision.** Two of the three facts here have not held up. `Headers` is now
on seven boards including `Teensy` and `ESP32DevKit`, and the three that lack it -- `ArduinoUno`,
`ArduinoMega` and the full-size `RaspberryPi` -- are exactly the boards that ship with their
headers soldered, so the distribution is the product line rather than drift; adding it to the Uno
would offer a choice the part does not have. The "show pin labels" toggle is rejected outright,
chiefly because a new boolean deserializes as `false` and would strip the silkscreen from every
existing file. The last clause was the real one and is now **fixed**, though not the way it
was written. `WemosD1Mini` did hardcode `SILK_PIN_NAMES_LEFT` / `SILK_PIN_NAMES_RIGHT`, and they
were `PIN_NAMES` with the annotations stripped -- the right one also **reversed**, because that
row's control points run bottom to top. The labels were correct on screen, so what this cost was
not a visible defect but a duplicate that had to be kept in step by hand and would have drifted
silently the first time a pin was renamed.
Moving it to the shared `drawPinLabels` would have been the wrong fix: that helper stands its
labels on end, and the D1 mini prints its names flat between its rows, which its 0.9" row
spacing has room for. So the drawing stays local and only the *source* changed -- text from
`getSilkPinLabel(i)` and position from `getRelativeOffsets()`, both indexed by control point, so
the row direction is followed rather than mirrored and the reversed array disappears rather than
being corrected. The render is byte-identical with and without headers.
`silkLabelsComeFromTheNodeNames` and `theRightRowRunsBottomToTop` cover it.
4. **Pin-name conventions differ.** `ESP32DevKit` uses `GND_1`, `ArduinoNano` uses `GND1`. And
`AbstractMakerBoard.getDisplayPinLabel` truncates at the first `_` and `(`, so `ArduinoNano`'s
`D3 (~)` silently loses its PWM marker on the silkscreen. Pick one convention before adding
@@ -442,10 +458,11 @@ Each item is an independent commit; none depends on another except where sequenc
1. **BOM treatment of `Version`** (§8.2). Options: leave as-is; switch these boards to a BOM
policy that includes the variant; or expose the variant through a value-like property. Affects
every board in the package, so worth deciding once.
2. **Uniform property set** (§8.3). Which of `Headers` and a new "Show Pin Labels" toggle should
exist on all boards, and whether retrofitting `Headers` onto `ArduinoUno` / `Teensy` /
`ESP32DevKit` is wanted. New boolean fields deserialize as `false` in old files, so the default
must be chosen to preserve the current look.
2. ~~**Uniform property set** (§8.3)~~ -- **decided in the peripherals plan's §11.2.** Neither
`Headers` nor a "Show Pin Labels" toggle is universalised: a maker component gets a property
when the real part gives the buyer a choice, and a drawing preference is not a property. The
serialization warning recorded here is what kills the toggle rather than merely constraining
its default.
3. **ESP32-C6 in the same pass as C3?** §5.2 Item 7 is written for both since they share a body.
If time-boxing, ship C3 first and add C6 as a one-branch follow-up.
4. **Nano pin-label verbosity** (§5.2 Item 6). Recommendation as written: annotate bus pins on all
+145 -7
View File
@@ -25,7 +25,7 @@ robotics are deliberately deferred; §10 records why.
| D1 | Which package ships second | **`displays` only.** | Displays are archetype-complete in a way sensors are not: the hobby world has roughly seven display archetypes and the drafts already cover all seven, whereas the nine drafted sensors are a sample of hundreds with no canonical members. Displays also reach the *existing* guitar and amp audience — 7-segment counters, character LCDs and addressable strips appear in builds with no microcontroller anywhere. |
| D2 | How coverage grows inside an archetype | **A `Version`-style enum on the existing class where the footprint family is shared; a new class where it is not.** | Same rule as the board roadmap's D2. `OLEDDisplay` is now the only class with no variant enum at all, which makes it the largest structural gap left in the package; `LEDMatrix` gained its `Modules` enum in §6.4 and `TFTDisplay` completed its `Controller` enum in §6.2. |
| D3 | Where dimension and pin data live | **Hardcoded `Size` constants and `String[]` arrays in the component class,** per-variant where a variant changes them. | Matches every shipped board. No pin-definition resource is introduced. |
| D4 | Screen content | **A neutral dark panel, with at most a small part-number silk.** No faked user interface. | Four drafts currently render English demo text ("HELLO WORLD! 16x2", "OLED DISPLAY") at fixed font sizes, which does not scale with the board, bakes a language into a drawing, and dates quickly. A dark glass panel is what the part looks like when it is not powered, which is how every other component is drawn. |
| D4 | Screen content | **No faked user interface — but the glass prints the part's own spec.** Amended by §6.8. | The original ruling was a neutral dark panel with at most a part-number silk, because four drafts rendered English demo text ("HELLO WORLD! 16x2", "OLED DISPLAY") at fixed font sizes, which does not scale with the board, bakes a language into a drawing and dates quickly. None of that reasoning is withdrawn and the demo text stays out. What changed is the problem: once a class carries six variants a dark rectangle no longer says which one it is, and all three places that do say — the property editor, the BOM and the Explorer pane — are off the canvas. §6.8 puts the variant string itself on the lit area; it is generated from the properties rather than typed, carries no prose and no language, and `Display.NONE` restores this row's dark glass for anyone who wants it. |
| D5 | Shared LED rendering | **One helper in `MakerBoardPainter`,** used by every addressable-RGB part. | `WS2812BRing` and `WS2812BStick` already contain two copies of the 5050-package-plus-diffuser-plus-IC-dot drawing, one scaled and one in raw pixels. A strip and a panel would make four. |
| D6 | Backward compatibility | Nothing here is a compatibility risk. | These classes have never been discoverable, so no `.diy` file references them. Field names, control-point counts and defaults can still be changed freely — **this slice is the last chance to do so.** |
| D7 | Geometry expressed in `Size` | **Every dimension derives from a `Size` constant.** | The drafts place bezels, screens and mounting holes with raw pixel offsets (`boardX = x - 60`, `bezelY = boardY + 45`), so nothing moves if a board dimension is corrected. The shipped boards do not do this. |
@@ -840,7 +840,107 @@ for the lit colour. Category `Displays & Outputs` even though the neighbours in
- **NeoPixel panel 8x8** as its own class, reusing `drawAddressableLed` and the ring's pad handling.
Worth doing only after §6.1, whose helper it depends on. **~0.5 day.**
### 6.8 Deliberately not in this slice
### 6.8 On-screen identification — the glass prints its own spec
**Decided, not yet built.** The four displays that have a lit area — `OLEDDisplay`, `TFTDisplay`,
`CharacterLCD`, `Nokia5110LCD` — draw their variant string inside it, in the panel's own lit colour,
on by default and switchable off per component. This amends D4 for text that *identifies the part*
and leaves D4's ban on invented content standing.
**Why it is worth reversing a decision for.** Variant coverage is what §6 spent itself on: the
OLED now carries three sizes times two interfaces, the TFT four controllers, the Character LCD two
sizes times two interfaces. On the canvas all six OLED combinations are the same dark letterbox on
the same blue board, and the three places that say which one is selected are the property editor,
the BOM and the Explorer pane — none of them visible while drawing. A layout with four displays on
it cannot be read back at all.
**The string is the one that already exists.** `getValueForDisplay()` returns exactly what this
section wants for every class in the package, because §11 item 1 already made it the BOM's value
column:
| Class | Already returns |
|---|---|
| `OLEDDisplay` | `0.96" SSD1306 128x64, I2C` |
| `TFTDisplay` | `2.8" ILI9341 240x320 (Touch + SD)` |
| `CharacterLCD` | `16x2, I2C Backpack` |
| `Nokia5110LCD` | *nothing — see below* |
`Nokia5110LCD` is the exception and needs one small addition: it is a single part, so it never
overrode `getVariantLabel()` and `getValueForDisplay()` returns the empty string, which would leave
its glass blank. It should return `84x48 PCD8544` — the resolution and the controller, which is what
this section is for and which also fills the BOM's value column that §11 item 1 left empty on it.
A single part does not need the variant string for BOM *grouping*, but it is still the better answer
than nothing in that column.
It is used verbatim, word-wrapped to the lit area. The alternative — each class composing its own
diagonal, resolution and interface lines from the enum fields — was rejected because it creates a
second statement of the variant that can drift from the BOM's, which is the failure §3.2 kept
hitting with hand-rolled label arrays. One string per class, written once.
**Mechanism.** A static helper beside the others, `MakerBoardPainter.drawScreenText(g2d, area,
inkColor, font, text)`, which wraps the string to the area's width and draws nothing at all if the
wrapped block does not fit the area's height. Each display calls it on one line from inside its
orientation transform, straight after it fills the lit area, so the text turns with the board the
way the real screen's content does. Same shape as D5's LED helper: four call sites, one
implementation.
**Property.** `org.diylc.common.Display` on each of the four classes, `@EditableProperty(name =
"Screen")`, defaulting to `Display.VALUE`, with the usual null-tolerant getter. The shared enum is
reused rather than a boolean invented because it is the idiom every other labelled component in the
library already follows, so the control is where a user expects to find it; `NONE` is the off switch
this section owes D4, and `NAME` and `BOTH` come along with the enum rather than being designed in —
`NAME` on the glass is genuinely useful when tying a drawn board to a netlist. It does **not** go on
`AbstractMakerBoard`: the other seven components in the package have no glass to print on.
**There is room, and the fit rule is a guard rather than a routine.** At 200 px/inch the lit areas
measure, in pixels:
| Panel | Lit area |
|---|---|
| 0.91" SSD1306 | 176 × 44 |
| 0.96" SSD1306 | 171 × 86 |
| 1.3" SH1106 | 232 × 116 |
| 1.54" ST7789 | 218 × 218 |
| 1.28" GC9A01 | 256 dia. |
| 1.8" ST7735 | 225 × 281 |
| 2.8" ILI9341 | 340 × 453 |
| Nokia 5110 | 291 × 213 |
| 16x2 LCD | 508 × 114 |
| 20x4 LCD | 606 × 198 |
The tightest is the 0.91" OLED, and two lines of the 11 px `SILK_FONT` need 22 px of its 44. So the
drop-rather-than-squeeze rule holds here as it does for silkscreen, but nothing in the package
currently triggers it. The round GC9A01 is the one case where width is not the board's width: text
must fit the inscribed square of the 256 px disc, not its diameter.
**Ink colour is each class's to pass.** The OLED's `PIXEL_BLUE` on its near-black active area, white
on the TFT's screen fill, the Nokia's dark LCD ink on its grey-green glass. The Character LCD is the
one that cannot use a constant: its screen colour is an editable property, so the ink is chosen from
the fill's brightness rather than fixed, or a user who picks a pale backlight gets white on white.
**Refinement worth taking on the Character LCD.** Lay the text on the module's real character grid —
one glyph per cell, truncated at the column count rather than wrapped freely — so the `16x2` is
demonstrated instead of merely asserted. It fits: `16x2 HD44780` is twelve characters and
`I2C Backpack` twelve, against sixteen columns. This is the only display whose resolution is a count
of characters rather than of pixels, so it is the only one where this is possible.
**The other seven are untouched.** The 7-segment, the bar graph, the LED matrix and the four NeoPixel
parts have discrete emitters rather than a screen; spec text there would have to be rendered as lit
dots to be honest, and an 8x8 grid carries one glyph, which identifies nothing. `LEDMatrix` keeps the
fixed lit pattern it already draws. `drawIcon` is also unchanged throughout — the `OLED` / `TFT` /
`LCD` text in the toolbox icons is a legibility device at 32 px and was never what D4 was about.
**Testing.** Per class, the §7 drawing smoke test gains the property in each of its four states,
plus an assertion that what reaches the helper is `getValueForDisplay()` rather than a literal, so a
later correction to an enum label cannot leave stale text on the glass. Whether it *looks* right is
§8's business, and this is precisely the silent rendering change §8 says there is no
baseline to catch — so the §8 sample projects should be generated with this in place rather than
before it.
**Effort.** ~0.5 day for the helper, the property on four classes and the tests. ~0.5 day more for
the character-grid refinement, which is independent and can follow.
### 6.9 Deliberately not in this slice
E-paper (1.54" / 2.9" SSD1680) — growing but still niche, and the flexible-cable mounting is real
new drawing work. Also: 14- and 16-segment alphanumeric, HT16K33 backpacks, 1.2" and 2.3" large
@@ -887,8 +987,9 @@ part name, then the variant list).
3. **§5 item 10 and §6.2-6.4** — sourcing, folded into the variant work for the three classes that
gain enums, since both touch the same constants.
4. **§6.1, §6.5, §6.6, §6.7** — the new classes, each an independent commit.
5. **§7** tests alongside each of the above, not after.
6. **§8** regression samples and the `update.xml` block, last.
5. **§6.8** — on-screen identification, after the variant enums it reads from are final.
6. **§7** tests alongside each of the above, not after.
7. **§8** regression samples and the `update.xml` block, last.
Eleven palette entries so far: the seven existing, plus the strip, the bar graph, the Jewel and the
Nokia, plus the panel if appetite holds. The `update.xml` 6.7.0 block now carries all eleven,
@@ -968,9 +1069,46 @@ gate the displays.
Two things this does not change: enum **constant names** are untouched, so nothing serialises
differently, and the grouping still splits correctly on the shorter values — two Uno versions
still produce two rows.
2. **A uniform property set** — the board roadmap's §8.3. This slice adds `Version`-style enums to
three more classes; deciding now whether a "Show Pin Labels" toggle and a consistent `Headers`
property belong on every maker component avoids retrofitting nine more classes later.
2. **A uniform property set** — the board roadmap's §8.3 — **decided, and the answer is that
uniformity is the wrong goal.** The rule that settles all three parts of it: *a maker component
gets a property when the real part gives the buyer a choice; a drawing preference is not a
property.*
**`Headers` stays where it is.** The roadmap called its distribution "drift", and a survey of
all 21 maker classes says otherwise: it exists on the seven boards sold both ways -- Nano,
ESP32DevKit, NodeMCU, Wemos D1 Mini, Pico, Zero and Teensy -- and is absent on exactly the three
that are not, the Uno, the Mega and the full-size Pi, all of which ship with their headers
soldered. The Zero carrying it while the full-size Pi does not is the tell: that is the product
line, not an oversight. Retrofitting it onto the Uno would offer a configuration the part does
not have. The roadmap's candidate list is also stale -- Teensy and ESP32DevKit have had the
property for some time. If it is ever wanted on the display modules the same test decides it: an
OLED or a Character LCD's parallel row genuinely ships with the strip loose in the bag; the
Ring, Stick, Strip and Jewel have no header to choose.
**"Show Pin Labels" is rejected**, for a compatibility reason first. A new `boolean
showPinLabels` on a shipped class **deserializes as `false`**, so every existing `.diy` file
would quietly lose its silkscreen; preserving the current look would force the field to be
named `hidePinLabels`, an inversion that reads as a bug forever after. Beyond that, the
silkscreen is a feature of the board rather than a view setting -- §3.2 item 2 is an extended
argument about *where* names fit, which a switch does not improve -- and the library has
essentially no precedent for it: one `"Show Bracket"` boolean against `Display`
(NAME/VALUE/NONE/BOTH) on 31 classes, which governs a component's own name and value and which
maker boards deliberately do not render. If the motivation is clutter, the better fix is drawing
silk only above a zoom threshold: no property, no serialized field, and it helps everyone rather
than whoever finds the checkbox.
**Colour follows `SevenSegmentDisplay`'s rule**, which costs nothing because the package already
obeys it: one property per distinct physical layer, named for the layer, ignored where the layer
does not exist. The negative half is the part worth keeping: never let one colour field paint
two different physical things, and never reset a colour from a variant setter.
That last point was still being broken. `ArduinoUno.setVersion()` overwrote `bodyColor`
unconditionally, so switching an R3 to an R4 discarded a colour the user had chosen, and the
drafted `DHTSensor.setModel()` did the same. Both now repaint only while the colour is still one
of the class's own defaults, so an untouched board still follows its version -- the R3 and R4
are genuinely different colours -- while a chosen one survives.
`ArduinoUnoTest.aChosenColourSurvivesAVersionSwitch` covers it, and fails against the old
behaviour.
**Colour is now part of this question, and `SevenSegmentDisplay` has a worked answer.** It
carried the inherited "Board Color" plus a local "LED Color", which was wrong twice over: three