From 6b3f1236b1990bcb2ebaee8be86b6fcc49b64d4d Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 26 Aug 2025 07:36:58 -0500 Subject: [PATCH] ci(release): update release workflow to tag based versioning (#2838) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .github/workflows/release.yml | 330 +++++++------- app/build.gradle.kts | 15 +- .../mesh/repository/radio/MockInterface.kt | 431 ++++++++++-------- buildSrc/src/main/kotlin/Configs.kt | 1 - 4 files changed, 410 insertions(+), 367 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7068a83f5..a34ebc2a2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,76 +2,70 @@ name: Make Release on: workflow_dispatch: - inputs: - branch: - description: 'Branch to build from' - required: true - default: 'main' # Or your most common release branch - type: string - create_github_release: - description: 'Create a GitHub Release (and upload assets)' - required: true - default: true - type: boolean + push: + tags: + - 'v*' -permissions: write-all # Needed for creating releases and uploading assets +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: read + id-token: write + attestations: write jobs: - # Job to prepare common environment variables like version - prepare-release-info: + prepare-build-info: runs-on: ubuntu-latest outputs: - versionCode: ${{ steps.calculate_version_code.outputs.versionCode }} - versionNameBase: ${{ steps.get_version.outputs.versionNameBase }} + APP_VERSION_NAME: ${{ steps.get_version_name.outputs.APP_VERSION_NAME }} + APP_VERSION_CODE: ${{ steps.calculate_version_code.outputs.versionCode }} steps: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ github.event.inputs.branch }} + fetch-depth: 0 submodules: 'recursive' - fetch-depth: 0 # Needed for git rev-list - - name: Get `versionNameBase` - id: get_version - run: | - echo "versionNameBase=$(grep -oP 'VERSION_NAME_BASE = \"\K[^\"]+' ./buildSrc/src/main/kotlin/Configs.kt)" >> $GITHUB_OUTPUT + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'jetbrains' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + build-scan-publish: true + build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' + build-scan-terms-of-use-agree: 'yes' + + - name: Determine Version Name from Tag + id: get_version_name + run: echo "APP_VERSION_NAME=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - name: Calculate Version Code id: calculate_version_code uses: ./.github/actions/calculate-version-code - # Job for F-Droid build build-fdroid: - needs: prepare-release-info # Depends on version info runs-on: ubuntu-latest - if: github.repository == 'meshtastic/Meshtastic-Android' - outputs: - apk_path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk - apk_name: fdroidRelease-${{ needs.prepare-release-info.outputs.versionNameBase }}-${{ needs.prepare-release-info.outputs.versionCode }}.apk + needs: prepare-build-info steps: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ github.event.inputs.branch }} - submodules: 'recursive' fetch-depth: 0 - - - name: Load secrets (only keystore for F-Droid) - run: | - echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME - echo "$KEYSTORE_PROPERTIES" > ./keystore.properties - env: - KEYSTORE: ${{ secrets.KEYSTORE }} - KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + submodules: 'recursive' - name: Set up JDK 21 uses: actions/setup-java@v5 with: java-version: '21' distribution: 'jetbrains' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 @@ -80,63 +74,47 @@ jobs: build-scan-publish: true build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' build-scan-terms-of-use-agree: 'yes' - add-job-summary: always - - name: Build F-Droid release - run: ./gradlew assembleFdroidRelease + - name: Load F-Droid secrets env: - VERSION_CODE: ${{ needs.prepare-release-info.outputs.versionCode }} + KEYSTORE_BASE64: ${{ secrets.KEYSTORE }} + KEYSTORE_FILENAME_SECRET: ${{ secrets.KEYSTORE_FILENAME }} + KEYSTORE_PROPERTIES_SECRET: ${{ secrets.KEYSTORE_PROPERTIES }} + run: | + echo "Writing keystore file for F-Droid" + echo "$KEYSTORE_BASE64" | base64 --decode > ./app/$KEYSTORE_FILENAME_SECRET + echo "Writing keystore.properties for F-Droid" + echo "$KEYSTORE_PROPERTIES_SECRET" > ./keystore.properties - - name: Upload F-Droid APK artifact (for release job) + - name: Build F-Droid Release APK + run: | + ./gradlew :app:assembleFdroidRelease --parallel --continue --scan + env: + VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + + - name: Upload F-Droid APK artifact uses: actions/upload-artifact@v4 with: name: fdroid-apk path: app/build/outputs/apk/fdroid/release/app-fdroid-release.apk - retention-days: 1 # Keep for a short period as it will be uploaded to release + retention-days: 1 - # Job for Play Store build build-google: - needs: prepare-release-info # Depends on version info runs-on: ubuntu-latest - if: github.repository == 'meshtastic/Meshtastic-Android' - outputs: - aab_path: app/build/outputs/bundle/googleRelease/app-google-release.aab - aab_name: googleRelease-${{ needs.prepare-release-info.outputs.versionNameBase }}-${{ needs.prepare-release-info.outputs.versionCode }}.aab - apk_path: app/build/outputs/apk/google/release/app-google-release.apk - apk_name: googleRelease-${{ needs.prepare-release-info.outputs.versionNameBase }}-${{ needs.prepare-release-info.outputs.versionCode }}.apk + needs: prepare-build-info steps: - name: Checkout code uses: actions/checkout@v5 with: - ref: ${{ github.event.inputs.branch }} - submodules: 'recursive' fetch-depth: 0 - - - name: Load secrets - run: | - rm -f ./app/google-services.json # Ensure clean state - echo $GSERVICES > ./app/google-services.json - echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME - echo "$KEYSTORE_PROPERTIES" > ./keystore.properties - echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties - echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties - echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY" >> ./secrets.properties - env: - GSERVICES: ${{ secrets.GSERVICES }} - KEYSTORE: ${{ secrets.KEYSTORE }} - KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} - DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} - DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} - GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + submodules: 'recursive' - name: Set up JDK 21 uses: actions/setup-java@v5 with: java-version: '21' distribution: 'jetbrains' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 @@ -145,132 +123,156 @@ jobs: build-scan-publish: true build-scan-terms-of-use-url: 'https://gradle.com/terms-of-service' build-scan-terms-of-use-agree: 'yes' - add-job-summary: always - - name: Build Play Store release - run: ./gradlew bundleGoogleRelease assembleGoogleRelease + - name: Load Google secrets env: - VERSION_CODE: ${{ needs.prepare-release-info.outputs.versionCode }} + GSERVICES_BASE64: ${{ secrets.GSERVICES }} + KEYSTORE_BASE64: ${{ secrets.KEYSTORE }} + KEYSTORE_FILENAME_SECRET: ${{ secrets.KEYSTORE_FILENAME }} + KEYSTORE_PROPERTIES_SECRET: ${{ secrets.KEYSTORE_PROPERTIES }} + DATADOG_APPLICATION_ID_SECRET: ${{ secrets.DATADOG_APPLICATION_ID }} + DATADOG_CLIENT_TOKEN_SECRET: ${{ secrets.DATADOG_CLIENT_TOKEN }} + GOOGLE_MAPS_API_KEY_SECRET: ${{ secrets.GOOGLE_MAPS_API_KEY }} + run: | + echo "Writing google-services.json" + echo "$GSERVICES_BASE64" | base64 --decode > ./app/google-services.json + echo "Writing keystore file for Google" + echo "$KEYSTORE_BASE64" | base64 --decode > ./app/$KEYSTORE_FILENAME_SECRET + echo "Writing keystore.properties for Google" + echo "$KEYSTORE_PROPERTIES_SECRET" > ./keystore.properties + echo "Writing other secrets to secrets.properties" + echo "datadogApplicationId=$DATADOG_APPLICATION_ID_SECRET" >> ./secrets.properties + echo "datadogClientToken=$DATADOG_CLIENT_TOKEN_SECRET" >> ./secrets.properties + echo "MAPS_API_KEY=$GOOGLE_MAPS_API_KEY_SECRET" >> ./secrets.properties - - name: Upload Play Store AAB artifact (for release job) + - name: Build Google Release Artifacts (AAB and APK) + run: | + ./gradlew :app:bundleGoogleRelease :app:assembleGoogleRelease --parallel --continue --scan + env: + VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} + + - name: Upload Google AAB artifact uses: actions/upload-artifact@v4 with: name: google-aab path: app/build/outputs/bundle/googleRelease/app-google-release.aab retention-days: 1 - - name: Upload Play Store APK artifact (for release job) + - name: Upload Google APK artifact uses: actions/upload-artifact@v4 with: name: google-apk path: app/build/outputs/apk/google/release/app-google-release.apk retention-days: 1 - # Job to create GitHub release and upload assets (runs after builds if enabled) - create-github-release: - needs: [ prepare-release-info, build-fdroid, build-google ] + publish-release: runs-on: ubuntu-latest - permissions: - id-token: write - contents: write - attestations: write - # Only run this job if the input create_github_release is true - if: github.repository == 'meshtastic/Meshtastic-Android' && github.event.inputs.create_github_release == 'true' + needs: [prepare-build-info, build-fdroid, build-google] + outputs: + RELEASE_UPLOAD_URL: ${{ steps.create_gh_release.outputs.upload_url }} + CHANGELOG: ${{ steps.generate_changelog.outputs.changelog }} + APP_VERSION_NAME: ${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }} + APP_VERSION_CODE: ${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }} steps: - - name: Set up version info - id: set_version_info - run: | - echo "versionCode=${{ needs.prepare-release-info.outputs.versionCode }}" >> $GITHUB_ENV - echo "versionNameBase=${{ needs.prepare-release-info.outputs.versionNameBase }}" >> $GITHUB_ENV - echo "versionNameFdroid=${{ needs.prepare-release-info.outputs.versionNameBase }} (${{ needs.prepare-release-info.outputs.versionCode }}) fdroid" >> $GITHUB_ENV - echo "versionNameGoogle=${{ needs.prepare-release-info.outputs.versionNameBase }} (${{ needs.prepare-release-info.outputs.versionCode }}) google" >> $GITHUB_ENV - - - name: Create version_info.txt - run: | - echo -e "versionCode=${{ env.versionCode }}\nversionNameBase=${{ env.versionNameBase }}" > ./version_info.txt + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + submodules: 'recursive' - name: Download F-Droid APK uses: actions/download-artifact@v5 with: name: fdroid-apk - path: ./fdroid-apk-download + path: ./build-artifacts/fdroid - name: Download Google AAB uses: actions/download-artifact@v5 with: name: google-aab - path: ./google-aab-download + path: ./build-artifacts/google/bundle - name: Download Google APK uses: actions/download-artifact@v5 with: name: google-apk - path: ./google-apk-download + path: ./build-artifacts/google/apk - - name: Create GitHub release - uses: actions/create-release@v1 - id: create_release_step + - name: Generate Changelog + id: generate_changelog + uses: mikepenz/release-changelog-builder-action@v5 with: + configuration: ".github/changelog-config.json" + owner: ${{ github.repository_owner }} + repo: ${{ github.event.repository.name }} + ignorePreReleases: true + failOnError: true + fetchViaCommits: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Prepare Play Store Release Notes + if: steps.generate_changelog.outputs.changelog != '' + run: | + mkdir -p play_store_release_notes/en-US + echo "${{ steps.generate_changelog.outputs.changelog }}" > play_store_release_notes/en-US/default.txt + echo "${{ steps.generate_changelog.outputs.changelog }}" > changelog.txt # Also create a root changelog.txt for GitHub release + + - name: Create version_info.txt + run: | + echo "versionNameBase=${{ needs.prepare-build-info.outputs.APP_VERSION_NAME }}" > ./version_info.txt + echo "versionCode=${{ needs.prepare-build-info.outputs.APP_VERSION_CODE }}" >> ./version_info.txt + + - name: Create GitHub Release + id: create_gh_release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + body: ${{ steps.generate_changelog.outputs.changelog }} + files: | + ./build-artifacts/google/bundle/app-google-release.aab + ./build-artifacts/google/apk/app-google-release.apk + ./build-artifacts/fdroid/app-fdroid-release.apk + ./version_info.txt + ./changelog.txt draft: true - prerelease: true - release_name: Meshtastic Android ${{ env.versionNameBase }} (${{ env.versionCode }}) alpha - tag_name: v${{ env.versionNameBase }} - commitish: ${{ github.event.inputs.branch }} - body: | - Version: ${{ env.versionNameBase }} (${{ env.versionCode }}) - - F-Droid version name: `${{ env.versionNameFdroid }}` - - Google Play version name: `${{ env.versionNameGoogle }}` - - Autogenerated by GitHub Action. Please review and edit before publishing. + prerelease: ${{ contains(github.ref_name, '-internal') || contains(github.ref_name, '-closed') || contains(github.ref_name, '-open') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Add F-Droid APK to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release_step.outputs.upload_url }} - asset_path: ./fdroid-apk-download/app-fdroid-release.apk - asset_name: ${{ needs.build-fdroid.outputs.apk_name }} - asset_content_type: application/vnd.android.package-archive - - - name: Add Play Store AAB to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release_step.outputs.upload_url }} - asset_path: ./google-aab-download/app-google-release.aab - asset_name: ${{ needs.build-google.outputs.aab_name }} - asset_content_type: application/octet-stream - - - name: Add Play Store APK to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release_step.outputs.upload_url }} - asset_path: ./google-apk-download/app-google-release.apk - asset_name: ${{ needs.build-google.outputs.apk_name }} - asset_content_type: application/vnd.android.package-archive - - - name: Add version_info.txt to release - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release_step.outputs.upload_url }} - asset_path: ./version_info.txt - asset_name: version_info.txt - asset_content_type: text/plain - + # Attest the build artifacts for supply chain security. + # See: https://github.com/meshtastic/Meshtastic-Android/attestations - name: Attest Build Provenance uses: actions/attest-build-provenance@v2 with: subject-path: | - ./google-apk-download/app-google-release.apk - ./google-aab-download/app-google-release.aab - ./fdroid-apk-download/app-fdroid-release.apk + ./build-artifacts/google/bundle/app-google-release.aab + ./build-artifacts/google/apk/app-google-release.apk + ./build-artifacts/fdroid/app-fdroid-release.apk + + - name: Determine Play Store Track + id: get_track + run: | + TAG_NAME="${{ github.ref_name }}" + if [[ "$TAG_NAME" == *"-internal"* ]]; then + echo "track=qa" >> $GITHUB_OUTPUT + elif [[ "$TAG_NAME" == *"-closed"* ]]; then + echo "track=newalpha" >> $GITHUB_OUTPUT + elif [[ "$TAG_NAME" == *"-open"* ]]; then + echo "track=beta" >> $GITHUB_OUTPUT + else + echo "track=production" >> $GITHUB_OUTPUT + fi + + - name: Upload AAB to Google Play + if: success() && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + uses: r0adkll/upload-google-play@v1.0.19 + with: + serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_JSON_KEY }} + packageName: com.geeksville.mesh + releaseFiles: ./build-artifacts/google/bundle/app-google-release.aab + track: ${{ steps.get_track.outputs.track }} + status: 'draft' + whatsNewDirectory: ./play_store_release_notes/en-US/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e66756f91..15eb9cf25 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,7 +57,11 @@ android { applicationId = Configs.APPLICATION_ID minSdk = Configs.MIN_SDK targetSdk = Configs.TARGET_SDK - versionCode = System.getenv("VERSION_CODE")?.toIntOrNull() ?: 30630 + // Prioritize ENV, then fallback for versionCode + versionCode = + System.getenv("VERSION_CODE")?.toIntOrNull() + ?: (System.currentTimeMillis() / 1000).toInt() // Meshtastic Development Build + versionName = System.getenv("VERSION_NAME") ?: "Dev Build" testInstrumentationRunner = "com.geeksville.mesh.TestRunner" buildConfigField("String", "MIN_FW_VERSION", "\"${Configs.MIN_FW_VERSION}\"") buildConfigField("String", "ABS_MIN_FW_VERSION", "\"${Configs.ABS_MIN_FW_VERSION}\"") @@ -112,18 +116,21 @@ android { } flavorDimensions.add("default") productFlavors { - val versionCode = defaultConfig.versionCode + // Read versionCode from defaultConfig after it's been potentially set by ENV or fallback + val resolvedVersionCode = defaultConfig.versionCode + val resolvedVersionName = defaultConfig.versionName + create("fdroid") { dimension = "default" dependenciesInfo { includeInApk = false } - versionName = "${Configs.VERSION_NAME_BASE} ($versionCode) fdroid" + versionName = "$resolvedVersionName ($resolvedVersionCode) fdroid" } create("google") { dimension = "default" // Enable Firebase Crashlytics for Google Play builds apply(plugin = libs.plugins.google.services.get().pluginId) apply(plugin = libs.plugins.firebase.crashlytics.get().pluginId) - versionName = "${Configs.VERSION_NAME_BASE} ($versionCode) google" + versionName = "$resolvedVersionName ($resolvedVersionCode) google" } } buildTypes { diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt index 3b686c5e9..29c05a1df 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt @@ -17,34 +17,50 @@ package com.geeksville.mesh.repository.radio -import com.geeksville.mesh.* +import com.geeksville.mesh.AdminProtos +import com.geeksville.mesh.ChannelProtos +import com.geeksville.mesh.ConfigKt +import com.geeksville.mesh.ConfigProtos +import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.Portnums +import com.geeksville.mesh.Position +import com.geeksville.mesh.TelemetryProtos import com.geeksville.mesh.android.Logging +import com.geeksville.mesh.channel import com.geeksville.mesh.concurrent.handledLaunch +import com.geeksville.mesh.config +import com.geeksville.mesh.deviceMetadata +import com.geeksville.mesh.fromRadio import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.getInitials -import com.geeksville.mesh.TelemetryProtos +import com.geeksville.mesh.queueStatus import com.google.protobuf.ByteString import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.delay import kotlin.random.Random -private val defaultLoRaConfig = ConfigKt.loRaConfig { - usePreset = true - region = ConfigProtos.Config.LoRaConfig.RegionCode.TW -} +private val defaultLoRaConfig = + ConfigKt.loRaConfig { + usePreset = true + region = ConfigProtos.Config.LoRaConfig.RegionCode.TW + } private val defaultChannel = channel { settings = Channel.default.settings role = ChannelProtos.Channel.Role.PRIMARY } -@Suppress("detekt:TooManyFunctions", "detekt:MagicNumber") /** A simulated interface that is used for testing in the simulator */ -class MockInterface @AssistedInject constructor( +@Suppress("detekt:TooManyFunctions", "detekt:MagicNumber") +class MockInterface +@AssistedInject +constructor( private val service: RadioInterfaceService, @Assisted val address: String, -) : IRadioInterface, Logging { +) : IRadioInterface, + Logging { companion object { private const val MY_NODE = 0x42424242 @@ -68,10 +84,8 @@ class MockInterface @AssistedInject constructor( when { pr.wantConfigId != 0 -> sendConfigResponse(pr.wantConfigId) - data != null && data.portnum == Portnums.PortNum.ADMIN_APP -> handleAdminPacket( - pr, - AdminProtos.AdminMessage.parseFrom(data.payload) - ) + data != null && data.portnum == Portnums.PortNum.ADMIN_APP -> + handleAdminPacket(pr, AdminProtos.AdminMessage.parseFrom(data.payload)) pr.hasPacket() && pr.packet.wantAck -> sendFakeAck(pr) else -> info("Ignoring data sent to mock interface $pr") } @@ -103,126 +117,154 @@ class MockInterface @AssistedInject constructor( info("Closing the mock interface") } - /// Generate a fake text message from a node - private fun makeTextMessage(numIn: Int) = - MeshProtos.FromRadio.newBuilder().apply { - packet = MeshProtos.MeshPacket.newBuilder().apply { - id = packetIdSequence.next() - from = numIn - to = 0xffffffff.toInt() // ugly way of saying broadcast - rxTime = (System.currentTimeMillis() / 1000).toInt() - rxSnr = 1.5f - decoded = MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.TEXT_MESSAGE_APP - payload = ByteString.copyFromUtf8("This simulated node sends Hi!") - }.build() - }.build() - } + // / Generate a fake text message from a node + private fun makeTextMessage(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply { + packet = + MeshProtos.MeshPacket.newBuilder() + .apply { + id = packetIdSequence.next() + from = numIn + to = 0xffffffff.toInt() // ugly way of saying broadcast + rxTime = (System.currentTimeMillis() / 1000).toInt() + rxSnr = 1.5f + decoded = + MeshProtos.Data.newBuilder() + .apply { + portnum = Portnums.PortNum.TEXT_MESSAGE_APP + payload = ByteString.copyFromUtf8("This simulated node sends Hi!") + } + .build() + } + .build() + } - private fun makeNeighborInfo(numIn: Int) = - MeshProtos.FromRadio.newBuilder().apply { - packet = MeshProtos.MeshPacket.newBuilder().apply { - id = packetIdSequence.next() - from = numIn - to = 0xffffffff.toInt() // broadcast - rxTime = (System.currentTimeMillis() / 1000).toInt() - rxSnr = 1.5f - decoded = MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.NEIGHBORINFO_APP - payload = MeshProtos.NeighborInfo.newBuilder() - .setNodeId(numIn) - .setLastSentById(numIn) - .setNodeBroadcastIntervalSecs(60) - .addNeighbors( - MeshProtos.Neighbor.newBuilder() - .setNodeId(numIn + 1) - .setSnr(10.0f) - .setLastRxTime((System.currentTimeMillis() / 1000).toInt()) - .setNodeBroadcastIntervalSecs(60) - .build(), - ) - .addNeighbors( - MeshProtos.Neighbor.newBuilder() - .setNodeId(numIn + 2) - .setSnr(12.0f) - .setLastRxTime((System.currentTimeMillis() / 1000).toInt()) - .setNodeBroadcastIntervalSecs(60) - .build() - ) - .build() - .toByteString() - }.build() - }.build() - } + private fun makeNeighborInfo(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply { + packet = + MeshProtos.MeshPacket.newBuilder() + .apply { + id = packetIdSequence.next() + from = numIn + to = 0xffffffff.toInt() // broadcast + rxTime = (System.currentTimeMillis() / 1000).toInt() + rxSnr = 1.5f + decoded = + MeshProtos.Data.newBuilder() + .apply { + portnum = Portnums.PortNum.NEIGHBORINFO_APP + payload = + MeshProtos.NeighborInfo.newBuilder() + .setNodeId(numIn) + .setLastSentById(numIn) + .setNodeBroadcastIntervalSecs(60) + .addNeighbors( + MeshProtos.Neighbor.newBuilder() + .setNodeId(numIn + 1) + .setSnr(10.0f) + .setLastRxTime((System.currentTimeMillis() / 1000).toInt()) + .setNodeBroadcastIntervalSecs(60) + .build(), + ) + .addNeighbors( + MeshProtos.Neighbor.newBuilder() + .setNodeId(numIn + 2) + .setSnr(12.0f) + .setLastRxTime((System.currentTimeMillis() / 1000).toInt()) + .setNodeBroadcastIntervalSecs(60) + .build(), + ) + .build() + .toByteString() + } + .build() + } + .build() + } - private fun makePosition(numIn: Int) = - MeshProtos.FromRadio.newBuilder().apply { - packet = MeshProtos.MeshPacket.newBuilder().apply { - id = packetIdSequence.next() - from = numIn - to = 0xffffffff.toInt() // ugly way of saying broadcast - rxTime = (System.currentTimeMillis() / 1000).toInt() - rxSnr = 1.5f - decoded = MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.POSITION_APP - payload = MeshProtos.Position.newBuilder() - .setLatitudeI(Position.degI(32.776665)) - .setLongitudeI(Position.degI(-96.796989)) - .setAltitude(150) - .setTime((System.currentTimeMillis() / 1000).toInt()) - .setPrecisionBits(15) - .build() - .toByteString() - }.build() - }.build() - } + private fun makePosition(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply { + packet = + MeshProtos.MeshPacket.newBuilder() + .apply { + id = packetIdSequence.next() + from = numIn + to = 0xffffffff.toInt() // ugly way of saying broadcast + rxTime = (System.currentTimeMillis() / 1000).toInt() + rxSnr = 1.5f + decoded = + MeshProtos.Data.newBuilder() + .apply { + portnum = Portnums.PortNum.POSITION_APP + payload = + MeshProtos.Position.newBuilder() + .setLatitudeI(Position.degI(32.776665)) + .setLongitudeI(Position.degI(-96.796989)) + .setAltitude(150) + .setTime((System.currentTimeMillis() / 1000).toInt()) + .setPrecisionBits(15) + .build() + .toByteString() + } + .build() + } + .build() + } - private fun makeTelemetry(numIn: Int) = - MeshProtos.FromRadio.newBuilder().apply { - packet = MeshProtos.MeshPacket.newBuilder().apply { - id = packetIdSequence.next() - from = numIn - to = 0xffffffff.toInt() // broadcast - rxTime = (System.currentTimeMillis() / 1000).toInt() - rxSnr = 1.5f - decoded = MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.TELEMETRY_APP - payload = TelemetryProtos.Telemetry.newBuilder() - .setTime((System.currentTimeMillis() / 1000).toInt()) - .setDeviceMetrics( - TelemetryProtos.DeviceMetrics.newBuilder() - .setBatteryLevel(85) - .setVoltage(4.1f) - .setChannelUtilization(0.12f) - .setAirUtilTx(0.05f) - .setUptimeSeconds(123456) - .build() - ) - .build() - .toByteString() - }.build() - }.build() - } + private fun makeTelemetry(numIn: Int) = MeshProtos.FromRadio.newBuilder().apply { + packet = + MeshProtos.MeshPacket.newBuilder() + .apply { + id = packetIdSequence.next() + from = numIn + to = 0xffffffff.toInt() // broadcast + rxTime = (System.currentTimeMillis() / 1000).toInt() + rxSnr = 1.5f + decoded = + MeshProtos.Data.newBuilder() + .apply { + portnum = Portnums.PortNum.TELEMETRY_APP + payload = + TelemetryProtos.Telemetry.newBuilder() + .setTime((System.currentTimeMillis() / 1000).toInt()) + .setDeviceMetrics( + TelemetryProtos.DeviceMetrics.newBuilder() + .setBatteryLevel(85) + .setVoltage(4.1f) + .setChannelUtilization(0.12f) + .setAirUtilTx(0.05f) + .setUptimeSeconds(123456) + .build(), + ) + .build() + .toByteString() + } + .build() + } + .build() + } private fun makeDataPacket(fromIn: Int, toIn: Int, data: MeshProtos.Data.Builder) = MeshProtos.FromRadio.newBuilder().apply { - packet = MeshProtos.MeshPacket.newBuilder().apply { - id = packetIdSequence.next() - from = fromIn - to = toIn - rxTime = (System.currentTimeMillis() / 1000).toInt() - rxSnr = 1.5f - decoded = data.build() - }.build() + packet = + MeshProtos.MeshPacket.newBuilder() + .apply { + id = packetIdSequence.next() + from = fromIn + to = toIn + rxTime = (System.currentTimeMillis() / 1000).toInt() + rxSnr = 1.5f + decoded = data.build() + } + .build() } - private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = - makeDataPacket(fromIn, toIn, MeshProtos.Data.newBuilder().apply { + private fun makeAck(fromIn: Int, toIn: Int, msgId: Int) = makeDataPacket( + fromIn, + toIn, + MeshProtos.Data.newBuilder().apply { portnum = Portnums.PortNum.ROUTING_APP - payload = MeshProtos.Routing.newBuilder().apply { - }.build().toByteString() + payload = MeshProtos.Routing.newBuilder().apply {}.build().toByteString() requestId = msgId - }) + }, + ) private fun sendQueueStatus(msgId: Int) = service.handleFromRadio( fromRadio { @@ -231,99 +273,92 @@ class MockInterface @AssistedInject constructor( free = 16 meshPacketId = msgId } - }.toByteArray() + } + .toByteArray(), ) - private fun sendAdmin( - fromIn: Int, - toIn: Int, - reqId: Int, - initFn: AdminProtos.AdminMessage.Builder.() -> Unit - ) { - val p = makeDataPacket(fromIn, toIn, MeshProtos.Data.newBuilder().apply { - portnum = Portnums.PortNum.ADMIN_APP - payload = AdminProtos.AdminMessage.newBuilder().also { - initFn(it) - }.build().toByteString() - requestId = reqId - }) + private fun sendAdmin(fromIn: Int, toIn: Int, reqId: Int, initFn: AdminProtos.AdminMessage.Builder.() -> Unit) { + val p = + makeDataPacket( + fromIn, + toIn, + MeshProtos.Data.newBuilder().apply { + portnum = Portnums.PortNum.ADMIN_APP + payload = AdminProtos.AdminMessage.newBuilder().also { initFn(it) }.build().toByteString() + requestId = reqId + }, + ) service.handleFromRadio(p.build().toByteArray()) } - /// Send a fake ack packet back if the sender asked for want_ack + // / Send a fake ack packet back if the sender asked for want_ack private fun sendFakeAck(pr: MeshProtos.ToRadio) = service.serviceScope.handledLaunch { delay(2000) - service.handleFromRadio( - makeAck(MY_NODE + 1, pr.packet.from, pr.packet.id).build().toByteArray() - ) + service.handleFromRadio(makeAck(MY_NODE + 1, pr.packet.from, pr.packet.id).build().toByteArray()) } private fun sendConfigResponse(configId: Int) { debug("Sending mock config response") + // / Generate a fake node info entry @Suppress("MagicNumber") - /// Generate a fake node info entry - fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = - MeshProtos.FromRadio.newBuilder().apply { - nodeInfo = MeshProtos.NodeInfo.newBuilder().apply { - num = numIn - user = MeshProtos.User.newBuilder().apply { - id = DataPacket.nodeNumToDefaultId(numIn) - longName = "Sim " + Integer.toHexString(num) - shortName = getInitials(longName) - hwModel = MeshProtos.HardwareModel.ANDROID_SIM - }.build() - position = MeshProtos.Position.newBuilder().apply { - latitudeI = Position.degI(lat) - longitudeI = Position.degI(lon) - altitude = 35 - time = (System.currentTimeMillis() / 1000).toInt() - precisionBits = Random.nextInt(10, 19) - }.build() - }.build() - } + fun makeNodeInfo(numIn: Int, lat: Double, lon: Double) = MeshProtos.FromRadio.newBuilder().apply { + nodeInfo = + MeshProtos.NodeInfo.newBuilder() + .apply { + num = numIn + user = + MeshProtos.User.newBuilder() + .apply { + id = DataPacket.nodeNumToDefaultId(numIn) + longName = "Sim " + Integer.toHexString(num) + shortName = getInitials(longName) + hwModel = MeshProtos.HardwareModel.ANDROID_SIM + } + .build() + position = + MeshProtos.Position.newBuilder() + .apply { + latitudeI = Position.degI(lat) + longitudeI = Position.degI(lon) + altitude = 35 + time = (System.currentTimeMillis() / 1000).toInt() + precisionBits = Random.nextInt(10, 19) + } + .build() + } + .build() + } // Simulated network data to feed to our app - val packets = arrayOf( - // MyNodeInfo - MeshProtos.FromRadio.newBuilder().apply { - myInfo = MeshProtos.MyNodeInfo.newBuilder().apply { - myNodeNum = MY_NODE - }.build() - }, + val packets = + arrayOf( + // MyNodeInfo + MeshProtos.FromRadio.newBuilder().apply { + myInfo = MeshProtos.MyNodeInfo.newBuilder().apply { myNodeNum = MY_NODE }.build() + }, + MeshProtos.FromRadio.newBuilder().apply { + metadata = deviceMetadata { + firmwareVersion = "9.9.9.abcdefg" + hwModel = MeshProtos.HardwareModel.ANDROID_SIM + } + }, - MeshProtos.FromRadio.newBuilder().apply { - metadata = deviceMetadata { - firmwareVersion = "${BuildConfig.VERSION_NAME}.abcdefg" - } - }, + // Fake NodeDB + makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas + makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson + MeshProtos.FromRadio.newBuilder().apply { config = config { lora = defaultLoRaConfig } }, + MeshProtos.FromRadio.newBuilder().apply { channel = defaultChannel }, + MeshProtos.FromRadio.newBuilder().apply { configCompleteId = configId }, - // Fake NodeDB - makeNodeInfo(MY_NODE, 32.776665, -96.796989), // dallas - makeNodeInfo(MY_NODE + 1, 32.960758, -96.733521), // richardson + // Done with config response, now pretend to receive some text messages - MeshProtos.FromRadio.newBuilder().apply { - config = config { lora = defaultLoRaConfig } - }, + makeTextMessage(MY_NODE + 1), + makeNeighborInfo(MY_NODE + 1), + makePosition(MY_NODE + 1), + makeTelemetry(MY_NODE + 1), + ) - MeshProtos.FromRadio.newBuilder().apply { - channel = defaultChannel - }, - - MeshProtos.FromRadio.newBuilder().apply { - configCompleteId = configId - }, - - // Done with config response, now pretend to receive some text messages - - makeTextMessage(MY_NODE + 1), - makeNeighborInfo(MY_NODE + 1), - makePosition(MY_NODE + 1), - makeTelemetry(MY_NODE + 1) - ) - - packets.forEach { p -> - service.handleFromRadio(p.build().toByteArray()) - } + packets.forEach { p -> service.handleFromRadio(p.build().toByteArray()) } } } diff --git a/buildSrc/src/main/kotlin/Configs.kt b/buildSrc/src/main/kotlin/Configs.kt index cbe8fd781..94e9f335a 100644 --- a/buildSrc/src/main/kotlin/Configs.kt +++ b/buildSrc/src/main/kotlin/Configs.kt @@ -20,7 +20,6 @@ object Configs { const val MIN_SDK = 26 const val TARGET_SDK = 36 const val COMPILE_SDK = 36 - const val VERSION_NAME_BASE = "2.6.35" const val MIN_FW_VERSION = "2.5.14" // Minimum device firmware version supported by this app const val ABS_MIN_FW_VERSION = "2.3.15" // Minimum device firmware version supported by this app }