diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts
index 700a84bb5..71823c763 100644
--- a/build-logic/convention/build.gradle.kts
+++ b/build-logic/convention/build.gradle.kts
@@ -56,7 +56,6 @@ dependencies {
compileOnly(libs.androidx.room.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin)
compileOnly(libs.test.retry.gradlePlugin)
- implementation(libs.wire.schema)
detektPlugins(libs.detekt.formatting)
}
diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/proto/FieldMetadataSchemaHandler.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/proto/FieldMetadataSchemaHandler.kt
deleted file mode 100644
index ebf20c037..000000000
--- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/proto/FieldMetadataSchemaHandler.kt
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright (c) 2025-2026 Meshtastic LLC
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package org.meshtastic.buildlogic.proto
-
-import com.squareup.wire.schema.Extend
-import com.squareup.wire.schema.Field
-import com.squareup.wire.schema.MessageType
-import com.squareup.wire.schema.Options.Companion.FIELD_OPTIONS
-import com.squareup.wire.schema.ProtoFile
-import com.squareup.wire.schema.ProtoMember
-import com.squareup.wire.schema.Schema
-import com.squareup.wire.schema.SchemaHandler
-import com.squareup.wire.schema.Service
-import com.squareup.wire.schema.Type
-import okio.Path
-import okio.Path.Companion.toPath
-import okio.buffer
-
-/**
- * Wire custom [SchemaHandler] that walks every message field annotated with
- * `(meshtastic.config_field)` and emits a Kotlin source file containing a
- * static metadata registry.
- *
- * This runs at build time during Wire codegen — the generated registry is
- * compiled into the app with zero runtime overhead and no `wire-schema` dep.
- *
- * ## Auto-discovery
- *
- * The handler automatically finds ALL fields across ALL messages that carry
- * `(meshtastic.config_field)`. Adding the annotation to new proto fields
- * requires **zero code changes here** — the registry regenerates on next build.
- *
- * ## When to modify this file
- *
- * Only if the `ConfigFieldMetadata` message in `field_metadata.proto` gains
- * new fields (e.g. `admin_only`, `min_value`, `max_value`). In that case,
- * update [parseConfigFieldOption] and [FieldMeta] to extract the new values.
- */
-class FieldMetadataSchemaHandler : SchemaHandler() {
-
- private val configFieldMember: ProtoMember =
- ProtoMember.get(FIELD_OPTIONS, "meshtastic.config_field")
-
- /**
- * Override the top-level handle to walk all types and collect field metadata,
- * then emit a single generated file.
- */
- override fun handle(schema: Schema, context: Context) {
- val registry = mutableMapOf>()
-
- for (protoFile in schema.protoFiles) {
- if (!context.inSourcePath(protoFile)) continue
- collectFieldMetadata(protoFile.types, registry)
- }
-
- if (registry.isNotEmpty()) {
- val output = generateRegistrySource(registry)
- val outputPath = context.outDirectory / "org/meshtastic/proto/ConfigFieldMetadataRegistry.kt"
- context.fileSystem.createDirectories(outputPath.parent!!)
- context.fileSystem.sink(outputPath).buffer().use { sink ->
- sink.writeUtf8(output)
- }
- }
- }
-
- private fun collectFieldMetadata(
- types: List,
- registry: MutableMap>,
- ) {
- for (type in types) {
- if (type is MessageType) {
- for (field in type.fieldsAndOneOfFields) {
- val optionValue = field.options.get(configFieldMember)
- if (optionValue != null) {
- val messageKey = type.type.toString()
- val meta = parseConfigFieldOption(optionValue)
- registry.getOrPut(messageKey) { mutableListOf() }
- .add(FieldEntry(name = field.name, tag = field.tag, meta = meta))
- }
- }
- // Recurse into nested types
- collectFieldMetadata(type.nestedTypes, registry)
- }
- }
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun parseConfigFieldOption(value: Any?): FieldMeta {
- // Wire schema options are returned as Map
- return when (value) {
- is Map<*, *> -> {
- val map = value as Map
- val diyOnly = map.entries.find { it.key.member == "diy_only" }?.value
- FieldMeta(diyOnly = diyOnly == "true" || diyOnly == true)
- }
- else -> FieldMeta(diyOnly = false)
- }
- }
-
- /**
- * Converts a proto type like "meshtastic.Config.PositionConfig" to the
- * Wire-generated Kotlin class name "org.meshtastic.proto.Config.PositionConfig".
- */
- private fun protoTypeToKotlinClass(protoType: String): String {
- // Wire maps package "meshtastic" → "org.meshtastic.proto"
- // and nested messages become nested Kotlin classes.
- val parts = protoType.split(".")
- return if (parts.firstOrNull() == "meshtastic") {
- "org.meshtastic.proto." + parts.drop(1).joinToString(".")
- } else {
- protoType
- }
- }
-
- private fun generateRegistrySource(registry: Map>): String {
- val sb = StringBuilder()
- sb.appendLine("// AUTO-GENERATED by FieldMetadataSchemaHandler — do not edit.")
- sb.appendLine("@file:Suppress(\"ktlint\")")
- sb.appendLine()
- sb.appendLine("package org.meshtastic.proto")
- sb.appendLine()
- sb.appendLine("/**")
- sb.appendLine(" * Build-time generated field metadata from `(meshtastic.config_field)` proto annotations.")
- sb.appendLine(" *")
- sb.appendLine(" * Usage — just reference the field by name:")
- sb.appendLine(" * ```")
- sb.appendLine(" * PositionConfigFields.rx_gpio.diyOnly // true")
- sb.appendLine(" * PositionConfigFields.tx_gpio.diyOnly // true")
- sb.appendLine(" * ```")
- sb.appendLine(" */")
- sb.appendLine()
- sb.appendLine("/** Metadata for a single proto field. */")
- sb.appendLine("data class ProtoFieldMeta(val name: String, val tag: Int, val diyOnly: Boolean)")
- sb.appendLine()
-
- // Generate one object per message type
- for ((protoType, fields) in registry.entries.sortedBy { it.key }) {
- val kotlinClass = protoTypeToKotlinClass(protoType)
- // "meshtastic.Config.PositionConfig" → "PositionConfigFields"
- val objectName = protoType.split(".").last() + "Fields"
-
- sb.appendLine("/** Field metadata for [${kotlinClass}]. */")
- sb.appendLine("object $objectName {")
- for (entry in fields.sortedBy { it.name }) {
- sb.appendLine(" val ${entry.name} = ProtoFieldMeta(name = \"${entry.name}\", tag = ${entry.tag}, diyOnly = ${entry.meta.diyOnly})")
- }
- sb.appendLine()
- sb.appendLine(" /** All fields carrying metadata on this message. */")
- sb.appendLine(" val all: kotlin.collections.List = listOf(${fields.sortedBy { it.name }.joinToString { it.name }})")
- sb.appendLine("}")
- sb.appendLine()
- }
-
- return sb.toString()
- }
-
- // Unused — we override the top-level handle() instead
- override fun handle(type: Type, context: Context): Path? = null
- override fun handle(service: Service, context: Context): List = emptyList()
- override fun handle(extend: Extend, field: Field, context: Context): Path? = null
-
- private data class FieldMeta(val diyOnly: Boolean)
- private data class FieldEntry(val name: String, val tag: Int, val meta: FieldMeta)
-}
-
-/** Factory for Wire's Gradle plugin to instantiate the handler. */
-class FieldMetadataSchemaHandlerFactory : SchemaHandler.Factory {
- override fun create(
- includes: List,
- excludes: List,
- exclusive: Boolean,
- outDirectory: String,
- options: Map,
- ): SchemaHandler = FieldMetadataSchemaHandler()
-}
-
-
-
-
-
-
-
diff --git a/core/proto/build.gradle.kts b/core/proto/build.gradle.kts
index 4d2ab4b50..c3650d26b 100644
--- a/core/proto/build.gradle.kts
+++ b/core/proto/build.gradle.kts
@@ -30,7 +30,6 @@ kotlin {
sourceSets {
commonMain {
dependencies { api(libs.wire.runtime) }
- kotlin.srcDir(layout.buildDirectory.dir("generated/source/wire-metadata"))
}
}
}
@@ -50,15 +49,10 @@ wire {
// Codebase is already written to use the nullable properties (e.g. packet.decoded vs
// packet.payload_variant.decoded).
boxOneOfsMinSize = 5000
- }
- // Emit a static ConfigFieldMetadataRegistry from (meshtastic.config_field) annotations.
- // Fully automatic — annotating new proto fields requires no changes here or in the handler.
- // Only modify the handler if ConfigFieldMetadata gains new sub-fields.
- custom {
- schemaHandlerFactoryClass =
- "org.meshtastic.buildlogic.proto.FieldMetadataSchemaHandlerFactory"
- out = layout.buildDirectory.dir("generated/source/wire-metadata").get().asFile.path
- exclusive = false
+
+ // Emit Kotlin annotations from scalar proto field options (e.g. diy_only).
+ // Wire natively generates @DiyOnly annotations on fields that carry the option.
+ emitAppliedOptions = true
}
root("meshtastic.*")
prune("meshtastic.MeshPacket#delayed")
diff --git a/core/proto/src/commonMain/kotlin/org/meshtastic/core/proto/FieldMetadataDemo.kt b/core/proto/src/commonMain/kotlin/org/meshtastic/core/proto/FieldMetadataDemo.kt
new file mode 100644
index 000000000..f701aceeb
--- /dev/null
+++ b/core/proto/src/commonMain/kotlin/org/meshtastic/core/proto/FieldMetadataDemo.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2025-2026 Meshtastic LLC
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package org.meshtastic.core.proto
+
+import org.meshtastic.proto.DiyOnlyOption
+
+/**
+ * DEMO: Wire 6 natively generates `@DiyOnlyOption(true)` on proto fields that carry
+ * `[(meshtastic.diy_only) = true]` — zero custom build code needed.
+ *
+ * The annotation has `RUNTIME` retention and targets `PROPERTY` + `FIELD`, so
+ * it can be queried via reflection on JVM/Android or used as a compile-time marker.
+ *
+ * Generated code in `Config.PositionConfig`:
+ * ```kotlin
+ * @DiyOnlyOption(true)
+ * @field:WireField(tag = 8, ...)
+ * public val rx_gpio: Int = 0,
+ * ```
+ *
+ * See: https://github.com/meshtastic/protobufs/pull/905
+ */
+object FieldMetadataDemo {
+
+ /**
+ * The [DiyOnlyOption] annotation is available at compile time for reference.
+ * On JVM/Android, you can reflect over it at runtime:
+ *
+ * ```kotlin
+ * // JVM/Android only (kotlin-reflect):
+ * val isDiy = Config.PositionConfig::rx_gpio
+ * .findAnnotation()?.value ?: false
+ * ```
+ *
+ * For KMP-safe access without reflection, use a constants map:
+ */
+ val diyOnlyFields: Set = setOf("rx_gpio", "tx_gpio")
+
+ /**
+ * UI gating — hide settings that are diy_only when device isn't DIY:
+ *
+ * ```kotlin
+ * if ("rx_gpio" !in FieldMetadataDemo.diyOnlyFields || deviceIsDiy) {
+ * DropDownPreference(title = stringResource(Res.string.gps_receive_gpio), ...)
+ * }
+ * ```
+ */
+ fun uiGatingExample(deviceIsDiy: Boolean) {
+ val allFields = listOf("fixed_position", "gps_enabled", "rx_gpio", "tx_gpio")
+ for (field in allFields) {
+ val isDiyOnly = field in diyOnlyFields
+ if (!isDiyOnly || deviceIsDiy) {
+ println("Showing $field setting")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto
index 9bf4a87d8..fc5a1a25d 160000
--- a/core/proto/src/main/proto
+++ b/core/proto/src/main/proto
@@ -1 +1 @@
-Subproject commit 9bf4a87d88a971c89aba1ad2bd9c33fac4f411f5
+Subproject commit fc5a1a25de8a79fd28dbf1b6ec7bd61100f7eb71