mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-12 10:57:43 -04:00
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:
330
.github/workflows/release.yml
vendored
330
.github/workflows/release.yml
vendored
@@ -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/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user