ci(release): update release workflow to tag based versioning (#2838)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2025-08-26 07:36:58 -05:00
committed by GitHub
parent 40395c7c69
commit 6b3f1236b1
4 changed files with 410 additions and 367 deletions

View File

@@ -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/

View File

@@ -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 {

View File

@@ -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()) }
}
}

View File

@@ -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
}