You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@plc4x.apache.org by cd...@apache.org on 2026/08/31 09:50:00 UTC

(plc4x) branch develop updated (5187044430 -> 75ca3297c3)

This is an automated email from the ASF dual-hosted git repository.

chrisdutz pushed a change to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git


    from 5187044430 doc: Updated the contribution guideline to include a part about AI.
     new 5286d85753 chore: Updated the bacnet vendor ids.
     new 75ca3297c3 feat: Added a isSecret method to the Option type.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../bacnetip/readwrite/model/BACnetVendorId.go     |  48 +++++++
 .../org/apache/plc4x/java/api/metadata/Option.java |  12 ++
 .../apache/plc4x/java/api/metadata/OptionTest.java | 106 +++++++++++++++
 .../java/bacnetip/readwrite/BACnetVendorId.java    |   6 +
 .../apache/plc4x/java/spi/drivers/DriverBase.java  |   9 +-
 .../drivers/messages/metadata/DefaultOption.java   |  12 ++
 .../spi/drivers/DriverBaseSecretOptionTest.java    | 149 +++++++++++++++++++++
 .../messages/metadata/DefaultOptionTest.java       | 102 ++++++++++++++
 .../protocols/bacnetip/bacnet-vendorids.mspec      |   3 +
 9 files changed, 442 insertions(+), 5 deletions(-)
 create mode 100644 plc4j/api/src/test/java/org/apache/plc4x/java/api/metadata/OptionTest.java
 create mode 100644 plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/DriverBaseSecretOptionTest.java
 create mode 100644 plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOptionTest.java


(plc4x) 01/02: chore: Updated the bacnet vendor ids.

Posted by cd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git

commit 5286d85753a57e89f9c46e326268c14edcfe9b6c
Author: Christofer Dutz <ch...@c-ware.de>
AuthorDate: Mon Aug 31 11:40:39 2026 +0200

    chore: Updated the bacnet vendor ids.
---
 .../bacnetip/readwrite/model/BACnetVendorId.go     | 48 ++++++++++++++++++++++
 .../java/bacnetip/readwrite/BACnetVendorId.java    |  6 +++
 .../protocols/bacnetip/bacnet-vendorids.mspec      |  3 ++
 3 files changed, 57 insertions(+)

diff --git a/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go b/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go
index 88954bf8be..de2eaeb8be 100644
--- a/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go
+++ b/plc4go/protocols/bacnetip/readwrite/model/BACnetVendorId.go
@@ -1686,6 +1686,9 @@ const (
 	BACnetVendorId_LOGICALLY_US_LTD                                                             BACnetVendorId = 1651
 	BACnetVendorId_SOLABORATE_INCDBAHELLOCAREAI                                                 BACnetVendorId = 1652
 	BACnetVendorId_LUMEN_RADIOAB                                                                BACnetVendorId = 1653
+	BACnetVendorId_CORE_CONTROLS_AILLC                                                          BACnetVendorId = 1654
+	BACnetVendorId_MODUTROL_PTY_LTD                                                             BACnetVendorId = 1655
+	BACnetVendorId_KAESER_KOMPRESSORENSE                                                        BACnetVendorId = 1656
 	BACnetVendorId_UNKNOWN_VENDOR                                                               BACnetVendorId = 0xFFFF
 )
 
@@ -3337,6 +3340,9 @@ func init() {
 		BACnetVendorId_LOGICALLY_US_LTD,
 		BACnetVendorId_SOLABORATE_INCDBAHELLOCAREAI,
 		BACnetVendorId_LUMEN_RADIOAB,
+		BACnetVendorId_CORE_CONTROLS_AILLC,
+		BACnetVendorId_MODUTROL_PTY_LTD,
+		BACnetVendorId_KAESER_KOMPRESSORENSE,
 		BACnetVendorId_UNKNOWN_VENDOR,
 	}
 }
@@ -6243,6 +6249,18 @@ func (e BACnetVendorId) VendorId() uint16 {
 		{ /* '1653' */
 			return 1653
 		}
+	case 1654:
+		{ /* '1654' */
+			return 1654
+		}
+	case 1655:
+		{ /* '1655' */
+			return 1655
+		}
+	case 1656:
+		{ /* '1656' */
+			return 1656
+		}
 	case 166:
 		{ /* '166' */
 			return 166
@@ -12837,6 +12855,18 @@ func (e BACnetVendorId) VendorName() string {
 		{ /* '1653' */
 			return "Lumen Radio AB"
 		}
+	case 1654:
+		{ /* '1654' */
+			return "CoreControlsAi, LLC"
+		}
+	case 1655:
+		{ /* '1655' */
+			return "Modutrol Pty Ltd"
+		}
+	case 1656:
+		{ /* '1656' */
+			return "Kaeser Kompressoren SE"
+		}
 	case 166:
 		{ /* '166' */
 			return "Cooper Controls"
@@ -17980,6 +18010,12 @@ func BACnetVendorIdByValue(value uint16) (enum BACnetVendorId, ok bool) {
 		return BACnetVendorId_SOLABORATE_INCDBAHELLOCAREAI, true
 	case 1653:
 		return BACnetVendorId_LUMEN_RADIOAB, true
+	case 1654:
+		return BACnetVendorId_CORE_CONTROLS_AILLC, true
+	case 1655:
+		return BACnetVendorId_MODUTROL_PTY_LTD, true
+	case 1656:
+		return BACnetVendorId_KAESER_KOMPRESSORENSE, true
 	case 166:
 		return BACnetVendorId_COOPER_CONTROLS, true
 	case 167:
@@ -21274,6 +21310,12 @@ func BACnetVendorIdByName(value string) (enum BACnetVendorId, ok bool) {
 		return BACnetVendorId_SOLABORATE_INCDBAHELLOCAREAI, true
 	case "LUMEN_RADIOAB":
 		return BACnetVendorId_LUMEN_RADIOAB, true
+	case "CORE_CONTROLS_AILLC":
+		return BACnetVendorId_CORE_CONTROLS_AILLC, true
+	case "MODUTROL_PTY_LTD":
+		return BACnetVendorId_MODUTROL_PTY_LTD, true
+	case "KAESER_KOMPRESSORENSE":
+		return BACnetVendorId_KAESER_KOMPRESSORENSE, true
 	case "COOPER_CONTROLS":
 		return BACnetVendorId_COOPER_CONTROLS, true
 	case "DUKSAN_MECASYS_CO_LTD":
@@ -24640,6 +24682,12 @@ func (e BACnetVendorId) PLC4XEnumName() string {
 		return "SOLABORATE_INCDBAHELLOCAREAI"
 	case BACnetVendorId_LUMEN_RADIOAB:
 		return "LUMEN_RADIOAB"
+	case BACnetVendorId_CORE_CONTROLS_AILLC:
+		return "CORE_CONTROLS_AILLC"
+	case BACnetVendorId_MODUTROL_PTY_LTD:
+		return "MODUTROL_PTY_LTD"
+	case BACnetVendorId_KAESER_KOMPRESSORENSE:
+		return "KAESER_KOMPRESSORENSE"
 	case BACnetVendorId_COOPER_CONTROLS:
 		return "COOPER_CONTROLS"
 	case BACnetVendorId_DUKSAN_MECASYS_CO_LTD:
diff --git a/plc4j/drivers/bacnet/src/main/generated/org/apache/plc4x/java/bacnetip/readwrite/BACnetVendorId.java b/plc4j/drivers/bacnet/src/main/generated/org/apache/plc4x/java/bacnetip/readwrite/BACnetVendorId.java
index fd1ec2b102..799cad7e40 100644
--- a/plc4j/drivers/bacnet/src/main/generated/org/apache/plc4x/java/bacnetip/readwrite/BACnetVendorId.java
+++ b/plc4j/drivers/bacnet/src/main/generated/org/apache/plc4x/java/bacnetip/readwrite/BACnetVendorId.java
@@ -3311,6 +3311,12 @@ public enum BACnetVendorId {
 
   LUMEN_RADIOAB((int) 1653, (int) 1653, "Lumen Radio AB"),
 
+  CORE_CONTROLS_AILLC((int) 1654, (int) 1654, "CoreControlsAi, LLC"),
+
+  MODUTROL_PTY_LTD((int) 1655, (int) 1655, "Modutrol Pty Ltd"),
+
+  KAESER_KOMPRESSORENSE((int) 1656, (int) 1656, "Kaeser Kompressoren SE"),
+
   UNKNOWN_VENDOR((int) 0xFFFF, (int) 0xFFFF, "Unknown");
 
   private static final Map<Integer, BACnetVendorId> map;
diff --git a/protocols/bacnetip/src/main/generated/protocols/bacnetip/bacnet-vendorids.mspec b/protocols/bacnetip/src/main/generated/protocols/bacnetip/bacnet-vendorids.mspec
index 1dd3127868..f573861e4e 100644
--- a/protocols/bacnetip/src/main/generated/protocols/bacnetip/bacnet-vendorids.mspec
+++ b/protocols/bacnetip/src/main/generated/protocols/bacnetip/bacnet-vendorids.mspec
@@ -1664,6 +1664,9 @@
   ['1651' LOGICALLY_US_LTD ['1651', '"Logically Us Ltd"']]
   ['1652' SOLABORATE_INCDBAHELLOCAREAI ['1652', '"Solaborate, Inc. d/b/a hellocare.ai"']]
   ['1653' LUMEN_RADIOAB ['1653', '"Lumen Radio AB"']]
+  ['1654' CORE_CONTROLS_AILLC ['1654', '"CoreControlsAi, LLC"']]
+  ['1655' MODUTROL_PTY_LTD ['1655', '"Modutrol Pty Ltd"']]
+  ['1656' KAESER_KOMPRESSORENSE ['1656', '"Kaeser Kompressoren SE"']]
  
   ['0xFFFF' UNKNOWN_VENDOR ['0xFFFF', '"Unknown"']]
 ]


(plc4x) 02/02: feat: Added a isSecret method to the Option type.

Posted by cd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

chrisdutz pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/plc4x.git

commit 75ca3297c358571141b23e0d9d9d800a2ba8a20e
Author: Christofer Dutz <ch...@c-ware.de>
AuthorDate: Mon Aug 31 11:49:50 2026 +0200

    feat: Added a isSecret method to the Option type.
---
 .../org/apache/plc4x/java/api/metadata/Option.java |  12 ++
 .../apache/plc4x/java/api/metadata/OptionTest.java | 106 +++++++++++++++
 .../apache/plc4x/java/spi/drivers/DriverBase.java  |   9 +-
 .../drivers/messages/metadata/DefaultOption.java   |  12 ++
 .../spi/drivers/DriverBaseSecretOptionTest.java    | 149 +++++++++++++++++++++
 .../messages/metadata/DefaultOptionTest.java       | 102 ++++++++++++++
 6 files changed, 385 insertions(+), 5 deletions(-)

diff --git a/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Option.java b/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Option.java
index af8dcca2ad..4eee60a207 100644
--- a/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Option.java
+++ b/plc4j/api/src/main/java/org/apache/plc4x/java/api/metadata/Option.java
@@ -34,4 +34,16 @@ public interface Option {
     Optional<Object> getDefaultValue();
     Optional<String> getSince();
 
+    /**
+     * Whether this option carries a secret — a password, pre-shared key, token or passphrase —
+     * and must therefore be rendered masked in any form, log or string representation.
+     *
+     * <p>Defaults to {@code false} so that existing implementations remain source- and
+     * binary-compatible. Implementations derived from configuration classes report the presence
+     * of {@code @Secret} on the declaring field.</p>
+     */
+    default boolean isSecret() {
+        return false;
+    }
+
 }
diff --git a/plc4j/api/src/test/java/org/apache/plc4x/java/api/metadata/OptionTest.java b/plc4j/api/src/test/java/org/apache/plc4x/java/api/metadata/OptionTest.java
new file mode 100644
index 0000000000..67eeaf7991
--- /dev/null
+++ b/plc4j/api/src/test/java/org/apache/plc4x/java/api/metadata/OptionTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.api.metadata;
+
+import org.apache.plc4x.java.api.types.OptionType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@link Option#isSecret()} is a default method so that implementations written before it existed
+ * keep compiling and keep linking. That promise is what these tests hold: {@link LegacyOption}
+ * implements only the six methods the interface had before, and the fact that this file compiles
+ * at all is half the assertion.
+ */
+class OptionTest {
+
+    /**
+     * An implementation predating {@code isSecret()} - it overrides nothing beyond the originally
+     * abstract methods. Adding a new abstract method to {@link Option} would break this class at
+     * compile time, which is exactly the regression the default is there to prevent.
+     */
+    static class LegacyOption implements Option {
+        @Override
+        public String getKey() {
+            return "timeout";
+        }
+
+        @Override
+        public OptionType getType() {
+            return OptionType.LONG;
+        }
+
+        @Override
+        public String getDescription() {
+            return "request timeout";
+        }
+
+        @Override
+        public boolean isRequired() {
+            return false;
+        }
+
+        @Override
+        public Optional<Object> getDefaultValue() {
+            return Optional.of(5000L);
+        }
+
+        @Override
+        public Optional<String> getSince() {
+            return Optional.of("1.0");
+        }
+    }
+
+    /** An implementation that opts in, to show the default is overridable and not hard-wired. */
+    static class SecretOption extends LegacyOption {
+        @Override
+        public boolean isSecret() {
+            return true;
+        }
+    }
+
+    @Test
+    @DisplayName("an implementation that does not override reports not-secret")
+    void defaultsToNotSecret() {
+        // The safe default: an option nobody has classified must not be *assumed* sensitive,
+        // because the machinery that consumes this (redaction, toString masking) would otherwise
+        // hide every option of every driver that has not been migrated.
+        assertFalse(new LegacyOption().isSecret());
+    }
+
+    @Test
+    @DisplayName("an implementation can override the default")
+    void overridingReportsSecret() {
+        assertTrue(new SecretOption().isSecret());
+    }
+
+    @Test
+    @DisplayName("the default does not disturb the other accessors")
+    void leavesTheRestOfTheContractAlone() {
+        Option option = new LegacyOption();
+        assertFalse(option.isRequired());
+        assertTrue(option.getDefaultValue().isPresent());
+        assertTrue(option.getSince().isPresent());
+    }
+}
diff --git a/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/DriverBase.java b/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/DriverBase.java
index 2023023b8e..ca41d3ae12 100644
--- a/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/DriverBase.java
+++ b/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/DriverBase.java
@@ -29,10 +29,7 @@ import org.apache.plc4x.java.api.metadata.PlcDriverMetadata;
 import org.apache.plc4x.java.api.types.OptionType;
 import org.apache.plc4x.java.spi.config.Configuration;
 import org.apache.plc4x.java.spi.config.ConfigurationFactory;
-import org.apache.plc4x.java.spi.config.annotations.ConfigurationParameter;
-import org.apache.plc4x.java.spi.config.annotations.Description;
-import org.apache.plc4x.java.spi.config.annotations.Required;
-import org.apache.plc4x.java.spi.config.annotations.Since;
+import org.apache.plc4x.java.spi.config.annotations.*;
 import org.apache.plc4x.java.spi.config.annotations.defaults.BooleanDefaultValue;
 import org.apache.plc4x.java.spi.config.annotations.defaults.DoubleDefaultValue;
 import org.apache.plc4x.java.spi.config.annotations.defaults.FloatDefaultValue;
@@ -373,7 +370,9 @@ public abstract class DriverBase implements PlcDriver {
         Since sinceAnnotation = field.getAnnotation(Since.class);
         String since = (sinceAnnotation != null) ? sinceAnnotation.value() : null;
 
-        return new DefaultOption(key, type, description, required, defaultValue, since);
+        boolean secret = field.getAnnotation(Secret.class) != null;
+
+        return new DefaultOption(key, type, description, required, defaultValue, since, secret);
     }
 
     private static OptionType mapOptionType(Class<?> fieldType) {
diff --git a/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOption.java b/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOption.java
index baf8d5b171..31617537ca 100644
--- a/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOption.java
+++ b/plc4j/spi/drivers/src/main/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOption.java
@@ -31,15 +31,22 @@ public class DefaultOption implements Option {
     private final boolean required;
     private final Object defaultValue;
     private final String since;
+    private final boolean secret;
 
     public DefaultOption(String key, OptionType type, String description, boolean required,
                          Object defaultValue, String since) {
+        this(key, type, description, required, defaultValue, since, false);
+    }
+
+    public DefaultOption(String key, OptionType type, String description,
+                         boolean required, Object defaultValue, String since, boolean secret) {
         this.key = key;
         this.type = type;
         this.description = description;
         this.required = required;
         this.defaultValue = defaultValue;
         this.since = since;
+        this.secret = secret;
     }
 
     @Override
@@ -72,4 +79,9 @@ public class DefaultOption implements Option {
         return Optional.ofNullable(since);
     }
 
+    @Override
+    public boolean isSecret() {
+        return secret;
+    }
+
 }
diff --git a/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/DriverBaseSecretOptionTest.java b/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/DriverBaseSecretOptionTest.java
new file mode 100644
index 0000000000..8d2abe0d1c
--- /dev/null
+++ b/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/DriverBaseSecretOptionTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.drivers;
+
+import org.apache.plc4x.java.api.metadata.Option;
+import org.apache.plc4x.java.api.metadata.OptionMetadata;
+import org.apache.plc4x.java.api.types.OptionType;
+import org.apache.plc4x.java.spi.config.Configuration;
+import org.apache.plc4x.java.spi.config.annotations.ConfigurationParameter;
+import org.apache.plc4x.java.spi.config.annotations.Description;
+import org.apache.plc4x.java.spi.config.annotations.Required;
+import org.apache.plc4x.java.spi.config.annotations.Secret;
+import org.apache.plc4x.java.spi.transports.api.TransportInstance;
+import org.apache.plc4x.java.utils.auditlog.api.AuditLog;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The {@link Secret} marking on a configuration field has to survive the trip through
+ * {@code DriverBase}'s reflective option extraction and come out as {@link Option#isSecret()}.
+ * That is the whole point of the annotation: it is declared once beside the parameter, and every
+ * consumer of the driver metadata reads it from there rather than re-deciding by name.
+ *
+ * @see ConnectionStringRedactorTest for the redaction that the same marking drives
+ */
+class DriverBaseSecretOptionTest {
+
+    /** Marked on a superclass, to prove the hierarchy walk carries the flag too. */
+    static class BaseConfig implements Configuration {
+        @Secret
+        @ConfigurationParameter("inherited-token")
+        public String inheritedToken;
+
+        @ConfigurationParameter("inherited-host")
+        public String inheritedHost;
+    }
+
+    static class DriverConfig extends BaseConfig {
+        @Secret
+        @Required
+        @Description("the password")
+        @ConfigurationParameter("password")
+        public String password;
+
+        @ConfigurationParameter("username")
+        public String username;
+
+        @Secret
+        @ConfigurationParameter("psk-key")
+        public String pskKey;
+
+        // Deliberately unmarked: the identity says *which* key was refused, and hiding it costs
+        // the operator the diagnosis while protecting nothing.
+        @ConfigurationParameter("psk-identity")
+        public String pskIdentity;
+    }
+
+    /** Minimal {@link DriverBase} that exists only to expose {@link DriverConfig} as metadata. */
+    static final class StubDriver extends DriverBase {
+        @Override public String getProtocolCode() { return "stub"; }
+        @Override public String getProtocolName() { return "Stub"; }
+        @Override public Optional<String> getDefaultTransportCode() { return Optional.empty(); }
+        @Override public List<String> getSupportedTransportCodes() { return List.of(); }
+        @Override protected Class<? extends Configuration> getConfigurationClass() { return DriverConfig.class; }
+        @Override protected ConnectionBase<?> getConnection(Configuration c, TransportInstance<?> t, AuditLog a) {
+            throw new UnsupportedOperationException("test stub does not connect");
+        }
+    }
+
+    private static List<Option> options() {
+        OptionMetadata metadata = new StubDriver().getMetadata()
+            .getProtocolConfigurationOptionMetadata()
+            .orElseThrow(() -> new AssertionError("driver must expose protocol configuration options"));
+        return metadata.getOptions();
+    }
+
+    private static Option option(String key) {
+        return options().stream()
+            .filter(o -> key.equals(o.getKey()))
+            .findFirst()
+            .orElseThrow(() -> new AssertionError("no option named " + key + " in " + options()));
+    }
+
+    @Test
+    @DisplayName("a marked field becomes a secret option")
+    void markedFieldIsSecret() {
+        assertTrue(option("password").isSecret());
+    }
+
+    @Test
+    @DisplayName("an unmarked field does not")
+    void unmarkedFieldIsNotSecret() {
+        assertFalse(option("username").isSecret());
+    }
+
+    @Test
+    @DisplayName("a marked pre-shared key is secret, its identity is not")
+    void marksThePreSharedKeyButNotItsIdentity() {
+        // The pair that motivates declaring this at the field: no name-based rule separates them.
+        assertTrue(option("psk-key").isSecret());
+        assertFalse(option("psk-identity").isSecret());
+    }
+
+    @Test
+    @DisplayName("a marking on an inherited field survives the hierarchy walk")
+    void inheritedMarkingIsCarried() {
+        assertTrue(option("inherited-token").isSecret());
+        assertFalse(option("inherited-host").isSecret());
+    }
+
+    @Test
+    @DisplayName("marking a field changes nothing else about its option")
+    void markingLeavesTheOtherAttributesAlone() {
+        // The new annotation is read alongside the existing ones, not instead of them.
+        Option password = option("password");
+        assertEquals(OptionType.STRING, password.getType());
+        assertEquals("the password", password.getDescription());
+        assertTrue(password.isRequired());
+    }
+
+    @Test
+    @DisplayName("every declared parameter is still extracted")
+    void extractionStillReturnsEveryParameter() {
+        assertEquals(6, options().size(), "options were: " + options());
+    }
+}
diff --git a/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOptionTest.java b/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOptionTest.java
new file mode 100644
index 0000000000..11dba1877e
--- /dev/null
+++ b/plc4j/spi/drivers/src/test/java/org/apache/plc4x/java/spi/drivers/messages/metadata/DefaultOptionTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.drivers.messages.metadata;
+
+import org.apache.plc4x.java.api.types.OptionType;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * {@link DefaultOption} gained a {@code secret} field and a seventh constructor parameter, with
+ * the original six-argument constructor kept as a delegate. The delegation is the part worth
+ * pinning: a wrong argument order there would silently mislabel every option built by the old
+ * constructor, and every existing caller uses it.
+ */
+class DefaultOptionTest {
+
+    private static DefaultOption sixArg() {
+        return new DefaultOption("timeout", OptionType.LONG, "request timeout", false, 5000L, "1.0");
+    }
+
+    private static DefaultOption sevenArg(boolean secret) {
+        return new DefaultOption("timeout", OptionType.LONG, "request timeout", false, 5000L, "1.0", secret);
+    }
+
+    @Test
+    @DisplayName("the six-argument constructor produces a non-secret option")
+    void sixArgConstructorDefaultsToNotSecret() {
+        assertFalse(sixArg().isSecret());
+    }
+
+    @Test
+    @DisplayName("the seven-argument constructor carries the flag through")
+    void sevenArgConstructorCarriesTheFlag() {
+        assertTrue(sevenArg(true).isSecret());
+        assertFalse(sevenArg(false).isSecret());
+    }
+
+    @Test
+    @DisplayName("the six-argument constructor delegates every other value unchanged")
+    void sixArgConstructorDelegatesTheRest() {
+        // Guards the delegating call itself: same inputs through both constructors must give the
+        // same option, so a transposed argument in the delegate cannot pass unnoticed.
+        DefaultOption viaSix = sixArg();
+        DefaultOption viaSeven = sevenArg(false);
+
+        assertEquals(viaSeven.getKey(), viaSix.getKey());
+        assertEquals(viaSeven.getType(), viaSix.getType());
+        assertEquals(viaSeven.getDescription(), viaSix.getDescription());
+        assertEquals(viaSeven.isRequired(), viaSix.isRequired());
+        assertEquals(viaSeven.getDefaultValue(), viaSix.getDefaultValue());
+        assertEquals(viaSeven.getSince(), viaSix.getSince());
+        assertEquals(viaSeven.isSecret(), viaSix.isSecret());
+    }
+
+    @Test
+    @DisplayName("all values arrive where they belong")
+    void everyValueLandsInItsOwnAccessor() {
+        DefaultOption option = new DefaultOption(
+            "password", OptionType.STRING, "the password", true, "hunter2", "0.9", true);
+
+        assertEquals("password", option.getKey());
+        assertEquals(OptionType.STRING, option.getType());
+        assertEquals("the password", option.getDescription());
+        assertTrue(option.isRequired());
+        assertEquals(Optional.of("hunter2"), option.getDefaultValue());
+        assertEquals(Optional.of("0.9"), option.getSince());
+        assertTrue(option.isSecret());
+    }
+
+    @Test
+    @DisplayName("a secret option may still have no default and no since")
+    void nullDefaultAndSinceStayEmpty() {
+        DefaultOption option = new DefaultOption(
+            "psk-key", OptionType.STRING, "pre-shared key", true, null, null, true);
+
+        assertEquals(Optional.empty(), option.getDefaultValue());
+        assertEquals(Optional.empty(), option.getSince());
+        assertTrue(option.isSecret());
+    }
+}