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