From f6107893863e629cab1d2deb94d170cc3258548d Mon Sep 17 00:00:00 2001 From: James Rich Date: Thu, 30 Apr 2026 14:33:56 -0500 Subject: [PATCH 1/2] =?UTF-8?q?feat(proto):=20add=20Wire=20custom=20Schema?= =?UTF-8?q?Handler=20for=20proto=20field=20metadata=20Adds=20build-time=20?= =?UTF-8?q?codegen=20that=20auto-generates=20a=20ConfigFieldMetadataRegist?= =?UTF-8?q?ry=20from=20(meshtastic.config=5Ffield)=20annotations=20in=20.p?= =?UTF-8?q?roto=20files.=20-=20New=20FieldMetadataSchemaHandler=20in=20bui?= =?UTF-8?q?ld-logic=20walks=20all=20message=20fields=20-=20Emits=20a=20sta?= =?UTF-8?q?tic=20Kotlin=20registry=20mapping=20(messageType,=20fieldTag)?= =?UTF-8?q?=20=E2=86=92=20metadata=20-=20Zero=20runtime=20cost,=20no=20wir?= =?UTF-8?q?e-schema=20dep=20in=20app=20binary=20-=20Adding=20annotations?= =?UTF-8?q?=20to=20new=20fields=20requires=20no=20code=20changes=20Compani?= =?UTF-8?q?on=20to:=20https://github.com/meshtastic/protobufs/pull/905?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build-logic/convention/build.gradle.kts | 1 + .../proto/FieldMetadataSchemaHandler.kt | 197 ++++++++++++++++++ core/proto/build.gradle.kts | 16 +- core/proto/src/main/proto | 2 +- gradle/libs.versions.toml | 1 + 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/proto/FieldMetadataSchemaHandler.kt diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 71823c7630..700a84bb5c 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -56,6 +56,7 @@ 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 new file mode 100644 index 0000000000..ebf20c037b --- /dev/null +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/proto/FieldMetadataSchemaHandler.kt @@ -0,0 +1,197 @@ +/* + * 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 e60195e199..4d2ab4b50c 100644 --- a/core/proto/build.gradle.kts +++ b/core/proto/build.gradle.kts @@ -27,7 +27,12 @@ kotlin { // Override minSdk for ATAK compatibility (standard is 26) android { minSdk = 21 } - sourceSets { commonMain.dependencies { api(libs.wire.runtime) } } + sourceSets { + commonMain { + dependencies { api(libs.wire.runtime) } + kotlin.srcDir(layout.buildDirectory.dir("generated/source/wire-metadata")) + } + } } wire { @@ -46,6 +51,15 @@ wire { // 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 + } root("meshtastic.*") prune("meshtastic.MeshPacket#delayed") prune("meshtastic.MeshPacket.Delayed") diff --git a/core/proto/src/main/proto b/core/proto/src/main/proto index 10a16897b4..9bf4a87d88 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit 10a16897b46914854bb46bd94bedb16c6fad3a8b +Subproject commit 9bf4a87d88a971c89aba1ad2bd9c33fac4f411f5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c603a00e1c..53629727bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -167,6 +167,7 @@ maps-compose-widgets = { module = "com.google.maps.android:maps-compose-widgets" mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkit-barcode-scanning" } play-services-maps = { module = "com.google.android.gms:play-services-maps", version = "20.0.0" } wire-runtime = { module = "com.squareup.wire:wire-runtime", version.ref = "wire" } +wire-schema = { module = "com.squareup.wire:wire-schema", version.ref = "wire" } zxing-core = { module = "com.google.zxing:core", version = "3.5.4" } qrcode-kotlin = { module = "io.github.g0dkar:qrcode-kotlin", version.ref = "qrcode-kotlin" } From d11af85aee6368062d2ad47d241a112435e230b0 Mon Sep 17 00:00:00 2001 From: James Rich Date: Thu, 30 Apr 2026 15:49:57 -0500 Subject: [PATCH 2/2] refactor(proto): use Wire native emitAppliedOptions, delete custom SchemaHandler The upstream proto (meshtastic/protobufs#905) has been simplified to use a scalar bool extension instead of a message wrapper: [(meshtastic.diy_only) = true] Wire 6's emitAppliedOptions now natively generates @DiyOnlyOption(true) annotations on fields, eliminating the need for our custom FieldMetadataSchemaHandler. Changes: - Delete FieldMetadataSchemaHandler.kt (~200 LOC) - Remove wire-schema dependency from build-logic - Remove custom {} handler block from core:proto build - Add emitAppliedOptions = true to Wire kotlin {} config - Update FieldMetadataDemo to demonstrate the native annotation - Bump protobufs submodule to metadata-experiment branch --- build-logic/convention/build.gradle.kts | 1 - .../proto/FieldMetadataSchemaHandler.kt | 197 ------------------ core/proto/build.gradle.kts | 14 +- .../core/proto/FieldMetadataDemo.kt | 72 +++++++ core/proto/src/main/proto | 2 +- 5 files changed, 77 insertions(+), 209 deletions(-) delete mode 100644 build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/proto/FieldMetadataSchemaHandler.kt create mode 100644 core/proto/src/commonMain/kotlin/org/meshtastic/core/proto/FieldMetadataDemo.kt diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 700a84bb5c..71823c7630 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 ebf20c037b..0000000000 --- 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 4d2ab4b50c..c3650d26b3 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 0000000000..f701aceeb6 --- /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 9bf4a87d88..fc5a1a25de 160000 --- a/core/proto/src/main/proto +++ b/core/proto/src/main/proto @@ -1 +1 @@ -Subproject commit 9bf4a87d88a971c89aba1ad2bd9c33fac4f411f5 +Subproject commit fc5a1a25de8a79fd28dbf1b6ec7bd61100f7eb71