init
This commit is contained in:
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# Gradle / Android build output
|
||||
.gradle/
|
||||
.kotlin/
|
||||
**/build/
|
||||
**/.cxx/
|
||||
**/.externalNativeBuild/
|
||||
captures/
|
||||
|
||||
# Local machine configuration
|
||||
local.properties
|
||||
gradle.properties
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# Signing keys and credentials
|
||||
*.jks
|
||||
*.keystore
|
||||
*.p12
|
||||
*.pfx
|
||||
*.pem
|
||||
*.key
|
||||
.env
|
||||
.env.*
|
||||
google-services.json
|
||||
|
||||
# Packaged Android artifacts
|
||||
*.apk
|
||||
*.aab
|
||||
*.ap_
|
||||
*.idsig
|
||||
*.dex
|
||||
|
||||
# Temporary extracted artifacts in this workspace
|
||||
tmp-aar/
|
||||
tmp-apk-libs/
|
||||
|
||||
# Logs, reports, and runtime dumps
|
||||
*.log
|
||||
*.hprof
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
# OS / editor noise
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Local debug screenshots from this workspace
|
||||
/_current_screen.png
|
||||
/flight-screen.png
|
||||
/flight-screen-safe.png
|
||||
/launch-screen.png
|
||||
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
This Android Gradle project has two modules in `settings.gradle`. `sample/` is the app module (`com.zklh.dronecontroller`) with DJI sample screens, flight/control code, Compose UI, XML layouts, navigation resources, and assets under `sample/src/main/`. `uxsdk/` is a reusable DJI UX library with widgets, map/camera/flight UI, resources, and assets under `uxsdk/src/main/`. Shared dependencies live in `dependencies.gradle`; module settings live in each `build.gradle`. Treat `build/`, `.gradle/`, `tmp-aar/`, and `tmp-apk-libs/` as generated artifacts unless required.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Use the Gradle wrapper from the repository root.
|
||||
|
||||
- `.\gradlew.bat :sample:assembleDebug` builds the debug APK.
|
||||
- `.\gradlew.bat :uxsdk:assembleDebug` builds the UX SDK library variant.
|
||||
- `.\gradlew.bat :sample:installDebug` installs the app on a connected device or emulator.
|
||||
- `.\gradlew.bat :sample:testDebugUnitTest` runs local JVM tests when present.
|
||||
- `.\gradlew.bat :sample:connectedDebugAndroidTest` runs instrumented Android tests on a device.
|
||||
|
||||
Prefer targeted module commands while editing. Run broader builds before handoff when Gradle config, dependencies, shared resources, or module boundaries change.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Follow Android Studio defaults for Kotlin and Java: 4-space indentation, same-line braces, and Java 8/Kotlin JVM target 1.8 compatibility. Use `PascalCase` for classes, `camelCase` for methods/properties, and existing suffixes such as `Fragment`, `Activity`, `VM`, `Widget`, and `WidgetModel`. App resources use lowercase snake case; `uxsdk` resources must keep the `uxsdk_` prefix. Add shared dependency versions to `dependencies.gradle`.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
No dedicated `src/test` or `src/androidTest` trees are currently checked in. Add unit tests under `module/src/test/java` and device/UI tests under `module/src/androidTest/java`. Prefer unit tests for parsing, mission planning, safety checks, telemetry transforms, and view models; use instrumented tests for SDK, permissions, camera, map, and device-dependent flows. Example names: `VirtualStickVMTest`, `MissionParserInstrumentedTest`.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
This workspace does not include Git history, so no repository-specific convention can be inferred. Use short, imperative, scoped subjects such as `sample: fix virtual stick safety gate` or `uxsdk: update camera widget labels`. PRs should include a summary, affected module(s), verification commands, device/emulator and Android version when relevant, linked issues, and screenshots or recordings for UI changes. Call out changes to DJI SDK keys, signing, permissions, flight behavior, or safety controls.
|
||||
|
||||
## Security & Configuration Tips
|
||||
|
||||
`gradle.properties` references API keys, map tokens, signing values, Maven URLs, and SDK versions. Keep real secrets local, avoid adding new credentials or keystores, and mask values in logs and reviews. For DJI flight-command changes, document manual test conditions before changing safety-related behavior.
|
||||
41
build.gradle
Normal file
41
build.gradle
Normal file
@@ -0,0 +1,41 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
apply from: rootProject.file('dependencies.gradle')
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url 'https://maven.fabric.io/public' }
|
||||
maven { url 'https://plugins.gradle.org/m2/' }
|
||||
maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
|
||||
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$KOTLIN_VERSION"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
maven {
|
||||
url REPO_MAVEN2
|
||||
}
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url KOTLIN_MAVEN
|
||||
}
|
||||
maven { url JITPACK_MAVEN_URL }
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs new File(rootProject.projectDir.getAbsolutePath() + '/libs')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
dependencies.gradle
Normal file
95
dependencies.gradle
Normal file
@@ -0,0 +1,95 @@
|
||||
ext {
|
||||
deps = [:]
|
||||
//DJI MSDK V5包
|
||||
deps.aircraft = "com.dji:dji-sdk-v5-aircraft:5.18.0"
|
||||
deps.aircraftProvided = "com.dji:dji-sdk-v5-aircraft-provided:5.18.0"
|
||||
deps.networkImp = "com.dji:dji-sdk-v5-networkImp:5.18.0"
|
||||
|
||||
//三方依赖库
|
||||
deps.gson = 'com.google.code.gson:gson:2.9.1'
|
||||
deps.okio = 'com.squareup.okio:okio:1.17.2'
|
||||
deps.rx3Android = 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
deps.rx3Kt = 'io.reactivex.rxjava3:rxkotlin:3.0.0'
|
||||
deps.sqlcipher = 'net.zetetic:sqlcipher-android:4.11.0'
|
||||
deps.wire = 'com.squareup.wire:wire-runtime:2.2.0'
|
||||
|
||||
deps.okhttp3 = 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||
deps.leakcanary = 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||
deps.xcrash = 'com.iqiyi.xcrash:xcrash-android-lib:3.1.0'
|
||||
deps.rx2Java = 'io.reactivex.rxjava2:rxjava:2.2.4' // 目前在CSDK升级模块中使用rxjava2
|
||||
deps.maplibreTurf = 'org.maplibre.gl:android-sdk-turf:5.9.1'
|
||||
deps.maplibreSdk = 'org.maplibre.gl:android-sdk:10.3.5'
|
||||
deps.lottie = 'com.airbnb.android:lottie:3.3.1'//RTK扫描页中使用,用于实现动画效果
|
||||
deps.cardview = 'androidx.cardview:cardview:1.0.0'//PopoverView中使用
|
||||
deps.material = 'com.google.android.material:material:1.0.0'//AvoidanceShortcutWidget使用
|
||||
deps.lynx = 'com.github.pedrovgs:lynx:1.1.0'// app显示logcat
|
||||
deps.pahoMqtt = 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
|
||||
|
||||
/*RTK设置页面使用-start*/
|
||||
deps.mikepenzCommunityMaterial = "com.mikepenz:community-material-typeface:3.5.95.1-kotlin@aar"
|
||||
deps.mikepenzGoogleMaterial = 'com.mikepenz:google-material-typeface:3.0.1.4.original-kotlin@aar'
|
||||
deps.mikepenzIconicsViews = 'com.mikepenz:iconics-views:4.0.2@aar'
|
||||
deps.mikepenzIconicsCore = 'com.mikepenz:iconics-core:4.0.2@aar'
|
||||
deps.mikepenzIonicons = 'com.mikepenz:ionicons-typeface:2.0.1.5-kotlin@aar'
|
||||
/*RTK设置页面使用-end*/
|
||||
|
||||
//google map
|
||||
deps.playservicesplaces = 'com.google.android.gms:play-services-places:16.0.0'
|
||||
deps.playservicesmaps = 'com.google.android.gms:play-services-maps:16.0.0'
|
||||
deps.playserviceslocation = 'com.google.android.gms:play-services-location:16.0.0'
|
||||
deps.playservicesbase = 'com.google.android.gms:play-services-base:16.0.0'
|
||||
|
||||
//androidx
|
||||
deps.fragment = 'androidx.fragment:fragment:1.3.6'
|
||||
deps.aacCommon = 'androidx.arch.core:core-common:2.1.0'
|
||||
deps.aacRuntime = 'androidx.arch.core:core-runtime:2.1.0'
|
||||
deps.annotation = 'androidx.annotation:annotation:1.1.0'
|
||||
deps.appcompat = 'androidx.appcompat:appcompat:1.3.1'
|
||||
deps.constraintLayout = 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
deps.multidex = 'androidx.multidex:multidex:2.0.1'
|
||||
deps.recyclerview = 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
deps.lifecycleJava8 = 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||
deps.lifecycleRuntime = 'androidx.lifecycle:lifecycle-runtime:2.3.1'
|
||||
deps.lifecycleViewModel = 'androidx.lifecycle:lifecycle-viewmodel:2.3.1'
|
||||
deps.lifecycleLiveData = 'androidx.lifecycle:lifecycle-livedata:2.3.1'
|
||||
deps.lifecycleProcess = 'androidx.lifecycle:lifecycle-process:2.3.1'
|
||||
deps.room = 'androidx.room:room-runtime:2.4.1'
|
||||
deps.room_rxjava = 'androidx.room:room-rxjava3:2.4.1'
|
||||
deps.room_compiler = 'androidx.room:room-compiler:2.4.1'
|
||||
deps.legacySupport = 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
deps.media = 'androidx.media:media:1.0.0'
|
||||
deps.dynamicanimation = 'androidx.dynamicanimation:dynamicanimation:1.0.0'
|
||||
deps.ktxCore = "androidx.core:core-ktx:1.3.2"
|
||||
deps.ktxFrag = "androidx.fragment:fragment-ktx:1.3.3"
|
||||
deps.ktxNavigation = "androidx.navigation:navigation-fragment-ktx:2.5.3"
|
||||
deps.ktxNavigationUi = "androidx.navigation:navigation-ui-ktx:2.5.3"
|
||||
deps.documentfile = 'androidx.documentfile:documentfile:1.0.1'
|
||||
//解析xml
|
||||
deps.dom = 'org.dom4j:dom4j:2.1.1'
|
||||
|
||||
//kotlin
|
||||
deps.kotlinLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
|
||||
|
||||
//glide
|
||||
deps.glide = 'com.github.bumptech.glide:glide:4.9.0'
|
||||
deps.glidecompiler = 'com.github.bumptech.glide:compiler:4.9.0'
|
||||
|
||||
|
||||
//wpmz 航线文件解析
|
||||
//本地依赖验证时将该aar放到主工程目录sdklib 并将sdk中aircraft 的gradle添加为本地依赖方式 api (name:"djisdkwpmz", ext:'aar')
|
||||
deps.wpmzSdk = 'com.dji:wpmzsdk:1.0.5.1'
|
||||
//sample 扩展EditText
|
||||
deps.expandedit = "com.github.thomhurst:ExpandableHintText:1.0.7"
|
||||
|
||||
//测试
|
||||
deps.junit4 = "junit:junit:4.12"
|
||||
deps.jnitCore = 'androidx.test:core:1.5.0'
|
||||
deps.testRunner = 'androidx.test:runner:1.5.0'
|
||||
deps.junitExt = 'androidx.test.ext:junit:1.1.5'
|
||||
deps.espressoCore = 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
//aspectJ
|
||||
deps.aspectj_tools = 'org.aspectj:aspectjtools:1.9.6'
|
||||
deps.aspectj_rt = 'org.aspectj:aspectjrt:1.9.6'
|
||||
deps.aspectj_weaver = 'org.aspectj:aspectjweaver:1.9.6'
|
||||
}
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
8
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
#Wed Mar 31 21:01:35 CST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://downloads.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=120000
|
||||
validateDistributionUrl=true
|
||||
160
gradlew
vendored
Normal file
160
gradlew
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
152
sample/build.gradle
Normal file
152
sample/build.gradle
Normal file
@@ -0,0 +1,152 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
||||
|
||||
android {
|
||||
namespace "dji.sampleV5.aircraft"
|
||||
compileSdkVersion Integer.parseInt(project.ANDROID_COMPILE_SDK_VERSION)
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.zklh.dronecontroller"
|
||||
minSdkVersion Integer.parseInt(project.ANDROID_MIN_SDK_VERSION)
|
||||
targetSdkVersion Integer.parseInt(project.ANDROID_TARGET_SDK_VERSION)
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
manifestPlaceholders["API_KEY"] = project.AIRCRAFT_API_KEY
|
||||
manifestPlaceholders["GMAP_API_KEY"] = project.GMAP_API_KEY
|
||||
manifestPlaceholders["MAPLIBRE_TOKEN"] = project.MAPLIBRE_TOKEN
|
||||
buildConfigField "String", "AIRCRAFT_API_KEY", "\"${project.AIRCRAFT_API_KEY}\""
|
||||
buildConfigField "String", "GMAP_API_KEY", "\"${project.GMAP_API_KEY}\""
|
||||
buildConfigField "String", "MAPLIBRE_TOKEN", "\"${project.MAPLIBRE_TOKEN}\""
|
||||
buildConfigField "boolean", "SAFETY_FLIGHT_COMMANDS_ENABLED", "true"
|
||||
ndk {
|
||||
//noinspection ChromeOsAbiSupport
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
//配置签名信息
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(project.STORE_FILE)
|
||||
storePassword project.STORE_PASSWORD
|
||||
keyAlias project.KEY_ALIAS
|
||||
keyPassword project.KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources false
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
freeCompilerArgs += ["-Xjvm-default=all"]
|
||||
}
|
||||
|
||||
//关闭lint
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// MSDK requires extracted native libraries when android:extractNativeLibs is true.
|
||||
jniLibs {
|
||||
useLegacyPackaging true
|
||||
}
|
||||
// 因为mrtc库内部使用了NDK的c++_shared的编译参数
|
||||
// 与其他库重复引用了,因此选其中一个即可
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
doNotStrip "*/*/libconstants.so"
|
||||
doNotStrip "*/*/libdji_innertools.so"
|
||||
doNotStrip "*/*/libdjibase.so"
|
||||
doNotStrip "*/*/libDJICSDKCommon.so"
|
||||
doNotStrip "*/*/libDJIFlySafeCore-CSDK.so"
|
||||
doNotStrip "*/*/libdjifs_jni-CSDK.so"
|
||||
doNotStrip "*/*/libDJIRegister.so"
|
||||
doNotStrip "*/*/libdjisdk_jni.so"
|
||||
doNotStrip "*/*/libDJIUpgradeCore.so"
|
||||
doNotStrip "*/*/libDJIUpgradeJNI.so"
|
||||
doNotStrip "*/*/libDJIWaypointV2Core-CSDK.so"
|
||||
doNotStrip "*/*/libdjiwpv2-CSDK.so"
|
||||
doNotStrip "*/*/libFlightRecordEngine.so"
|
||||
doNotStrip "*/*/libvideo-framing.so"
|
||||
doNotStrip "*/*/libwaes.so"
|
||||
doNotStrip "*/*/libagora-rtsa-sdk.so"
|
||||
doNotStrip "*/*/libc++.so"
|
||||
doNotStrip "*/*/libc++_shared.so"
|
||||
doNotStrip "*/*/libmrtc_28181.so"
|
||||
doNotStrip "*/*/libmrtc_agora.so"
|
||||
doNotStrip "*/*/libmrtc_core.so"
|
||||
doNotStrip "*/*/libmrtc_core_jni.so"
|
||||
doNotStrip "*/*/libmrtc_data.so"
|
||||
doNotStrip "*/*/libmrtc_log.so"
|
||||
doNotStrip "*/*/libmrtc_onvif.so"
|
||||
doNotStrip "*/*/libmrtc_rtmp.so"
|
||||
doNotStrip "*/*/libmrtc_rtsp.so"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** <-----------------依赖MSDK--------------------> **/
|
||||
compileOnly deps.aircraftProvided
|
||||
implementation deps.aircraft
|
||||
|
||||
/** <-----------------sample所需--------------------> **/
|
||||
implementation project(':uxsdk')
|
||||
implementation deps.appcompat
|
||||
implementation deps.constraintLayout
|
||||
implementation deps.aacCommon
|
||||
implementation deps.aacRuntime
|
||||
implementation deps.kotlinLib
|
||||
implementation deps.ktxCore
|
||||
implementation deps.ktxFrag
|
||||
implementation deps.ktxNavigation
|
||||
implementation deps.ktxNavigationUi
|
||||
implementation deps.recyclerview
|
||||
implementation deps.legacySupport
|
||||
implementation deps.lifecycleViewModel
|
||||
implementation deps.lifecycleLiveData
|
||||
implementation deps.leakcanary
|
||||
implementation deps.glide
|
||||
implementation deps.dynamicanimation
|
||||
implementation deps.expandedit
|
||||
implementation deps.rx3Kt
|
||||
implementation deps.dom
|
||||
implementation deps.pahoMqtt
|
||||
kapt deps.glidecompiler
|
||||
implementation deps.lynx
|
||||
|
||||
implementation "androidx.activity:activity-compose:1.9.3"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7"
|
||||
implementation "androidx.compose.ui:ui:1.7.6"
|
||||
implementation "androidx.compose.ui:ui-tooling-preview:1.7.6"
|
||||
implementation "androidx.compose.material:material-icons-extended:1.7.6"
|
||||
implementation "androidx.compose.material3:material3:1.3.1"
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:1.7.6"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
||||
}
|
||||
116
sample/src/main/AndroidManifest.xml
Normal file
116
sample/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Sample permission requirement -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<!-- <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />-->
|
||||
<!-- <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />-->
|
||||
<!-- <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />-->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- 自安装使用 -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<!-- Google Maps -->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${GMAP_API_KEY}" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false"/>
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.accessory"
|
||||
android:required="true"/>
|
||||
|
||||
<application
|
||||
android:name="dji.sampleV5.aircraft.DJIAircraftApplication"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_main"
|
||||
android:label="@string/app_name_aircraft"
|
||||
android:supportsRtl="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
|
||||
|
||||
<meta-data
|
||||
android:name="com.dji.sdk.API_KEY"
|
||||
android:value="${API_KEY}"/>
|
||||
|
||||
<!-- Maplibre Token-->
|
||||
<meta-data
|
||||
android:name="com.dji.mapkit.maplibre.apikey"
|
||||
android:value="${MAPLIBRE_TOKEN}" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name="dji.sampleV5.aircraft.DJIAircraftMainActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.zklh.dronecontroller.MainActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="dji.sampleV5.aircraft.UsbAttachActivity"
|
||||
android:theme="@style/translucent_theme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
|
||||
android:resource="@xml/accessory_filter" />
|
||||
</activity>
|
||||
|
||||
<activity android:name="dji.v5.ux.sample.showcase.defaultlayout.DefaultLayoutActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="dji.v5.ux.sample.showcase.widgetlist.WidgetsActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name="dji.sampleV5.aircraft.AircraftTestingToolsActivity"
|
||||
android:theme="@style/full_screen_theme"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"/>
|
||||
</application>
|
||||
</manifest>
|
||||
563809
sample/src/main/assets/flysafe/France.json
Normal file
563809
sample/src/main/assets/flysafe/France.json
Normal file
File diff suppressed because it is too large
Load Diff
1485
sample/src/main/assets/flysafe/de.geojson
Normal file
1485
sample/src/main/assets/flysafe/de.geojson
Normal file
File diff suppressed because one or more lines are too long
BIN
sample/src/main/assets/mop/mopSample.jpeg
Normal file
BIN
sample/src/main/assets/mop/mopSample.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 MiB |
BIN
sample/src/main/assets/mop/mopSample.mp4
Normal file
BIN
sample/src/main/assets/mop/mopSample.mp4
Normal file
Binary file not shown.
BIN
sample/src/main/assets/waypointsample.kmz
Normal file
BIN
sample/src/main/assets/waypointsample.kmz
Normal file
Binary file not shown.
115
sample/src/main/java/com/zklh/dronecontroller/MainActivity.kt
Normal file
115
sample/src/main/java/com/zklh/dronecontroller/MainActivity.kt
Normal file
@@ -0,0 +1,115 @@
|
||||
package com.zklh.dronecontroller
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.content.ContextCompat
|
||||
import dji.sampleV5.aircraft.models.MSDKManagerVM
|
||||
import dji.sampleV5.aircraft.models.globalViewModels
|
||||
import dji.v5.common.register.DJISDKInitEvent
|
||||
import com.zklh.dronecontroller.core.msdk.DroneSdkManager
|
||||
import com.zklh.dronecontroller.ui.DroneControllerScreen
|
||||
import com.zklh.dronecontroller.ui.ZklhDroneTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val msdkManagerVM: MSDKManagerVM by globalViewModels()
|
||||
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) {
|
||||
DroneSdkManager.initMobileSdk(application)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
observeOfficialMsdkManager()
|
||||
enterImmersiveMode()
|
||||
requestRuntimePermissions()
|
||||
setContent {
|
||||
ZklhDroneTheme {
|
||||
DroneControllerScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeOfficialMsdkManager() {
|
||||
msdkManagerVM.lvInitProcess.observe(this) { (event, totalProcess) ->
|
||||
DroneSdkManager.onOfficialInitProcess(
|
||||
eventName = event.name,
|
||||
totalProcess = totalProcess,
|
||||
initialized = event == DJISDKInitEvent.INITIALIZE_COMPLETE
|
||||
)
|
||||
}
|
||||
msdkManagerVM.lvRegisterState.observe(this) { (success, error) ->
|
||||
DroneSdkManager.onOfficialRegisterState(
|
||||
success = success,
|
||||
errorMessage = error?.toString()
|
||||
)
|
||||
}
|
||||
msdkManagerVM.lvProductConnectionState.observe(this) { (connected, productId) ->
|
||||
DroneSdkManager.onOfficialProductConnection(connected, productId)
|
||||
}
|
||||
msdkManagerVM.lvDBDownloadProgress.observe(this) { (current, total) ->
|
||||
DroneSdkManager.onOfficialDatabaseProgress(current, total)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
enterImmersiveMode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestRuntimePermissions() {
|
||||
val permissions = buildList {
|
||||
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
add(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
add(Manifest.permission.RECORD_AUDIO)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (permissions.isNotEmpty()) {
|
||||
permissionLauncher.launch(permissions.toTypedArray())
|
||||
} else {
|
||||
DroneSdkManager.initMobileSdk(application)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterImmersiveMode() {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.decorView.post {
|
||||
window.decorView.windowInsetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.flight.FlightControlService
|
||||
import com.zklh.dronecontroller.core.flight.FlyToService
|
||||
import com.zklh.dronecontroller.core.gimbal.GimbalControlService
|
||||
import com.zklh.dronecontroller.core.livestream.LiveStreamingService
|
||||
import com.zklh.dronecontroller.core.media.CameraControlService
|
||||
import com.zklh.dronecontroller.core.media.CameraMediaService
|
||||
import com.zklh.dronecontroller.core.mission.WaypointMissionService
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.virtualstick.StickPosition
|
||||
import com.zklh.dronecontroller.core.virtualstick.VirtualStickService
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||
import dji.v5.manager.aircraft.virtualstick.Stick
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val CloudCommandLogTag = "ZklhCloudCommand"
|
||||
private const val DefaultFlyToHeightMeters = 20.0
|
||||
|
||||
class CloudCommandExecutor(
|
||||
private val flightControl: FlightControlService,
|
||||
private val cameraMedia: CameraMediaService,
|
||||
private val flyToService: FlyToService,
|
||||
private val liveStreaming: LiveStreamingService,
|
||||
private val virtualStick: VirtualStickService,
|
||||
private val waypointMission: WaypointMissionService,
|
||||
private val cameraControl: CameraControlService = CameraControlService(),
|
||||
private val gimbalControl: GimbalControlService = GimbalControlService()
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val virtualStickEnabling = AtomicBoolean(false)
|
||||
private val preparedMissionIds = ConcurrentHashMap<String, String>()
|
||||
private var lastStickAt = 0L
|
||||
private var stickTimeoutJob: Job? = null
|
||||
|
||||
suspend fun execute(request: CloudCommandRequest): DjiCommandResult {
|
||||
val command = request.normalizedMethod
|
||||
if (command.shouldLogCommand()) {
|
||||
Log.d(
|
||||
CloudCommandLogTag,
|
||||
"execute topic=${request.topic} type=${request.type} method=${request.method} normalized=$command data=${request.data}"
|
||||
)
|
||||
}
|
||||
val result = execute(command, request.data, request.type)
|
||||
if (command.shouldLogCommand()) {
|
||||
Log.d(CloudCommandLogTag, "execute result method=$command success=${result.success} message=${result.message}")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun execute(method: String, data: JSONObject): DjiCommandResult =
|
||||
execute(method.normalizeCloudCommand(), data, CloudCommandTopicType.Unknown)
|
||||
|
||||
private suspend fun execute(
|
||||
command: String,
|
||||
data: JSONObject,
|
||||
topicType: CloudCommandTopicType
|
||||
): DjiCommandResult {
|
||||
return when {
|
||||
command in PassiveAckCommands -> DjiCommandResult.ok("云控命令已确认:$command")
|
||||
command == "drc_mode_enter" -> enterDrcMode(data)
|
||||
command == "drc_mode_exit" -> exitDrcMode()
|
||||
command in StickCommands -> executeStickControl(data)
|
||||
command in TakeoffCommands && data.hasFlyToTarget() -> startFlyTo(data)
|
||||
command in TakeoffCommands -> flightControl.startTakeoff()
|
||||
command in StopTakeoffCommands -> flightControl.stopTakeoff()
|
||||
command in GoHomeCommands -> flightControl.startGoHome()
|
||||
command in StopGoHomeCommands -> flightControl.stopGoHome()
|
||||
command in AutoLandingCommands -> flightControl.startAutoLanding()
|
||||
command in ForceLandingCommands -> forceLanding()
|
||||
command in StopLandingCommands -> flightControl.stopAutoLanding()
|
||||
command in ConfirmLandingCommands -> flightControl.confirmLanding()
|
||||
command in PhotoCommands -> cameraMedia.takePhoto()
|
||||
command in RecordStartCommands -> cameraMedia.startRecord()
|
||||
command in RecordStopCommands -> cameraMedia.stopRecord()
|
||||
command in RecordToggleCommands -> cameraMedia.toggleRecord()
|
||||
command in CameraModeCommands -> setCameraMode(data)
|
||||
command in PanoramaCommands -> cameraMedia.shootPanorama()
|
||||
command in LaserFillLightCommands -> setLaserFillLight(data)
|
||||
command in LaserMeasureCommands -> setLaserMeasure(data)
|
||||
command in FlyToCommands -> startFlyTo(data)
|
||||
command in StopFlyToCommands -> flyToService.stopFlyTo()
|
||||
command in WaylinePrepareCommands -> prepareWayline(data)
|
||||
command in WaylineExecuteCommands -> executeWayline(data)
|
||||
command in WaylinePauseCommands -> waypointMission.pauseMission()
|
||||
command in WaylineRecoveryCommands -> waypointMission.resumeMission()
|
||||
command in WaylineUndoCommands -> stopWayline(data)
|
||||
command in CameraZoomCommands -> setCameraZoom(data)
|
||||
command in LensSwitchCommands -> switchLens(data)
|
||||
command in GimbalDragCommands -> rotateGimbal(data)
|
||||
command in GimbalResetCommands -> gimbalControl.reset()
|
||||
command in LiveStartCommands -> liveStreaming.startFromCloud(data)
|
||||
command in LiveStopCommands -> liveStreaming.stop()
|
||||
command in LiveQualityCommands -> liveStreaming.setQualityFromCloud(data)
|
||||
else -> DjiCommandResult.failed("暂未支持云控命令:$command")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enterDrcMode(data: JSONObject): DjiCommandResult {
|
||||
val result = ensureVirtualStickEnabled()
|
||||
if (result.success) {
|
||||
virtualStick.setSpeedLevel(data.optDoubleAny("speed_level", "speedLevel").takeIf { it > 0.0 } ?: 15.0)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private suspend fun exitDrcMode(): DjiCommandResult {
|
||||
sendNeutralStick()
|
||||
return virtualStick.disable()
|
||||
}
|
||||
|
||||
private suspend fun ensureVirtualStickEnabled(): DjiCommandResult {
|
||||
if (virtualStick.status.value.enabled) return DjiCommandResult.ok("虚拟摇杆已开启")
|
||||
if (!virtualStickEnabling.compareAndSet(false, true)) {
|
||||
return DjiCommandResult.ok("虚拟摇杆正在开启")
|
||||
}
|
||||
return try {
|
||||
virtualStick.enable()
|
||||
} finally {
|
||||
virtualStickEnabling.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun executeStickControl(
|
||||
data: JSONObject
|
||||
): DjiCommandResult {
|
||||
val enabled = ensureVirtualStickEnabled()
|
||||
if (!enabled.success) return enabled
|
||||
val speedLevel = data.optDoubleAny("speed_level", "speedLevel", "speed")
|
||||
if (speedLevel > 0.0) {
|
||||
virtualStick.setSpeedLevel(speedLevel)
|
||||
} else {
|
||||
virtualStick.setSpeedLevel(15.0)
|
||||
}
|
||||
val position = if (data.hasAny("roll", "pitch", "throttle", "yaw")) {
|
||||
data.toDrcStickPosition()
|
||||
} else {
|
||||
data.toProtocolStickPosition()
|
||||
}
|
||||
virtualStick.sendStickPosition(position)
|
||||
lastStickAt = System.currentTimeMillis()
|
||||
scheduleStickTimeout()
|
||||
return DjiCommandResult.ok("杆量控制已下发")
|
||||
}
|
||||
|
||||
private fun scheduleStickTimeout() {
|
||||
if (stickTimeoutJob?.isActive == true) return
|
||||
stickTimeoutJob = scope.launch {
|
||||
while (true) {
|
||||
delay(120)
|
||||
if (System.currentTimeMillis() - lastStickAt > 280L) {
|
||||
sendNeutralStick()
|
||||
stickTimeoutJob = null
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendNeutralStick() {
|
||||
virtualStick.sendStickPosition(StickPosition())
|
||||
}
|
||||
|
||||
private suspend fun forceLanding(): DjiCommandResult {
|
||||
val landing = flightControl.startAutoLanding()
|
||||
if (!landing.success) return landing
|
||||
delay(500)
|
||||
val confirm = flightControl.confirmLanding()
|
||||
return if (confirm.success) {
|
||||
DjiCommandResult.ok("强制降落指令已下发并确认")
|
||||
} else {
|
||||
DjiCommandResult.ok("强制降落指令已下发,确认降落返回:${confirm.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setLaserFillLight(data: JSONObject): DjiCommandResult {
|
||||
if (!data.hasAny("enable", "enabled", "ir_fill_light_enable", "irFillLightEnable")) {
|
||||
return cameraMedia.toggleLaserFillLight()
|
||||
}
|
||||
return cameraMedia.setLaserFillLightEnabled(data.optBooleanAny("enable", "enabled", "ir_fill_light_enable", "irFillLightEnable"))
|
||||
}
|
||||
|
||||
private suspend fun setLaserMeasure(data: JSONObject): DjiCommandResult {
|
||||
if (!data.hasAny("enable", "enabled")) {
|
||||
return cameraMedia.toggleLaserMeasure()
|
||||
}
|
||||
return cameraMedia.setLaserMeasureEnabled(data.optBooleanAny("enable", "enabled"))
|
||||
}
|
||||
|
||||
private suspend fun startFlyTo(data: JSONObject): DjiCommandResult {
|
||||
val target = data.optJSONObject("target_location")
|
||||
?: data.optJSONObject("targetLocation")
|
||||
?: data.optJSONObject("target_point")
|
||||
?: data.optJSONObject("targetPoint")
|
||||
?: data.optJSONObject("target")
|
||||
?: data.optJSONObject("location")
|
||||
?: data.optJSONObject("position")
|
||||
?: data.firstPoint()
|
||||
?: data
|
||||
val latitude = target.optDoubleAnyOrNull("latitude", "lat", "target_latitude", "targetLatitude")
|
||||
?: data.optDoubleAnyOrNull("targetLatitude", "target_latitude", "latitude", "lat")
|
||||
?: 0.0
|
||||
val longitude = target.optDoubleAnyOrNull("longitude", "lng", "lon", "target_longitude", "targetLongitude")
|
||||
?: data.optDoubleAnyOrNull("targetLongitude", "target_longitude", "longitude", "lng", "lon")
|
||||
?: 0.0
|
||||
val requestedHeight = target.optDoubleAnyOrNull("height", "altitude", "alt", "target_height", "targetHeight", "target_altitude")
|
||||
?: data.optDoubleAnyOrNull("targetHeight", "target_height", "height", "altitude", "alt", "commander_flight_height")
|
||||
val fallbackHeight = data.optDoubleAnyOrNull("security_takeoff_height", "securityTakeoffHeight", "safe_height", "safeHeight")
|
||||
?.takeIf { it > 0.0 }
|
||||
?: DefaultFlyToHeightMeters
|
||||
val height = requestedHeight?.takeIf { it > 0.0 } ?: fallbackHeight
|
||||
val maxSpeed = target.optIntAny(0, "max_speed", "maxSpeed")
|
||||
.takeIf { it > 0 }
|
||||
?: data.optIntAny(15, "max_speed", "maxSpeed")
|
||||
val securityTakeoffHeight = data.optIntAny(20, "security_takeoff_height", "securityTakeoffHeight")
|
||||
val flyToMode = data.optStringAny("fly_to_mode", "flyToMode")
|
||||
.toFlyToMode(defaultMode = FlyToMode.SET_HEIGHT)
|
||||
if (latitude == 0.0 || longitude == 0.0) {
|
||||
return DjiCommandResult.failed("指点飞行参数无效:需要 latitude、longitude")
|
||||
}
|
||||
Log.d(
|
||||
CloudCommandLogTag,
|
||||
"startFlyTo lat=$latitude lon=$longitude height=$height requestedHeight=$requestedHeight maxSpeed=$maxSpeed securityTakeoffHeight=$securityTakeoffHeight flyToMode=$flyToMode data=$data"
|
||||
)
|
||||
val releaseResult = releaseVirtualStickForAutonomousFlight()
|
||||
if (!releaseResult.success) {
|
||||
Log.w(CloudCommandLogTag, "release virtual stick before flyTo failed: ${releaseResult.message}")
|
||||
}
|
||||
return flyToService.startFlyTo(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
height = height,
|
||||
maxSpeed = maxSpeed,
|
||||
securityTakeoffHeight = securityTakeoffHeight,
|
||||
flyToMode = flyToMode
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun releaseVirtualStickForAutonomousFlight(): DjiCommandResult {
|
||||
sendNeutralStick()
|
||||
return if (virtualStick.status.value.enabled) {
|
||||
virtualStick.disable()
|
||||
} else {
|
||||
DjiCommandResult.ok("虚拟摇杆未开启")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setCameraMode(data: JSONObject): DjiCommandResult {
|
||||
if (data.hasAny("camera_mode", "cameraMode")) {
|
||||
return cameraMedia.setCloudCameraMode(data.optIntAny(-1, "camera_mode", "cameraMode"))
|
||||
}
|
||||
val mode = data.optStringAny("mode", "camera_mode", "cameraMode", "type").normalizeCloudCommand()
|
||||
return when (mode) {
|
||||
"photo", "shoot_photo", "shootphoto", "picture", "0" -> cameraMedia.setPhotoCaptureMode()
|
||||
"video", "record", "record_video", "recordvideo", "1" -> cameraMedia.setVideoCaptureMode()
|
||||
else -> DjiCommandResult.failed("相机模式参数无效:需要 camera_mode=0/1 或 mode=photo/video")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun prepareWayline(data: JSONObject): DjiCommandResult {
|
||||
val flightId = data.optStringAny("flight_id", "flightId", "job_id", "jobId", "mission_id", "missionId")
|
||||
val kmzPath = data.optStringAny("kmz_path", "kmzPath", "file_path", "filePath", "path")
|
||||
val fileUrl = data.optStringAny("file_url", "fileUrl", "url")
|
||||
.ifBlank {
|
||||
data.optJSONObject("file")?.optStringAny("url", "file_url", "fileUrl").orEmpty()
|
||||
}
|
||||
val localKmzPath = when {
|
||||
kmzPath.isNotBlank() -> kmzPath
|
||||
fileUrl.isNotBlank() -> downloadKmz(fileUrl)
|
||||
else -> return DjiCommandResult.failed("航线准备失败:未收到 KMZ 本地路径或下载地址")
|
||||
}
|
||||
val upload = waypointMission.uploadKmzFile(localKmzPath)
|
||||
if (!upload.success) return upload
|
||||
val missionId = data.optStringAny("mission_id", "missionId")
|
||||
.ifBlank { waypointMission.missionIdFromPath(localKmzPath) }
|
||||
if (flightId.isNotBlank()) {
|
||||
preparedMissionIds[flightId] = missionId
|
||||
}
|
||||
preparedMissionIds[missionId] = missionId
|
||||
return DjiCommandResult.ok("航线准备完成:$missionId")
|
||||
}
|
||||
|
||||
private suspend fun executeWayline(data: JSONObject): DjiCommandResult {
|
||||
val missionId = resolveMissionId(data)
|
||||
?: return DjiCommandResult.failed("航线执行失败:缺少 flight_id 或 mission_id")
|
||||
return waypointMission.startMission(missionId, data.optWaylineIds())
|
||||
}
|
||||
|
||||
private suspend fun stopWayline(data: JSONObject): DjiCommandResult {
|
||||
val missionId = resolveMissionId(data)
|
||||
?: return DjiCommandResult.failed("航线停止失败:缺少 flight_id 或 mission_id")
|
||||
return waypointMission.stopMission(missionId)
|
||||
}
|
||||
|
||||
private fun resolveMissionId(data: JSONObject): String? {
|
||||
val raw = data.optStringAny("mission_id", "missionId", "flight_id", "flightId", "job_id", "jobId")
|
||||
if (raw.isBlank()) return null
|
||||
return preparedMissionIds[raw] ?: raw
|
||||
}
|
||||
|
||||
private fun downloadKmz(fileUrl: String): String {
|
||||
val file = File.createTempFile("cloud-wayline-", ".kmz")
|
||||
URL(fileUrl).openStream().use { input ->
|
||||
file.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
return file.absolutePath
|
||||
}
|
||||
|
||||
private suspend fun setCameraZoom(data: JSONObject): DjiCommandResult {
|
||||
val zoomFactor = data.optDoubleAny("zoom_factor", "zoomFactor", "focal_length", "focalLength")
|
||||
if (zoomFactor <= 0.0) return DjiCommandResult.failed("变焦参数无效:缺少 zoom_factor")
|
||||
val cameraType = data.optStringAny("camera_type", "cameraType", "lens", "video_type", "videoType")
|
||||
.normalizeCloudCommand()
|
||||
return if (cameraType in setOf("ir", "thermal", "infrared")) {
|
||||
cameraControl.setThermalZoomRatio(zoomFactor)
|
||||
} else {
|
||||
cameraControl.setVisibleZoomRatio(zoomFactor)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun switchLens(data: JSONObject): DjiCommandResult {
|
||||
val lens = data.optStringAny("lens", "camera_type", "cameraType", "video_type", "videoType", "type")
|
||||
.ifBlank {
|
||||
when {
|
||||
data.optBooleanAny("enable", "enabled") -> "ir"
|
||||
else -> "wide"
|
||||
}
|
||||
}
|
||||
return cameraControl.setLens(lens)
|
||||
}
|
||||
|
||||
private suspend fun rotateGimbal(data: JSONObject): DjiCommandResult {
|
||||
val pitchSpeed = data.optDoubleAny("pitch_speed", "pitchSpeed", "gimbal_pitch_speed", "gimbalPitchSpeed")
|
||||
val yawSpeed = data.optDoubleAny("yaw_speed", "yawSpeed", "gimbal_yaw_speed", "gimbalYawSpeed")
|
||||
return gimbalControl.rotateBySpeed(pitchSpeed, yawSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
private val PassiveAckCommands = setOf(
|
||||
"flight_authority_grab",
|
||||
"payload_authority_grab",
|
||||
"cloud_control_auth_request",
|
||||
"cloud_control_release",
|
||||
"heart_beat",
|
||||
"drc_initial_state_subscribe"
|
||||
)
|
||||
private val StickCommands = setOf("stick_control", "drone_control", "drc_drone_stick_control")
|
||||
private val TakeoffCommands = setOf("start_takeoff", "takeoff", "take_off", "auto_takeoff")
|
||||
private val StopTakeoffCommands = setOf("stop_takeoff", "cancel_takeoff")
|
||||
private val GoHomeCommands = setOf("start_go_home", "go_home", "return_home", "start_rth", "return_auto")
|
||||
private val StopGoHomeCommands = setOf("stop_go_home", "cancel_go_home", "stop_rth", "cancel_return_home", "return_home_cancel")
|
||||
private val AutoLandingCommands = setOf("start_auto_landing", "auto_landing", "land", "landing_auto")
|
||||
private val ForceLandingCommands = setOf("drc_emergency_landing", "drc_force_landing", "emergency_landing", "force_landing")
|
||||
private val StopLandingCommands = setOf("stop_auto_landing", "cancel_landing")
|
||||
private val ConfirmLandingCommands = setOf("confirm_landing", "landing_confirm")
|
||||
private val PhotoCommands = setOf("take_photo", "photo", "camera_photo_take", "drc_camera_photo_take", "start_shoot_photo")
|
||||
private val RecordStartCommands = setOf("start_record", "camera_recording_start", "drc_camera_recording_start", "start_recording")
|
||||
private val RecordStopCommands = setOf("stop_record", "camera_recording_stop", "drc_camera_recording_stop", "stop_recording")
|
||||
private val RecordToggleCommands = setOf("toggle_record", "record")
|
||||
private val CameraModeCommands = setOf("camera_mode_switch", "drc_camera_mode_switch", "set_camera_mode", "camera_mode_set")
|
||||
private val PanoramaCommands = setOf("panorama", "shoot_panorama", "panorama_photo")
|
||||
private val LaserFillLightCommands = setOf(
|
||||
"laser_fill_light",
|
||||
"toggle_laser_fill_light",
|
||||
"infrared_fill_light_enable",
|
||||
"drc_infrared_fill_light_enable"
|
||||
)
|
||||
private val LaserMeasureCommands = setOf("laser_measure", "toggle_laser_measure")
|
||||
private val FlyToCommands = setOf("fly_to_point", "fly_to_point_update", "takeoff_to_point", "fly_to", "go_to_point", "gotargetpoint")
|
||||
private val StopFlyToCommands = setOf("stop_fly_to", "cancel_fly_to", "fly_to_point_stop")
|
||||
private val WaylinePrepareCommands = setOf("flighttask_prepare", "wayline_prepare")
|
||||
private val WaylineExecuteCommands = setOf("flighttask_execute", "wayline_execute", "start_wayline")
|
||||
private val WaylinePauseCommands = setOf("flighttask_pause", "wayline_pause")
|
||||
private val WaylineRecoveryCommands = setOf("flighttask_recovery", "wayline_resume", "wayline_recovery")
|
||||
private val WaylineUndoCommands = setOf("flighttask_undo", "wayline_stop", "stop_wayline")
|
||||
private val CameraZoomCommands = setOf("camera_focal_length_set", "drc_camera_focal_length_set", "drc_linkage_zoom_set", "linkage_zoom_set")
|
||||
private val LensSwitchCommands = setOf("camera_lens_change", "lens_change", "live_lens_change", "camera_video_stream_source_set", "camera_screen_split", "drc_camera_screen_split")
|
||||
private val GimbalDragCommands = setOf("camera_screen_drag", "drc_camera_screen_drag", "gimbal_rotate_by_speed")
|
||||
private val GimbalResetCommands = setOf("gimbal_reset", "drc_gimbal_reset")
|
||||
private val LiveStartCommands = setOf("live_start_push", "live_start", "start_live", "start_livestream")
|
||||
private val LiveStopCommands = setOf("live_stop_push", "live_stop", "stop_live", "stop_livestream")
|
||||
private val LiveQualityCommands = setOf("live_set_quality", "live_quality", "set_live_quality")
|
||||
|
||||
private fun JSONObject.toDrcStickPosition(): StickPosition {
|
||||
fun axis(name: String): Int =
|
||||
(((optDouble(name, 1024.0) - 1024.0) / 1024.0) * Stick.MAX_STICK_POSITION_ABS)
|
||||
.roundToInt()
|
||||
.coerceIn(-Stick.MAX_STICK_POSITION_ABS, Stick.MAX_STICK_POSITION_ABS)
|
||||
|
||||
return StickPosition(
|
||||
leftHorizontal = axis("yaw"),
|
||||
leftVertical = axis("throttle"),
|
||||
rightHorizontal = axis("roll"),
|
||||
rightVertical = axis("pitch")
|
||||
)
|
||||
}
|
||||
|
||||
private fun JSONObject.toProtocolStickPosition(): StickPosition {
|
||||
fun scaled(name: String, fullScale: Double): Int =
|
||||
((optDouble(name, 0.0) / fullScale) * Stick.MAX_STICK_POSITION_ABS)
|
||||
.roundToInt()
|
||||
.coerceIn(-Stick.MAX_STICK_POSITION_ABS, Stick.MAX_STICK_POSITION_ABS)
|
||||
|
||||
return StickPosition(
|
||||
leftHorizontal = scaled("w", 20.0),
|
||||
leftVertical = scaled("h", 5.0),
|
||||
rightHorizontal = scaled("y", 5.0),
|
||||
rightVertical = scaled("x", 5.0)
|
||||
)
|
||||
}
|
||||
|
||||
private fun JSONObject.optDoubleAny(vararg names: String): Double {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optDouble(name)
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
private fun JSONObject.optDoubleAnyOrNull(vararg names: String): Double? {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optDouble(name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun JSONObject.optIntAny(defaultValue: Int, vararg names: String): Int {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optInt(name, defaultValue)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
private fun JSONObject.optStringAny(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optString(name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optBooleanAny(vararg names: String): Boolean {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optBoolean(name)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun String.toFlyToMode(defaultMode: FlyToMode): FlyToMode =
|
||||
when (normalizeCloudCommand()) {
|
||||
"smart_height", "smartheight", "smart" -> FlyToMode.SMART_HEIGHT
|
||||
"set_height", "setheight", "fixed_height", "fixedheight", "height", "" -> defaultMode
|
||||
else -> defaultMode
|
||||
}
|
||||
|
||||
private fun JSONObject.hasAny(vararg names: String): Boolean =
|
||||
names.any { has(it) && !isNull(it) }
|
||||
|
||||
private fun JSONObject.firstPoint(): JSONObject? {
|
||||
val points = optJSONArray("points") ?: return null
|
||||
if (points.length() <= 0) return null
|
||||
return points.optJSONObject(0)
|
||||
}
|
||||
|
||||
private fun JSONObject.hasFlyToTarget(): Boolean {
|
||||
val target = optJSONObject("target_location")
|
||||
?: optJSONObject("targetLocation")
|
||||
?: optJSONObject("target_point")
|
||||
?: optJSONObject("targetPoint")
|
||||
?: optJSONObject("target")
|
||||
?: optJSONObject("location")
|
||||
?: optJSONObject("position")
|
||||
?: firstPoint()
|
||||
?: this
|
||||
return target.hasAny("latitude", "lat", "target_latitude", "targetLatitude") &&
|
||||
target.hasAny("longitude", "lng", "lon", "target_longitude", "targetLongitude")
|
||||
}
|
||||
|
||||
private fun String.shouldLogCommand(): Boolean =
|
||||
this !in PassiveAckCommands && this !in StickCommands
|
||||
|
||||
private fun JSONObject.optWaylineIds(): List<Int> {
|
||||
val ids = optJSONArray("wayline_ids") ?: optJSONArray("waylineIds") ?: return listOf(0)
|
||||
return buildList {
|
||||
for (index in 0 until ids.length()) {
|
||||
add(ids.optInt(index, 0))
|
||||
}
|
||||
}.ifEmpty { listOf(0) }
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
enum class CloudCommandTopicType {
|
||||
Services,
|
||||
PropertySet,
|
||||
DrcDown,
|
||||
Unknown
|
||||
}
|
||||
|
||||
data class CloudCommandRequest(
|
||||
val topic: String,
|
||||
val targetSn: String,
|
||||
val type: CloudCommandTopicType,
|
||||
val method: String,
|
||||
val data: JSONObject,
|
||||
val tid: String,
|
||||
val bid: String,
|
||||
val seq: Long?
|
||||
) {
|
||||
val normalizedMethod: String = method.normalizeCloudCommand()
|
||||
|
||||
internal fun mismatchedSn(identity: CloudDeviceIdentity): String? {
|
||||
val localSns = setOf(identity.remoteControllerSn, identity.aircraftSn)
|
||||
.filter { it.isNotBlank() }
|
||||
.toSet()
|
||||
if (targetSn.isNotBlank() && targetSn !in localSns) {
|
||||
return targetSn
|
||||
}
|
||||
for (field in SnFields) {
|
||||
val value = data.optString(field).trim()
|
||||
if (value.isNotBlank() && value !in localSns) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(topic: String, payload: String): CloudCommandRequest {
|
||||
val request = JSONObject(payload)
|
||||
val data = request.optJSONObject("data") ?: JSONObject()
|
||||
val method = request.optString("method")
|
||||
.ifBlank { data.optString("method") }
|
||||
.ifBlank { request.optString("cmd") }
|
||||
.ifBlank { data.optString("cmd") }
|
||||
return CloudCommandRequest(
|
||||
topic = topic,
|
||||
targetSn = topic.split("/").getOrNull(2).orEmpty(),
|
||||
type = topic.type(),
|
||||
method = method,
|
||||
data = data,
|
||||
tid = request.optString("tid"),
|
||||
bid = request.optString("bid"),
|
||||
seq = request.optLongOrNull("seq") ?: data.optLongOrNull("seq")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val SnFields = listOf(
|
||||
"sn",
|
||||
"device_sn",
|
||||
"deviceSn",
|
||||
"gateway_sn",
|
||||
"gatewaySn",
|
||||
"dock_sn",
|
||||
"dockSn",
|
||||
"drone_sn",
|
||||
"droneSn"
|
||||
)
|
||||
|
||||
private fun String.type(): CloudCommandTopicType =
|
||||
when {
|
||||
endsWith("/services") -> CloudCommandTopicType.Services
|
||||
endsWith("/property/set") -> CloudCommandTopicType.PropertySet
|
||||
endsWith("/drc/down") -> CloudCommandTopicType.DrcDown
|
||||
else -> CloudCommandTopicType.Unknown
|
||||
}
|
||||
|
||||
fun String.normalizeCloudCommand(): String =
|
||||
trim()
|
||||
.replace("-", "_")
|
||||
.replace(".", "_")
|
||||
.lowercase()
|
||||
|
||||
private fun JSONObject.optLongOrNull(name: String): Long? =
|
||||
if (has(name) && !isNull(name)) optLong(name) else null
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import android.util.Log
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
import java.net.URLEncoder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val CloudLoginLogTag = "ZklhCloudLogin"
|
||||
private const val CloudPlatformHost = "uav.zklhjs.com"
|
||||
private const val CloudPlatformFallbackIp = "221.226.33.58"
|
||||
|
||||
class CloudLoginClient {
|
||||
suspend fun login(request: CloudLoginRequest): Result<CloudSession> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val originCandidates = apiOriginCandidates(request.baseUrl)
|
||||
val djiBaseUrlCandidates = djiApiBaseUrlCandidates(request.baseUrl)
|
||||
val tenantName = request.tenantName.trim()
|
||||
val username = request.username.trim()
|
||||
Log.i(
|
||||
CloudLoginLogTag,
|
||||
"login start tenantName=$tenantName username=$username originCandidates=$originCandidates djiBaseUrlCandidates=$djiBaseUrlCandidates mqttOverride=${request.mqttAddressOverride.isNotBlank()}"
|
||||
)
|
||||
require(tenantName.isNotBlank()) { "请输入租户名称" }
|
||||
require(username.isNotBlank()) { "请输入账号" }
|
||||
require(request.password.isNotBlank()) { "请输入密码" }
|
||||
|
||||
val tenant = resolveTenantId(originCandidates, tenantName)
|
||||
Log.i(CloudLoginLogTag, "tenant resolved tenantId=${tenant.tenantId} baseUrl=${tenant.baseUrl}")
|
||||
val payload = JSONObject()
|
||||
.put("username", username)
|
||||
.put("password", request.password)
|
||||
.put("flag", request.flag)
|
||||
val login = loginManage(djiBaseUrlCandidates, tenant.baseUrl, tenant.tenantId, payload)
|
||||
val data = login.data
|
||||
val mqttAddress = request.mqttAddressOverride.trim().ifBlank {
|
||||
data.optStringAny("mqtt_addr", "mqttAddr")
|
||||
}
|
||||
val rawMqttUsername = data.optStringAny("mqtt_username", "mqttUsername").ifBlank { username }
|
||||
CloudSession(
|
||||
baseUrl = login.baseUrl,
|
||||
tenantId = tenant.tenantId,
|
||||
tenantName = tenantName,
|
||||
username = data.optStringAny("username").ifBlank { username },
|
||||
workspaceId = data.optStringAny("workspace_id", "workspaceId"),
|
||||
accessToken = data.optStringAny("access_token", "accessToken"),
|
||||
mqttAddress = normalizeMqttAddress(mqttAddress),
|
||||
mqttUsername = normalizeMqttUsername(tenant.tenantId, rawMqttUsername),
|
||||
mqttPassword = data.optStringAny("mqtt_password", "mqttPassword").ifBlank { request.password },
|
||||
deptId = data.optLongAny("dept_id", "deptId")
|
||||
).also { session ->
|
||||
require(session.workspaceId.isNotBlank()) { "登录成功但未返回 workspace_id" }
|
||||
require(session.mqttAddress.isNotBlank()) { "登录成功但未返回 mqtt_addr,请手动填写 MQTT 地址" }
|
||||
require(session.accessToken.isNotBlank()) { "登录成功但未返回 access_token" }
|
||||
Log.i(
|
||||
CloudLoginLogTag,
|
||||
"login success baseUrl=${session.baseUrl} tenantId=${session.tenantId} workspaceId=${session.workspaceId} mqttAddress=${session.mqttAddress} mqttUsername=${session.mqttUsername}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveTenantId(baseUrls: List<String>, tenantName: String): TenantResolution {
|
||||
if (tenantName.all { it.isDigit() }) {
|
||||
Log.d(CloudLoginLogTag, "tenantName is numeric, use as tenantId=$tenantName")
|
||||
return TenantResolution(tenantId = tenantName, baseUrl = baseUrls.first())
|
||||
}
|
||||
var lastError: Throwable? = null
|
||||
val tenantNameCandidates = if (tenantName == "管理平台") {
|
||||
listOf("管理平台", "中科联航")
|
||||
} else {
|
||||
listOf(tenantName)
|
||||
}
|
||||
Log.d(CloudLoginLogTag, "resolve tenant candidates=$tenantNameCandidates baseUrls=$baseUrls")
|
||||
for (candidate in tenantNameCandidates) {
|
||||
val encodedName = URLEncoder.encode(candidate, Charsets.UTF_8.name())
|
||||
for (baseUrl in baseUrls) {
|
||||
val result = runCatching {
|
||||
Log.i(CloudLoginLogTag, "resolve tenant request baseUrl=$baseUrl tenantName=$candidate")
|
||||
val response = getJson(
|
||||
url = "$baseUrl/saas/admin-api/system/tenant/get-id-by-name?name=$encodedName",
|
||||
tenantId = "1",
|
||||
token = null
|
||||
)
|
||||
val tenantId = parseTenantIdResponse(response)
|
||||
Log.i(CloudLoginLogTag, "resolve tenant success baseUrl=$baseUrl tenantName=$candidate tenantId=$tenantId")
|
||||
TenantResolution(tenantId = tenantId, baseUrl = baseUrl)
|
||||
}
|
||||
result.onSuccess { return it }
|
||||
result.onFailure {
|
||||
Log.w(CloudLoginLogTag, "resolve tenant failed baseUrl=$baseUrl tenantName=$candidate: ${it.message}")
|
||||
lastError = it
|
||||
}
|
||||
}
|
||||
}
|
||||
error(lastError?.message ?: "租户名称解析失败")
|
||||
}
|
||||
|
||||
private fun loginManage(
|
||||
djiBaseUrls: List<String>,
|
||||
preferredOrigin: String,
|
||||
tenantId: String,
|
||||
payload: JSONObject
|
||||
): LoginResponse {
|
||||
var lastError: Throwable? = null
|
||||
val orderedBaseUrls = (listOf("$preferredOrigin/dji") + djiBaseUrls).distinct()
|
||||
for (baseUrl in orderedBaseUrls) {
|
||||
val result = runCatching {
|
||||
Log.i(CloudLoginLogTag, "manage login request baseUrl=$baseUrl tenantId=$tenantId username=${payload.optString("username")}")
|
||||
val response = postJson(
|
||||
url = "$baseUrl/manage/api/v1/login",
|
||||
tenantId = tenantId,
|
||||
token = null,
|
||||
body = payload
|
||||
)
|
||||
val root = JSONObject(response)
|
||||
val code = root.optInt("code", 0)
|
||||
Log.i(CloudLoginLogTag, "manage login response baseUrl=$baseUrl code=$code message=${root.optMessage("")}")
|
||||
if (code != 0 && code != 200) {
|
||||
error(root.optMessage("第三方云登录失败"))
|
||||
}
|
||||
LoginResponse(
|
||||
baseUrl = baseUrl,
|
||||
data = root.optJSONObject("data") ?: root
|
||||
)
|
||||
}
|
||||
result.onSuccess { return it }
|
||||
result.onFailure {
|
||||
Log.w(CloudLoginLogTag, "manage login failed baseUrl=$baseUrl tenantId=$tenantId: ${it.message}")
|
||||
lastError = it
|
||||
}
|
||||
}
|
||||
error(lastError?.message ?: "第三方云登录失败")
|
||||
}
|
||||
|
||||
private fun parseTenantIdResponse(response: String): String {
|
||||
val text = response.trim()
|
||||
if (!text.startsWith("{")) {
|
||||
return text.trim('"').also { require(it.isNotBlank()) { "租户名称解析失败:返回为空" } }
|
||||
}
|
||||
val root = JSONObject(text)
|
||||
val code = root.optInt("code", 0)
|
||||
if (code != 0 && code != 200) {
|
||||
error(root.optMessage("租户名称解析失败"))
|
||||
}
|
||||
val tenantId = root.opt("data")?.toString()?.trim().orEmpty()
|
||||
require(tenantId.isNotBlank() && tenantId != "null") { "租户名称解析失败:未返回租户 ID" }
|
||||
return tenantId
|
||||
}
|
||||
|
||||
private fun getJson(
|
||||
url: String,
|
||||
tenantId: String,
|
||||
token: String?
|
||||
): String {
|
||||
val connection = URI(url).toURL().openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "GET"
|
||||
connection.connectTimeout = 10_000
|
||||
connection.readTimeout = 15_000
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
platformHostHeaderForUrl(url)?.let { connection.setRequestProperty("Host", it) }
|
||||
if (tenantId.isNotBlank()) {
|
||||
connection.setRequestProperty("Tenant-Id", tenantId)
|
||||
connection.setRequestProperty("tenant-id", tenantId)
|
||||
}
|
||||
if (!token.isNullOrBlank()) connection.setRequestProperty("x-auth-token", token)
|
||||
val stream = if (connection.responseCode in 200..299) {
|
||||
connection.inputStream
|
||||
} else {
|
||||
connection.errorStream ?: connection.inputStream
|
||||
}
|
||||
val text = BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
|
||||
if (connection.responseCode !in 200..299) {
|
||||
error("HTTP ${connection.responseCode}: $text")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
suspend fun bindDevice(session: CloudSession, deviceSn: String): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
if (deviceSn.isBlank() || session.workspaceId.isBlank()) return@runCatching
|
||||
val encodedSn = URLEncoder.encode(deviceSn, Charsets.UTF_8.name())
|
||||
val body = JSONObject()
|
||||
.put("workspace_id", session.workspaceId)
|
||||
.put("workspaceId", session.workspaceId)
|
||||
postJson(
|
||||
url = "${session.baseUrl}/manage/api/v1/devices/$encodedSn/binding",
|
||||
tenantId = session.tenantId,
|
||||
token = session.accessToken,
|
||||
body = body
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postJson(
|
||||
url: String,
|
||||
tenantId: String,
|
||||
token: String?,
|
||||
body: JSONObject
|
||||
): String {
|
||||
val connection = URI(url).toURL().openConnection() as HttpURLConnection
|
||||
connection.requestMethod = "POST"
|
||||
connection.connectTimeout = 10_000
|
||||
connection.readTimeout = 15_000
|
||||
connection.doOutput = true
|
||||
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
platformHostHeaderForUrl(url)?.let { connection.setRequestProperty("Host", it) }
|
||||
if (tenantId.isNotBlank()) {
|
||||
connection.setRequestProperty("Tenant-Id", tenantId)
|
||||
connection.setRequestProperty("tenant-id", tenantId)
|
||||
}
|
||||
if (!token.isNullOrBlank()) connection.setRequestProperty("x-auth-token", token)
|
||||
OutputStreamWriter(connection.outputStream, Charsets.UTF_8).use { writer ->
|
||||
writer.write(body.toString())
|
||||
}
|
||||
val stream = if (connection.responseCode in 200..299) {
|
||||
connection.inputStream
|
||||
} else {
|
||||
connection.errorStream ?: connection.inputStream
|
||||
}
|
||||
val text = BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
|
||||
if (connection.responseCode !in 200..299) {
|
||||
error("HTTP ${connection.responseCode}: $text")
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun normalizeApiBaseUrl(rawUrl: String): String {
|
||||
return djiApiBaseUrlCandidates(rawUrl).first()
|
||||
}
|
||||
|
||||
private fun apiOriginCandidates(rawUrl: String): List<String> {
|
||||
val origin = normalizeOrigin(rawUrl)
|
||||
return (platformFallbackOrigin(origin)?.let { listOf(origin, it) } ?: listOf(origin)).distinct()
|
||||
}
|
||||
|
||||
private fun djiApiBaseUrlCandidates(rawUrl: String): List<String> {
|
||||
return apiOriginCandidates(rawUrl).map { "$it/dji" }
|
||||
}
|
||||
|
||||
private fun normalizeOrigin(rawUrl: String): String {
|
||||
val withScheme = rawUrl.trim().ifBlank { "http://uav.zklhjs.com" }.let {
|
||||
if (it.startsWith("http://", true) || it.startsWith("https://", true)) it else "http://$it"
|
||||
}
|
||||
val uri = URI(withScheme)
|
||||
val scheme = uri.scheme ?: "http"
|
||||
val host = uri.host ?: withScheme.removePrefix("$scheme://").substringBefore('/').substringBefore(':')
|
||||
val port = if (uri.port > 0) ":${uri.port}" else ""
|
||||
return "$scheme://$host$port"
|
||||
}
|
||||
|
||||
private fun normalizeMqttUsername(tenantId: String, rawUsername: String): String {
|
||||
val username = rawUsername.trim()
|
||||
return if (username.startsWith("${tenantId}_")) username else "${tenantId}_$username"
|
||||
}
|
||||
|
||||
fun normalizeMqttAddress(rawAddress: String): String {
|
||||
val value = rawAddress.trim()
|
||||
if (value.isBlank()) return ""
|
||||
return when {
|
||||
value.startsWith("tcp://", true) ||
|
||||
value.startsWith("ssl://", true) ||
|
||||
value.startsWith("ws://", true) ||
|
||||
value.startsWith("wss://", true) -> value
|
||||
value.startsWith("mqtt://", true) -> "tcp://" + value.substringAfter("://")
|
||||
else -> "tcp://$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun mqttAddressCandidates(rawAddress: String): List<String> {
|
||||
val normalized = normalizeMqttAddress(rawAddress)
|
||||
if (normalized.isBlank()) return emptyList()
|
||||
return listOfNotNull(normalized, platformFallbackMqttAddress(normalized)).distinct()
|
||||
}
|
||||
|
||||
private fun platformFallbackOrigin(origin: String): String? {
|
||||
val uri = URI(origin)
|
||||
val scheme = uri.scheme ?: return null
|
||||
val host = uri.host ?: return null
|
||||
if (!scheme.equals("http", ignoreCase = true) || !host.equals(CloudPlatformHost, ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
val port = if (uri.port > 0) ":${uri.port}" else ""
|
||||
return "$scheme://$CloudPlatformFallbackIp$port"
|
||||
}
|
||||
|
||||
private fun platformFallbackMqttAddress(address: String): String? {
|
||||
val uri = runCatching { URI(address) }.getOrNull() ?: return null
|
||||
val host = uri.host ?: return null
|
||||
if (!host.equals(CloudPlatformHost, ignoreCase = true)) return null
|
||||
return URI(uri.scheme, uri.userInfo, CloudPlatformFallbackIp, uri.port, uri.path, uri.query, uri.fragment).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun platformHostHeaderForUrl(url: String): String? {
|
||||
val uri = runCatching { URI(url) }.getOrNull() ?: return null
|
||||
if (!uri.host.equals(CloudPlatformFallbackIp, ignoreCase = true)) return null
|
||||
val defaultPort = (uri.scheme.equals("http", ignoreCase = true) && uri.port == 80) ||
|
||||
(uri.scheme.equals("https", ignoreCase = true) && uri.port == 443)
|
||||
val port = if (uri.port > 0 && !defaultPort) ":${uri.port}" else ""
|
||||
return "$CloudPlatformHost$port"
|
||||
}
|
||||
|
||||
private data class TenantResolution(
|
||||
val tenantId: String,
|
||||
val baseUrl: String
|
||||
)
|
||||
|
||||
private data class LoginResponse(
|
||||
val baseUrl: String,
|
||||
val data: JSONObject
|
||||
)
|
||||
|
||||
private fun JSONObject.optStringAny(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optString(name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optLongAny(vararg names: String): Long? {
|
||||
for (name in names) {
|
||||
if (has(name) && !isNull(name)) return optLong(name)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun JSONObject.optMessage(fallback: String): String =
|
||||
optStringAny("msg", "message").ifBlank { fallback }
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
data class CloudLoginRequest(
|
||||
val baseUrl: String,
|
||||
val tenantName: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val flag: Int = 2,
|
||||
val mqttAddressOverride: String = ""
|
||||
)
|
||||
|
||||
data class CloudSession(
|
||||
val baseUrl: String,
|
||||
val tenantId: String,
|
||||
val tenantName: String,
|
||||
val username: String,
|
||||
val workspaceId: String,
|
||||
val accessToken: String,
|
||||
val mqttAddress: String,
|
||||
val mqttUsername: String,
|
||||
val mqttPassword: String,
|
||||
val deptId: Long? = null
|
||||
) {
|
||||
val payloadUsername: String
|
||||
get() = if (tenantId.isNotBlank() && username.isNotBlank()) {
|
||||
"${tenantId}_${username}"
|
||||
} else {
|
||||
mqttUsername
|
||||
}
|
||||
}
|
||||
|
||||
data class CloudMqttState(
|
||||
val connected: Boolean = false,
|
||||
val connecting: Boolean = false,
|
||||
val session: CloudSession? = null,
|
||||
val message: String = "未连接",
|
||||
val lastOnlineAt: Long = 0L,
|
||||
val lastOsdAt: Long = 0L,
|
||||
val lastCommand: String = "",
|
||||
val lastError: String? = null
|
||||
)
|
||||
|
||||
internal data class CloudDeviceIdentity(
|
||||
val remoteControllerSn: String = "",
|
||||
val aircraftSn: String = "",
|
||||
val productConnected: Boolean = false,
|
||||
val productLinkConnected: Boolean = false
|
||||
) {
|
||||
val readyForOnline: Boolean
|
||||
get() = remoteControllerSn.isNotBlank() && aircraftSn.isNotBlank()
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
package com.zklh.dronecontroller.core.cloud
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DroneSdkState
|
||||
import com.zklh.dronecontroller.core.telemetry.TelemetrySnapshot
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.sqrt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
|
||||
import org.eclipse.paho.client.mqttv3.MqttCallback
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
|
||||
import org.eclipse.paho.client.mqttv3.MqttException
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
private const val CloudMqttLogTag = "ZklhCloudMqtt"
|
||||
private const val RcDomain = 2
|
||||
private const val DroneDomain = 0
|
||||
private const val RcPlus2Type = 174
|
||||
private const val Matrice4Type = 99
|
||||
private const val RcSubType = 0
|
||||
private const val Matrice4SubType = 1
|
||||
private const val Matrice4CameraPayloadIndex = "89-0-0"
|
||||
private const val Matrice4LiveVideoIndex = "normal-0"
|
||||
private const val ThingVersion = "1.2.0"
|
||||
private const val CloudAccessType = "msdk"
|
||||
private const val OSD_MIN_INTERVAL_MS = 1_000L
|
||||
private const val JsonClassKey = "@class"
|
||||
private const val OsdRemoteControlClass = "com.dji.sdk.cloudapi.device.OsdRemoteControl"
|
||||
private const val WirelessLinkClass = "com.dji.sdk.cloudapi.device.WirelessLink"
|
||||
private const val OsdRcDroneClass = "com.dji.sdk.cloudapi.device.OsdRcDrone"
|
||||
private const val DroneBatteryClass = "com.dji.sdk.cloudapi.device.DroneBattery"
|
||||
private const val BatteryClass = "com.dji.sdk.cloudapi.device.Battery"
|
||||
private const val DronePositionStateClass = "com.dji.sdk.cloudapi.device.DronePositionState"
|
||||
private const val RcDistanceLimitStatusClass = "com.dji.sdk.cloudapi.device.RcDistanceLimitStatus"
|
||||
private const val StorageClass = "com.dji.sdk.cloudapi.device.Storage"
|
||||
private const val OsdCameraClass = "com.dji.sdk.cloudapi.device.OsdCamera"
|
||||
private const val RcDronePayloadClass = "com.dji.sdk.cloudapi.device.RcDronePayload"
|
||||
private const val JavaArrayListClass = "java.util.ArrayList"
|
||||
private const val EarthRadiusMeters = 6_371_000.0
|
||||
|
||||
class CloudMqttService(
|
||||
private val loginClient: CloudLoginClient = CloudLoginClient()
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val _state = MutableStateFlow(CloudMqttState())
|
||||
val state: StateFlow<CloudMqttState> = _state.asStateFlow()
|
||||
|
||||
private val connecting = AtomicBoolean(false)
|
||||
private var client: MqttClient? = null
|
||||
private var commandExecutor: CloudCommandExecutor? = null
|
||||
private var session: CloudSession? = null
|
||||
private var deviceIdentity = CloudDeviceIdentity()
|
||||
private var telemetry = TelemetrySnapshot()
|
||||
private var subscribedControlKey = ""
|
||||
private var onlineKey = ""
|
||||
private var liveCapacityKey = ""
|
||||
private var lastOsdAt = 0L
|
||||
|
||||
fun connect(
|
||||
session: CloudSession,
|
||||
commandExecutor: CloudCommandExecutor
|
||||
) {
|
||||
if (!connecting.compareAndSet(false, true)) return
|
||||
this.session = session
|
||||
this.commandExecutor = commandExecutor
|
||||
_state.update {
|
||||
it.copy(
|
||||
connecting = true,
|
||||
session = session,
|
||||
message = "正在连接 MQTT:${session.mqttAddress}",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
scope.launch {
|
||||
runCatching {
|
||||
client?.takeIf { it.isConnected }?.disconnect()
|
||||
client?.close()
|
||||
val clientId = "zklh-rc-${deviceIdentity.remoteControllerSn.ifBlank { UUID.randomUUID().toString() }}"
|
||||
val mqttAddresses = CloudLoginClient.mqttAddressCandidates(session.mqttAddress)
|
||||
.ifEmpty { listOf(session.mqttAddress) }
|
||||
var connectedClient: MqttClient? = null
|
||||
var connectedSession = session
|
||||
var lastConnectError: Throwable? = null
|
||||
for (mqttAddress in mqttAddresses) {
|
||||
val result = runCatching {
|
||||
Log.i(CloudMqttLogTag, "connect attempt address=$mqttAddress username=${session.mqttUsername}")
|
||||
val mqttClient = MqttClient(mqttAddress, clientId, MemoryPersistence())
|
||||
mqttClient.setCallback(callback())
|
||||
val options = MqttConnectOptions().apply {
|
||||
isAutomaticReconnect = true
|
||||
isCleanSession = true
|
||||
connectionTimeout = 10
|
||||
keepAliveInterval = 20
|
||||
userName = session.mqttUsername
|
||||
password = session.mqttPassword.toCharArray()
|
||||
}
|
||||
mqttClient.connect(options)
|
||||
mqttClient
|
||||
}
|
||||
if (result.isSuccess) {
|
||||
connectedClient = result.getOrThrow()
|
||||
connectedSession = session.copy(mqttAddress = mqttAddress)
|
||||
break
|
||||
} else {
|
||||
val error = result.exceptionOrNull()
|
||||
Log.w(CloudMqttLogTag, "connect attempt failed address=$mqttAddress: ${error?.message}")
|
||||
lastConnectError = error
|
||||
}
|
||||
}
|
||||
val mqttClient = connectedClient ?: throw lastConnectError ?: IllegalStateException("MQTT 连接失败")
|
||||
client = mqttClient
|
||||
this@CloudMqttService.session = connectedSession
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = true,
|
||||
connecting = false,
|
||||
session = connectedSession,
|
||||
message = "MQTT 已连接",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
subscribeControlTopicsIfReady()
|
||||
publishOnlineIfReady(force = true)
|
||||
publishOsdIfReady(force = true)
|
||||
}.onFailure { error ->
|
||||
Log.e(CloudMqttLogTag, "connect failed", error)
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
connecting = false,
|
||||
message = "MQTT 连接失败",
|
||||
lastError = error.message ?: error.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
connecting.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
scope.launch {
|
||||
runCatching { publishStatusOnline(online = false) }
|
||||
runCatching { client?.disconnect() }
|
||||
runCatching { client?.close() }
|
||||
client = null
|
||||
subscribedControlKey = ""
|
||||
onlineKey = ""
|
||||
liveCapacityKey = ""
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
connecting = false,
|
||||
message = "第三方云已断开",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
runCatching { publishStatusOnline(online = false) }
|
||||
runCatching { client?.disconnectForcibly(500, 500, false) }
|
||||
runCatching { client?.close() }
|
||||
client = null
|
||||
subscribedControlKey = ""
|
||||
onlineKey = ""
|
||||
liveCapacityKey = ""
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
fun updateDeviceState(sdkState: DroneSdkState) {
|
||||
deviceIdentity = CloudDeviceIdentity(
|
||||
remoteControllerSn = sdkState.remoteControllerSerialNumber,
|
||||
aircraftSn = sdkState.aircraftSerialNumber,
|
||||
productConnected = sdkState.productConnected,
|
||||
productLinkConnected = sdkState.productLinkConnected
|
||||
)
|
||||
scope.launch {
|
||||
subscribeControlTopicsIfReady()
|
||||
publishOnlineIfReady(force = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTelemetry(snapshot: TelemetrySnapshot) {
|
||||
telemetry = snapshot
|
||||
scope.launch {
|
||||
publishOsdIfReady(force = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun callback(): MqttCallback =
|
||||
object : MqttCallback {
|
||||
override fun connectionLost(cause: Throwable?) {
|
||||
Log.e(CloudMqttLogTag, "connection lost", cause)
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
connecting = false,
|
||||
message = "MQTT 连接已断开",
|
||||
lastError = cause?.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun messageArrived(topic: String, message: MqttMessage) {
|
||||
val payload = message.payload.toString(Charsets.UTF_8)
|
||||
Log.d(CloudMqttLogTag, "message arrived topic=$topic payload=$payload")
|
||||
scope.launch { handleCommand(topic, payload) }
|
||||
}
|
||||
|
||||
override fun deliveryComplete(token: IMqttDeliveryToken?) = Unit
|
||||
}
|
||||
|
||||
private fun subscribeControlTopicsIfReady() {
|
||||
val mqttClient = client ?: return
|
||||
val rcSn = deviceIdentity.remoteControllerSn
|
||||
val aircraftSn = deviceIdentity.aircraftSn
|
||||
val key = "$rcSn/$aircraftSn"
|
||||
if (!mqttClient.isConnected || rcSn.isBlank() || subscribedControlKey == key) return
|
||||
val topics = buildList {
|
||||
add("thing/product/$rcSn/services")
|
||||
add("thing/product/$rcSn/property/set")
|
||||
add("thing/product/$rcSn/drc/down")
|
||||
if (aircraftSn.isNotBlank()) {
|
||||
add("thing/product/$aircraftSn/services")
|
||||
add("thing/product/$aircraftSn/property/set")
|
||||
}
|
||||
}.toTypedArray()
|
||||
runCatching {
|
||||
mqttClient.subscribe(topics, IntArray(topics.size) { 1 })
|
||||
subscribedControlKey = key
|
||||
_state.update { it.copy(message = "MQTT 已订阅云控命令:${topics.size} 个 topic") }
|
||||
Log.d(CloudMqttLogTag, "subscribed control topics=${topics.joinToString()}")
|
||||
}.onFailure { error ->
|
||||
Log.e(CloudMqttLogTag, "subscribe failed", error)
|
||||
_state.update { it.copy(lastError = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleCommand(topic: String, payload: String) {
|
||||
val executor = commandExecutor ?: return
|
||||
val request = runCatching { CloudCommandRequest.parse(topic, payload) }.getOrElse { error ->
|
||||
publishReply(topic, "", "", "", null, false, "云控命令 JSON 解析失败:${error.message}")
|
||||
return
|
||||
}
|
||||
val mismatchedSn = request.mismatchedSn(deviceIdentity)
|
||||
if (mismatchedSn != null) {
|
||||
publishReply(request, false, "云控命令目标 SN 不属于本机:$mismatchedSn")
|
||||
return
|
||||
}
|
||||
if (request.method.isBlank()) {
|
||||
publishReply(request, false, "云控命令缺少 method")
|
||||
return
|
||||
}
|
||||
val result = runCatching { executor.execute(request) }
|
||||
.getOrElse { error -> com.zklh.dronecontroller.core.msdk.DjiCommandResult.failed(error.message ?: error.toString()) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
lastCommand = "${request.method}:${result.message}",
|
||||
message = if (result.success) "云控命令已执行" else "云控命令执行失败"
|
||||
)
|
||||
}
|
||||
publishReply(request, result.success, result.message)
|
||||
}
|
||||
|
||||
private fun publishOnlineIfReady(force: Boolean) {
|
||||
if (!isConnected()) return
|
||||
val identity = deviceIdentity
|
||||
if (!identity.readyForOnline) return
|
||||
val key = "${identity.remoteControllerSn}/${identity.aircraftSn}/${session?.workspaceId}"
|
||||
if (!force && onlineKey == key) return
|
||||
publishStatusOnline(online = true)
|
||||
onlineKey = key
|
||||
val now = System.currentTimeMillis()
|
||||
_state.update { it.copy(lastOnlineAt = now, message = "设备上线信息已上报") }
|
||||
publishLiveCapacityIfReady(identity)
|
||||
bindDevices(identity)
|
||||
}
|
||||
|
||||
private fun bindDevices(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
scope.launch {
|
||||
loginClient.bindDevice(cloudSession, identity.remoteControllerSn)
|
||||
.onFailure { Log.w(CloudMqttLogTag, "bind rc failed: ${it.message}") }
|
||||
loginClient.bindDevice(cloudSession, identity.aircraftSn)
|
||||
.onFailure { Log.w(CloudMqttLogTag, "bind drone failed: ${it.message}") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishStatusOnline(online: Boolean) {
|
||||
val cloudSession = session ?: return
|
||||
val identity = deviceIdentity
|
||||
val rcSn = identity.remoteControllerSn
|
||||
if (rcSn.isBlank()) return
|
||||
val subDevices = JSONArray()
|
||||
if (online && identity.aircraftSn.isNotBlank()) {
|
||||
subDevices.put(
|
||||
JSONObject()
|
||||
.put("sn", identity.aircraftSn)
|
||||
.put("domain", DroneDomain)
|
||||
.put("type", Matrice4Type)
|
||||
.put("sub_type", Matrice4SubType)
|
||||
.put("index", "A")
|
||||
.put("thing_version", ThingVersion)
|
||||
)
|
||||
}
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("method", "update_topo")
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put("domain", RcDomain)
|
||||
.put("type", RcPlus2Type)
|
||||
.put("sub_type", RcSubType)
|
||||
.put("thing_version", ThingVersion)
|
||||
.put("access_type", CloudAccessType)
|
||||
.put("sub_devices", subDevices)
|
||||
)
|
||||
publish("sys/product/$rcSn/status", payload, qos = 1)
|
||||
}
|
||||
|
||||
private fun publishLiveCapacityIfReady(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
val rcSn = identity.remoteControllerSn
|
||||
val aircraftSn = identity.aircraftSn
|
||||
if (rcSn.isBlank() || aircraftSn.isBlank()) return
|
||||
val key = "$rcSn/$aircraftSn/${cloudSession.workspaceId}"
|
||||
if (liveCapacityKey == key) return
|
||||
|
||||
val video = JSONObject()
|
||||
.put("video_index", Matrice4LiveVideoIndex)
|
||||
.put("video_type", "normal")
|
||||
.put("switchable_video_types", JSONArray().put("normal"))
|
||||
val camera = JSONObject()
|
||||
.put("available_video_number", 1)
|
||||
.put("coexist_video_number_max", 1)
|
||||
.put("camera_index", Matrice4CameraPayloadIndex)
|
||||
.put("video_list", JSONArray().put(video))
|
||||
val device = JSONObject()
|
||||
.put("sn", aircraftSn)
|
||||
.put("available_video_number", 1)
|
||||
.put("coexist_video_number_max", 1)
|
||||
.put("camera_list", JSONArray().put(camera))
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("gateway", rcSn)
|
||||
.put("method", "livestream_ability_update")
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put(
|
||||
"live_capacity",
|
||||
JSONObject()
|
||||
.put("available_video_number", 1)
|
||||
.put("coexist_video_number_max", 1)
|
||||
.put("device_list", JSONArray().put(device))
|
||||
)
|
||||
)
|
||||
publish("thing/product/$rcSn/state", payload, qos = 1)
|
||||
liveCapacityKey = key
|
||||
Log.d(CloudMqttLogTag, "live capacity published rc=$rcSn aircraft=$aircraftSn")
|
||||
}
|
||||
|
||||
private fun publishOsdIfReady(force: Boolean) {
|
||||
if (!isConnected()) return
|
||||
val identity = deviceIdentity
|
||||
if (!identity.readyForOnline) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (!force && now - lastOsdAt < OSD_MIN_INTERVAL_MS) return
|
||||
lastOsdAt = now
|
||||
publishRcOsd(identity)
|
||||
publishDroneOsd(identity)
|
||||
_state.update { it.copy(lastOsdAt = now) }
|
||||
}
|
||||
|
||||
private fun publishRcOsd(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
Log.d(
|
||||
CloudMqttLogTag,
|
||||
"RcOsdPosition lat=${telemetry.rcLatitude} lon=${telemetry.rcLongitude} valid=${telemetry.rcLocationValid} satellites=${telemetry.rcGpsSatelliteCount}"
|
||||
)
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("gateway", identity.remoteControllerSn)
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put(JsonClassKey, OsdRemoteControlClass)
|
||||
.put("latitude", telemetry.rcLatitude)
|
||||
.put("longitude", telemetry.rcLongitude)
|
||||
.put("height", telemetry.rcAltitude.toFloat())
|
||||
.put("capacity_percent", telemetry.rcBatteryPercent.takeIf { it in 0..100 } ?: 0)
|
||||
.put("is_cloud_control_auth", 1)
|
||||
.put(
|
||||
"wireless_link",
|
||||
JSONObject()
|
||||
.put(JsonClassKey, WirelessLinkClass)
|
||||
.put("dongle_number", 0)
|
||||
.put("link_workmode", 0)
|
||||
.put("sdr_link_state", identity.productLinkConnected)
|
||||
.put("sdr_quality", if (identity.productLinkConnected) 5 else 0)
|
||||
.put("4g_freq_band", 0.0)
|
||||
.put("4g_gnd_quality", 0)
|
||||
.put("4g_link_state", false)
|
||||
.put("4g_quality", 0)
|
||||
.put("4g_uav_quality", 0)
|
||||
)
|
||||
)
|
||||
publish("thing/product/${identity.remoteControllerSn}/osd", payload, qos = 0)
|
||||
}
|
||||
|
||||
private fun publishDroneOsd(identity: CloudDeviceIdentity) {
|
||||
val cloudSession = session ?: return
|
||||
val horizontalSpeed = sqrt(telemetry.speedX * telemetry.speedX + telemetry.speedY * telemetry.speedY)
|
||||
val latitude = telemetry.droneOsdLatitude()
|
||||
val longitude = telemetry.droneOsdLongitude()
|
||||
Log.d(
|
||||
CloudMqttLogTag,
|
||||
"DroneOsdPosition source=${telemetry.droneOsdPositionSource()} lat=$latitude lon=$longitude " +
|
||||
"gpsValid=${telemetry.gpsValid} gps=${telemetry.gpsSatelliteCount} rtkHealthy=${telemetry.rtkHealthy} " +
|
||||
"rtkUsable=${telemetry.rtkFusionDataUsable} solution=${telemetry.rtkPositioningSolution}"
|
||||
)
|
||||
val relativeAltitude = telemetry.altitude.toFloat()
|
||||
val payload = topicRequest(cloudSession)
|
||||
.put("gateway", identity.remoteControllerSn)
|
||||
.put(
|
||||
"data",
|
||||
JSONObject()
|
||||
.put(JsonClassKey, OsdRcDroneClass)
|
||||
.put("attitude_head", telemetry.heading.toFloat())
|
||||
.put("attitude_pitch", telemetry.attitudePitch)
|
||||
.put("attitude_roll", telemetry.attitudeRoll)
|
||||
.put("elevation", relativeAltitude)
|
||||
.put("gear", telemetry.gear)
|
||||
.put("height", relativeAltitude)
|
||||
.put("original_height", relativeAltitude)
|
||||
.put("home_distance", telemetry.homeDistanceMeters())
|
||||
.put("horizontal_speed", horizontalSpeed.toFloat())
|
||||
.put("vertical_speed", telemetry.speedZ.toFloat())
|
||||
.put("latitude", latitude)
|
||||
.put("longitude", longitude)
|
||||
.put("mode_code", telemetry.toModeCode(identity.productConnected))
|
||||
.put("total_flight_distance", telemetry.totalFlightDistance)
|
||||
.put("total_flight_time", telemetry.totalFlightTime.toFloat())
|
||||
.put("total_flight_sorties", telemetry.totalFlightSorties)
|
||||
.put("wind_direction", telemetry.windDirection)
|
||||
.put("wind_speed", telemetry.windSpeed.toFloat())
|
||||
.put("firmware_version", telemetry.firmwareVersion)
|
||||
.put("battery", droneBatteryJson())
|
||||
.put("position_state", dronePositionStateJson())
|
||||
.put("payload", JSONArray().put(rcDronePayloadJson()))
|
||||
.put("storage", storageJson())
|
||||
.put("cameras", JSONArray().put(osdCameraJson()))
|
||||
.put("height_limit", telemetry.heightLimit)
|
||||
.put("distance_limit_status", rcDistanceLimitStatusJson())
|
||||
.put("track_id", "")
|
||||
)
|
||||
publish("thing/product/${identity.aircraftSn}/osd", payload, qos = 0)
|
||||
}
|
||||
|
||||
private fun droneBatteryJson(): JSONObject {
|
||||
val battery = JSONObject()
|
||||
.put(JsonClassKey, DroneBatteryClass)
|
||||
.put("batteries", droneBatteryItemsJson())
|
||||
telemetry.batteryPercent.takeIf { it in 0..100 }?.let {
|
||||
battery.put("capacity_percent", it)
|
||||
}
|
||||
if (telemetry.batteryPercentNeededToLandKnown) {
|
||||
battery.put("landing_power", telemetry.batteryPercentNeededToLand)
|
||||
}
|
||||
if (telemetry.remainingFlightTimeKnown) {
|
||||
battery.put("remain_flight_time", telemetry.remainingFlightTime)
|
||||
}
|
||||
if (telemetry.batteryPercentNeededToGoHomeKnown) {
|
||||
battery.put("return_home_power", telemetry.batteryPercentNeededToGoHome)
|
||||
}
|
||||
return battery
|
||||
}
|
||||
|
||||
private fun droneBatteryItemsJson(): JSONArray {
|
||||
val batteryItem = JSONObject()
|
||||
.put(JsonClassKey, BatteryClass)
|
||||
.put("index", 0)
|
||||
telemetry.batteryFirmwareVersion.takeIf { it.isNotBlank() }?.let {
|
||||
batteryItem.put("firmware_version", it)
|
||||
}
|
||||
if (telemetry.batteryLoopTimesKnown) {
|
||||
batteryItem.put("loop_times", telemetry.batteryLoopTimes)
|
||||
}
|
||||
telemetry.batteryPercent.takeIf { it in 0..100 }?.let {
|
||||
batteryItem.put("capacity_percent", it)
|
||||
}
|
||||
telemetry.batterySerialNumber.takeIf { it.isNotBlank() }?.let {
|
||||
batteryItem.put("sn", it)
|
||||
}
|
||||
if (telemetry.batteryTemperatureKnown) {
|
||||
batteryItem.put("temperature", telemetry.batteryTemperature)
|
||||
}
|
||||
if (telemetry.batteryVoltageKnown) {
|
||||
batteryItem.put("voltage", telemetry.batteryVoltageMv)
|
||||
}
|
||||
if (telemetry.batteryHighVoltageStorageKnown) {
|
||||
batteryItem.put("high_voltage_storage_days", telemetry.batteryHighVoltageStorageDays())
|
||||
}
|
||||
return if (batteryItem.length() > 2) cloudArrayList(batteryItem) else cloudArrayList()
|
||||
}
|
||||
|
||||
private fun dronePositionStateJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, DronePositionStateClass)
|
||||
.put("gps_number", telemetry.gpsSatelliteCount)
|
||||
.put("is_fixed", if (telemetry.hasFixedPosition()) 1 else 0)
|
||||
.put("quality", telemetry.gpsSignalLevel)
|
||||
.put("rtk_number", telemetry.rtkSatelliteCount)
|
||||
|
||||
private fun rcDistanceLimitStatusJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, RcDistanceLimitStatusClass)
|
||||
.put("state", if (telemetry.distanceLimitEnabled) 1 else 0)
|
||||
.put("distance_limit", telemetry.distanceLimit)
|
||||
|
||||
private fun storageJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, StorageClass)
|
||||
.put("total", telemetry.storageTotal.toCloudStorageUnit())
|
||||
.put("used", telemetry.storageUsed().toCloudStorageUnit())
|
||||
|
||||
private fun osdCameraJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, OsdCameraClass)
|
||||
.put("payload_index", Matrice4CameraPayloadIndex)
|
||||
.put("camera_mode", telemetry.cameraModeCode())
|
||||
.put("photo_state", telemetry.photoState)
|
||||
.put("recording_state", telemetry.recordingState)
|
||||
.put("record_time", telemetry.recordingTime)
|
||||
.put("remain_photo_num", telemetry.remainPhotoNum.toLong())
|
||||
.put("remain_record_duration", telemetry.remainRecordDuration)
|
||||
.put("zoom_factor", telemetry.zoomFactor)
|
||||
.put("ir_zoom_factor", telemetry.irZoomFactor)
|
||||
.put("screen_split_enable", false)
|
||||
|
||||
private fun rcDronePayloadJson(): JSONObject =
|
||||
JSONObject()
|
||||
.put(JsonClassKey, RcDronePayloadClass)
|
||||
.put("payload_index", Matrice4CameraPayloadIndex)
|
||||
.put("gimbal_pitch", telemetry.gimbalPitch.toFloat())
|
||||
.put("gimbal_roll", telemetry.gimbalRoll.toFloat())
|
||||
.put("gimbal_yaw", telemetry.gimbalYaw.toFloat())
|
||||
.put("measure_target_altitude", telemetry.laserTargetAltitude.toFloat())
|
||||
.put("measure_target_distance", telemetry.laserDistance.toFloat())
|
||||
.put("measure_target_error_state", telemetry.laserTargetErrorState())
|
||||
.put("measure_target_latitude", telemetry.laserTargetLatitude.toFloat())
|
||||
.put("measure_target_longitude", telemetry.laserTargetLongitude.toFloat())
|
||||
.put("smart_track_point", JSONArray())
|
||||
.put("thermal_global_temperature_max", telemetry.thermalMaxTemperature.toFloat())
|
||||
|
||||
private fun topicRequest(cloudSession: CloudSession): JSONObject =
|
||||
JSONObject()
|
||||
.put("tid", UUID.randomUUID().toString())
|
||||
.put("bid", UUID.randomUUID().toString())
|
||||
.put("timestamp", System.currentTimeMillis())
|
||||
.put("username", cloudSession.payloadUsername)
|
||||
|
||||
private fun publishReply(
|
||||
request: CloudCommandRequest,
|
||||
success: Boolean,
|
||||
message: String
|
||||
) {
|
||||
publishReply(request.topic, request.tid, request.bid, request.method, request.seq, success, message)
|
||||
}
|
||||
|
||||
private fun publishReply(
|
||||
topic: String,
|
||||
tid: String,
|
||||
bid: String,
|
||||
method: String,
|
||||
seq: Long?,
|
||||
success: Boolean,
|
||||
message: String
|
||||
) {
|
||||
val payloadUsername = session?.payloadUsername.orEmpty()
|
||||
val isDrcReply = topic.endsWith("/drc/down")
|
||||
val replyTopic = when {
|
||||
isDrcReply -> topic.removeSuffix("/down") + "/up"
|
||||
topic.endsWith("/services") -> "${topic}_reply"
|
||||
topic.endsWith("/property/set") -> "${topic}_reply"
|
||||
else -> "${topic}_reply"
|
||||
}
|
||||
val data = JSONObject()
|
||||
.put("result", if (success) 0 else -1)
|
||||
.put("message", message)
|
||||
if (isDrcReply && seq != null) {
|
||||
data.put("output", JSONObject().put("seq", seq))
|
||||
}
|
||||
val payload = JSONObject()
|
||||
.put("tid", tid.ifBlank { UUID.randomUUID().toString() })
|
||||
.put("bid", bid.ifBlank { UUID.randomUUID().toString() })
|
||||
.put("timestamp", System.currentTimeMillis())
|
||||
.put("username", payloadUsername)
|
||||
.put("method", method.ifBlank { "reply" })
|
||||
.put("data", data)
|
||||
if (isDrcReply && seq != null) {
|
||||
payload.put("seq", seq)
|
||||
}
|
||||
publish(replyTopic, payload, qos = 1)
|
||||
}
|
||||
|
||||
private fun publish(topic: String, payload: JSONObject, qos: Int) {
|
||||
val mqttClient = client ?: return
|
||||
if (!mqttClient.isConnected) return
|
||||
runCatching {
|
||||
val message = MqttMessage(payload.toString().toByteArray(Charsets.UTF_8)).apply {
|
||||
this.qos = qos
|
||||
isRetained = false
|
||||
}
|
||||
mqttClient.publish(topic, message)
|
||||
Log.d(CloudMqttLogTag, "published topic=$topic payload=$payload")
|
||||
}.onFailure { error ->
|
||||
if (error is MqttException) Log.e(CloudMqttLogTag, "publish failed reason=${error.reasonCode}", error)
|
||||
_state.update { it.copy(lastError = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isConnected(): Boolean = client?.isConnected == true && session != null
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.toModeCode(productConnected: Boolean): Int {
|
||||
if (!productConnected) return 14
|
||||
if (!isFlying && !motorsOn && !landingMode) return 0
|
||||
val flightModeStringCompact = flightModeString.cloudModeCompact()
|
||||
val fcFlightModeCompact = fcFlightMode.cloudModeCompact()
|
||||
val flightModeCompact = flightMode.cloudModeCompact()
|
||||
val compact = listOf(flightModeStringCompact, fcFlightModeCompact, flightModeCompact)
|
||||
.joinToString("|")
|
||||
val keyModeIsManualControl = flightModeCompact.isManualControlCloudMode()
|
||||
val stringModeIsManualControl = flightModeStringCompact.isManualControlCloudMode()
|
||||
return when {
|
||||
compact.contains("TAKEOFFPREPARATION") || compact.contains("TAKEOFFPREPARE") -> 1
|
||||
compact.contains("READYFORTAKEOFF") || compact.contains("TAKEOFFFINISHED") -> 2
|
||||
compact.contains("AUTOTAKEOFF") || compact.contains("TAKEOFFAUTO") -> 4
|
||||
compact.contains("WAYLINE") || compact.contains("ROUTE") || compact.contains("WAYPOINT") -> 5
|
||||
compact.contains("PANORAMA") -> 6
|
||||
compact.contains("ACTIVETRACK") -> 7
|
||||
compact.contains("ADSB") -> 8
|
||||
compact.contains("GOHOME") || compact.contains("RETURN") || compact.contains("RTH") -> 9
|
||||
compact.contains("AUTOLANDING") || compact.contains("LANDINGAUTO") || landingMode -> 10
|
||||
compact.contains("FORCEDLANDING") || compact.contains("LANDINGFORCED") -> 11
|
||||
compact.contains("THREEBLADE") || compact.contains("THREEPROPELLER") -> 12
|
||||
compact.contains("UPDATING") || compact.contains("UPGRADING") -> 13
|
||||
compact.contains("DISCONNECTED") -> 14
|
||||
compact.contains("APAS") -> 15
|
||||
compact.contains("VIRTUALJOYSTICK") -> 16
|
||||
compact.contains("COMMAND") || compact.contains("LIVEFLIGHTCONTROL") -> 17
|
||||
compact.contains("POI") || compact.contains("ORBIT") -> 20
|
||||
compact.contains("FLYTOLIVETARGET") ||
|
||||
compact.contains("GOTARGETPOINT") ||
|
||||
compact.contains("CLICKGO") ||
|
||||
compact.contains("TAPFLY") -> 17
|
||||
keyModeIsManualControl || stringModeIsManualControl -> 3
|
||||
compact.contains("AIRBORNERTKCONVERGENCE") || compact.contains("RTK") -> 18
|
||||
else -> 3
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.cloudModeCompact(): String =
|
||||
uppercase()
|
||||
.replace(" ", "")
|
||||
.replace("_", "")
|
||||
.replace("-", "")
|
||||
|
||||
private fun String.isManualControlCloudMode(): Boolean =
|
||||
this == "MANUAL" ||
|
||||
this == "JOYSTICK" ||
|
||||
this == "PGPS" ||
|
||||
contains("手动飞行") ||
|
||||
contains("空中悬停") ||
|
||||
contains("运动") ||
|
||||
contains("姿态") ||
|
||||
contains("平稳") ||
|
||||
contains("MANUALFLIGHT") ||
|
||||
contains("GPSNORMAL") ||
|
||||
contains("GPSATTI") ||
|
||||
contains("GPSCL") ||
|
||||
contains("GPSHOMELOCK") ||
|
||||
contains("GPSBRAKE") ||
|
||||
contains("GPS") ||
|
||||
contains("ATTI") ||
|
||||
contains("HOVER") ||
|
||||
contains("SPORT") ||
|
||||
contains("TRIPOD") ||
|
||||
contains("CINEMATIC") ||
|
||||
contains("CINE") ||
|
||||
contains("FPV")
|
||||
|
||||
private fun TelemetrySnapshot.droneOsdLatitude(): Double =
|
||||
if (hasUsableRtkPosition()) rtkLatitude else latitude
|
||||
|
||||
private fun TelemetrySnapshot.droneOsdLongitude(): Double =
|
||||
if (hasUsableRtkPosition()) rtkLongitude else longitude
|
||||
|
||||
private fun TelemetrySnapshot.droneOsdPositionSource(): String =
|
||||
if (hasUsableRtkPosition()) "RTK" else "FC"
|
||||
|
||||
private fun TelemetrySnapshot.hasFixedPosition(): Boolean =
|
||||
gpsValid || hasUsableRtkPosition()
|
||||
|
||||
private fun TelemetrySnapshot.hasUsableRtkPosition(): Boolean =
|
||||
rtkLocationValid && (rtkHealthy || rtkFusionDataUsable || rtkPositioningSolution.isSolvedRtkSolution())
|
||||
|
||||
private fun String.isSolvedRtkSolution(): Boolean {
|
||||
val compact = uppercase().replace("_", "")
|
||||
return compact == "SINGLEPOINT" || compact == "FLOAT" || compact == "FIXEDPOINT"
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.homeDistanceMeters(): Float {
|
||||
if (!isValidCoordinate(latitude, longitude) || !isValidCoordinate(homeLatitude, homeLongitude)) return 0f
|
||||
val dLat = Math.toRadians(latitude - homeLatitude)
|
||||
val dLon = Math.toRadians(longitude - homeLongitude)
|
||||
val startLat = Math.toRadians(homeLatitude)
|
||||
val endLat = Math.toRadians(latitude)
|
||||
val a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(startLat) * cos(endLat) * sin(dLon / 2) * sin(dLon / 2)
|
||||
return (EarthRadiusMeters * 2 * atan2(sqrt(a), sqrt(1 - a))).toFloat()
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.storageUsed(): Int =
|
||||
(storageTotal - storageRemain).coerceAtLeast(0)
|
||||
|
||||
private fun Int.toCloudStorageUnit(): Long {
|
||||
val value = coerceAtLeast(0).toLong()
|
||||
return if (value in 1..999_999) value * 1000L else value
|
||||
}
|
||||
|
||||
private fun TelemetrySnapshot.laserTargetErrorState(): Int =
|
||||
if (laserDistance > 0.0 && isValidCoordinate(laserTargetLatitude, laserTargetLongitude)) 0 else 3
|
||||
|
||||
private fun TelemetrySnapshot.batteryHighVoltageStorageDays(): Int =
|
||||
(batteryHighVoltageStorageSeconds / 86_400L)
|
||||
.coerceIn(0L, Int.MAX_VALUE.toLong())
|
||||
.toInt()
|
||||
|
||||
private fun cloudArrayList(vararg items: Any): JSONArray =
|
||||
JSONArray()
|
||||
.put(JavaArrayListClass)
|
||||
.put(
|
||||
JSONArray().apply {
|
||||
items.forEach { put(it) }
|
||||
}
|
||||
)
|
||||
|
||||
private fun TelemetrySnapshot.cameraModeCode(): Int {
|
||||
val mode = cameraWorkMode.uppercase()
|
||||
return when {
|
||||
mode.contains("RECORD") || mode.contains("VIDEO") -> 1
|
||||
mode.contains("PANO") -> 3
|
||||
mode.contains("INTERVAL") || mode.contains("TIMED") -> 4
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidCoordinate(latitude: Double, longitude: Double): Boolean {
|
||||
if (latitude.isNaN() || longitude.isNaN()) return false
|
||||
if (latitude == 0.0 && longitude == 0.0) return false
|
||||
return latitude in -90.0..90.0 && longitude in -180.0..180.0
|
||||
}
|
||||
|
||||
private fun JSONObject.optLongOrNull(name: String): Long? =
|
||||
if (has(name) && !isNull(name)) optLong(name) else null
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.zklh.dronecontroller.core.diagnostics
|
||||
|
||||
import dji.v5.manager.diagnostic.DeviceHealthManager
|
||||
import dji.v5.manager.diagnostic.DJIDeviceHealthInfo
|
||||
import dji.v5.manager.diagnostic.DJIDeviceHealthInfoChangeListener
|
||||
import dji.v5.manager.diagnostic.WarningLevel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
data class DroneWarningState(
|
||||
val active: Boolean = false,
|
||||
val level: WarningLevel = WarningLevel.NORMAL,
|
||||
val message: String = "无实时告警",
|
||||
val count: Int = 0,
|
||||
val messages: List<String> = emptyList(),
|
||||
val raw: String = ""
|
||||
)
|
||||
|
||||
class DroneWarningRepository {
|
||||
private val _state = MutableStateFlow(DroneWarningState())
|
||||
val state: StateFlow<DroneWarningState> = _state.asStateFlow()
|
||||
|
||||
private val healthInfoChangeListener = DJIDeviceHealthInfoChangeListener {
|
||||
updateWarnings()
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var started = false
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
DeviceHealthManager.getInstance().addDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
||||
updateWarnings()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!started) return
|
||||
started = false
|
||||
DeviceHealthManager.getInstance().removeDJIDeviceHealthInfoChangeListener(healthInfoChangeListener)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_state.value = DroneWarningState()
|
||||
}
|
||||
|
||||
private fun updateWarnings() {
|
||||
val messages = DeviceHealthManager.getInstance().currentDJIDeviceHealthInfos
|
||||
.map { it.toMessage() }
|
||||
.filter { it.text.isNotBlank() }
|
||||
.sortedWith(compareByDescending<WarningMessage> { warningSeverity(it.level) }.thenBy { it.text })
|
||||
|
||||
if (messages.isEmpty()) {
|
||||
_state.value = DroneWarningState()
|
||||
return
|
||||
}
|
||||
|
||||
val top = messages.first()
|
||||
_state.value = DroneWarningState(
|
||||
active = true,
|
||||
level = top.level,
|
||||
message = top.text,
|
||||
count = messages.size,
|
||||
messages = messages.map { it.text },
|
||||
raw = messages.joinToString(separator = "\n") { "${it.level}:${it.code}:${it.text}" }
|
||||
)
|
||||
}
|
||||
|
||||
private fun DJIDeviceHealthInfo.toMessage(): WarningMessage {
|
||||
val description = description()
|
||||
val title = title()
|
||||
val code = informationCode()
|
||||
val text = when {
|
||||
description.isNotBlank() -> description
|
||||
title.isNotBlank() -> title
|
||||
else -> code
|
||||
}
|
||||
return WarningMessage(text = text, level = warningLevel(), code = code)
|
||||
}
|
||||
|
||||
private data class WarningMessage(
|
||||
val text: String,
|
||||
val level: WarningLevel,
|
||||
val code: String
|
||||
)
|
||||
|
||||
private fun warningSeverity(level: WarningLevel): Int =
|
||||
when (level) {
|
||||
WarningLevel.SERIOUS_WARNING -> 5
|
||||
WarningLevel.WARNING -> 4
|
||||
WarningLevel.CAUTION -> 3
|
||||
WarningLevel.NOTICE -> 2
|
||||
WarningLevel.UNKNOWN -> 1
|
||||
WarningLevel.NORMAL -> 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.zklh.dronecontroller.core.flight
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class FlightControlService {
|
||||
suspend fun startTakeoff(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStartTakeoff.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动起飞已开始"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopTakeoff(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStopTakeoff.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("起飞已取消"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startAutoLanding(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStartAutoLanding.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动降落已开始"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopAutoLanding(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStopAutoLanding.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动降落已取消"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun confirmLanding(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyConfirmLanding.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动降落已确认"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startGoHome(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStartGoHome.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("自动返航已开始"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopGoHome(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
FlightControllerKey.KeyStopGoHome.create().action({
|
||||
continuation.resume(DjiCommandResult.ok("返航已取消"))
|
||||
}, { error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.zklh.dronecontroller.core.flight
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.intelligent.IntelligentFlightManager
|
||||
import dji.v5.manager.intelligent.flyto.FlyToParam
|
||||
import dji.v5.manager.intelligent.flyto.FlyToTarget
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
private const val FlyToLogTag = "ZklhFlyTo"
|
||||
|
||||
class FlyToService {
|
||||
suspend fun startFlyTo(
|
||||
latitude: Double,
|
||||
longitude: Double,
|
||||
height: Double,
|
||||
maxSpeed: Int = 14,
|
||||
securityTakeoffHeight: Int = 20,
|
||||
flyToMode: FlyToMode = FlyToMode.SET_HEIGHT
|
||||
): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val manager = IntelligentFlightManager.getInstance().flyToMissionManager
|
||||
val target = FlyToTarget().apply {
|
||||
targetLocation = LocationCoordinate3D(latitude, longitude, height)
|
||||
this.maxSpeed = maxSpeed
|
||||
this.securityTakeoffHeight = securityTakeoffHeight
|
||||
}
|
||||
Log.d(
|
||||
FlyToLogTag,
|
||||
"startFlyTo lat=$latitude lon=$longitude height=$height mode=$flyToMode maxSpeed=$maxSpeed securityTakeoffHeight=$securityTakeoffHeight"
|
||||
)
|
||||
manager.startMission(
|
||||
target,
|
||||
null,
|
||||
callback(continuation, "指点飞行已开始")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopFlyTo(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.stopMission(
|
||||
callback(continuation, "指点飞行已停止")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setFlyToMode(mode: FlyToMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val param = FlyToParam().apply { flyToMode = mode }
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(
|
||||
param,
|
||||
callback(continuation, "指点飞行模式已更新:${mode.name}")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun setFlyToHeight(height: Int): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val param = FlyToParam().apply { this.height = height }
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(
|
||||
param,
|
||||
callback(continuation, "指点飞行高度已更新:${height}m")
|
||||
)
|
||||
}
|
||||
|
||||
private fun callback(
|
||||
continuation: kotlinx.coroutines.CancellableContinuation<DjiCommandResult>,
|
||||
successMessage: String
|
||||
): CommonCallbacks.CompletionCallback =
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.zklh.dronecontroller.core.gimbal
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.sdk.keyvalue.key.GimbalKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||
import dji.sdk.keyvalue.value.gimbal.CtrlInfo
|
||||
import dji.sdk.keyvalue.value.gimbal.GimbalSpeedRotation
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class GimbalControlService(
|
||||
private val gimbalIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||
) {
|
||||
suspend fun rotateBySpeed(
|
||||
pitchSpeed: Double,
|
||||
yawSpeed: Double
|
||||
): DjiCommandResult =
|
||||
runAction(
|
||||
KeyTools.createKey(GimbalKey.KeyRotateBySpeed, gimbalIndex),
|
||||
GimbalSpeedRotation(
|
||||
pitchSpeed.coerceIn(-100.0, 100.0),
|
||||
yawSpeed.coerceIn(-100.0, 100.0),
|
||||
0.0,
|
||||
CtrlInfo()
|
||||
),
|
||||
"云台控制已下发"
|
||||
)
|
||||
|
||||
suspend fun reset(): DjiCommandResult =
|
||||
runAction(
|
||||
KeyTools.createKey(GimbalKey.KeyRestoreFactorySettings, gimbalIndex),
|
||||
EmptyMsg(),
|
||||
"云台复位已下发"
|
||||
)
|
||||
|
||||
private suspend fun <Param, Result> runAction(
|
||||
key: DJIKey.ActionKey<Param, Result>,
|
||||
param: Param,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.action(
|
||||
param,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
package com.zklh.dronecontroller.core.livestream
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamSettings
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamStatus
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamStatusListener
|
||||
import dji.v5.manager.datacenter.livestream.LiveStreamType
|
||||
import dji.v5.manager.datacenter.livestream.LiveVideoBitrateMode
|
||||
import dji.v5.manager.datacenter.livestream.StreamQuality
|
||||
import dji.v5.manager.datacenter.livestream.settings.RtmpSettings
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
private const val LiveStreamLogTag = "ZklhLiveStream"
|
||||
private const val UrlTypeRtmp = 1
|
||||
private const val UrlTypeWhip = 4
|
||||
private const val ZlmRtmpPort = 1935
|
||||
private const val ZlmDefaultApp = "live"
|
||||
private const val DefaultLiveQuality = 3
|
||||
|
||||
data class LiveStreamingState(
|
||||
val streaming: Boolean = false,
|
||||
val busy: Boolean = false,
|
||||
val url: String = "",
|
||||
val urlType: String = "",
|
||||
val videoId: String = "",
|
||||
val quality: Int = DefaultLiveQuality,
|
||||
val fps: Int = 0,
|
||||
val vbps: Int = 0,
|
||||
val resolution: String = "",
|
||||
val message: String = "直播未开启",
|
||||
val lastError: String? = null
|
||||
)
|
||||
|
||||
private data class LiveStartConfig(
|
||||
val url: String,
|
||||
val urlType: Int,
|
||||
val videoId: String,
|
||||
val quality: Int
|
||||
)
|
||||
|
||||
class LiveStreamingService {
|
||||
private val _state = MutableStateFlow(LiveStreamingState())
|
||||
val state: StateFlow<LiveStreamingState> = _state.asStateFlow()
|
||||
|
||||
private var listenerRegistered = false
|
||||
private var lastConfig: LiveStartConfig? = null
|
||||
|
||||
private val statusListener = object : LiveStreamStatusListener {
|
||||
override fun onLiveStreamStatusUpdate(status: LiveStreamStatus?) {
|
||||
val resolution = status?.resolution?.let { "${it.width}x${it.height}" }.orEmpty()
|
||||
_state.update {
|
||||
it.copy(
|
||||
streaming = status?.isStreaming ?: liveStreamManager().isStreaming,
|
||||
fps = status?.fps ?: 0,
|
||||
vbps = status?.vbps ?: 0,
|
||||
resolution = resolution,
|
||||
message = if (status?.isStreaming == true) "直播推流中" else it.message,
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: IDJIError?) {
|
||||
val message = error?.toString() ?: "直播推流异常"
|
||||
Log.e(LiveStreamLogTag, "live stream error=$message")
|
||||
_state.update { it.copy(lastError = message, message = message) }
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
ensureListener()
|
||||
_state.update { it.copy(streaming = liveStreamManager().isStreaming) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
if (listenerRegistered) {
|
||||
runCatching { liveStreamManager().removeLiveStreamStatusListener(statusListener) }
|
||||
listenerRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun toggleManual(): DjiCommandResult {
|
||||
ensureListener()
|
||||
if (liveStreamManager().isStreaming || _state.value.streaming) {
|
||||
return stop()
|
||||
}
|
||||
val config = lastConfig
|
||||
?: return DjiCommandResult.failed("请先从平台下发 RTMP 推流地址,再在遥控器侧开启直播")
|
||||
return startWithConfig(config, manual = true)
|
||||
}
|
||||
|
||||
suspend fun startFromCloud(data: JSONObject): DjiCommandResult {
|
||||
ensureListener()
|
||||
val rawConfig = data.toLiveStartConfig()
|
||||
?: return DjiCommandResult.failed("直播参数无效:需要直播推流参数")
|
||||
val config = rawConfig.toMsdkRtmpConfig()
|
||||
?: return DjiCommandResult.failed("直播参数无效:无法转换为 MSDK 可用的 RTMP 推流地址")
|
||||
if (rawConfig.urlType == UrlTypeWhip || rawConfig.url.contains("whip", ignoreCase = true)) {
|
||||
Log.i(LiveStreamLogTag, "converted WHIP push url to RTMP: ${config.url}")
|
||||
}
|
||||
if (!config.isRtmpPush()) {
|
||||
return DjiCommandResult.failed("当前仅支持 RTMP 推流,收到 url_type=${config.urlType}")
|
||||
}
|
||||
return startWithConfig(config, manual = false)
|
||||
}
|
||||
|
||||
suspend fun stop(): DjiCommandResult {
|
||||
ensureListener()
|
||||
if (!liveStreamManager().isStreaming && !_state.value.streaming) {
|
||||
_state.update { it.copy(streaming = false, busy = false, message = "直播未开启", lastError = null) }
|
||||
return DjiCommandResult.ok("直播未开启")
|
||||
}
|
||||
_state.update { it.copy(busy = true, message = "正在关闭直播", lastError = null) }
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
liveStreamManager().stopStream(object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_state.update { it.copy(streaming = false, busy = false, message = "直播已关闭", lastError = null) }
|
||||
continuation.resume(DjiCommandResult.ok("直播已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
val message = error.toString()
|
||||
_state.update { it.copy(busy = false, lastError = message, message = "关闭直播失败:$message") }
|
||||
continuation.resume(DjiCommandResult.failed("关闭直播失败:$message"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun setQualityFromCloud(data: JSONObject): DjiCommandResult {
|
||||
ensureListener()
|
||||
val quality = data.optIntAny(_state.value.quality, "video_quality", "videoQuality", "quality")
|
||||
val streamQuality = quality.toStreamQuality()
|
||||
liveStreamManager().liveStreamQuality = streamQuality
|
||||
lastConfig = lastConfig?.copy(quality = quality)
|
||||
_state.update { it.copy(quality = quality, message = "直播清晰度已设置:${streamQuality.name}", lastError = null) }
|
||||
return DjiCommandResult.ok("直播清晰度已设置:${streamQuality.name}")
|
||||
}
|
||||
|
||||
private suspend fun startWithConfig(config: LiveStartConfig, manual: Boolean): DjiCommandResult {
|
||||
lastConfig = config
|
||||
val manager = liveStreamManager()
|
||||
if (manager.isStreaming && _state.value.url == config.url) {
|
||||
_state.update { it.copy(streaming = true, busy = false, message = "直播已在推流中", lastError = null) }
|
||||
return DjiCommandResult.ok("直播已在推流中")
|
||||
}
|
||||
if (manager.isStreaming) {
|
||||
val stopResult = stop()
|
||||
if (!stopResult.success) return stopResult
|
||||
}
|
||||
|
||||
val cameraIndex = config.videoId.toCameraIndex()
|
||||
runCatching {
|
||||
cameraStreamManager().enableStream(cameraIndex, true)
|
||||
manager.cameraIndex = cameraIndex
|
||||
manager.liveStreamSettings = LiveStreamSettings.Builder()
|
||||
.setLiveStreamType(LiveStreamType.RTMP)
|
||||
.setRtmpSettings(RtmpSettings.Builder().setUrl(config.url).build())
|
||||
.build()
|
||||
manager.liveStreamQuality = config.quality.toStreamQuality()
|
||||
manager.liveStreamScaleType = ICameraStreamManager.ScaleType.CENTER_CROP
|
||||
manager.liveVideoBitrateMode = LiveVideoBitrateMode.AUTO
|
||||
manager.setLiveAudioEnabled(false)
|
||||
}.onFailure { error ->
|
||||
val message = error.message ?: error.toString()
|
||||
_state.update { it.copy(busy = false, lastError = message, message = "直播配置失败:$message") }
|
||||
return DjiCommandResult.failed("直播配置失败:$message")
|
||||
}
|
||||
|
||||
Log.d(
|
||||
LiveStreamLogTag,
|
||||
"start live stream manual=$manual url=${config.url} videoId=${config.videoId} quality=${config.quality} camera=$cameraIndex"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
busy = true,
|
||||
url = config.url,
|
||||
urlType = "RTMP",
|
||||
videoId = config.videoId,
|
||||
quality = config.quality,
|
||||
message = "正在开启直播",
|
||||
lastError = null
|
||||
)
|
||||
}
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
manager.startStream(object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_state.update { it.copy(streaming = true, busy = false, message = "直播已开启", lastError = null) }
|
||||
continuation.resume(DjiCommandResult.ok("直播已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
val message = error.toString()
|
||||
_state.update { it.copy(streaming = false, busy = false, lastError = message, message = "开启直播失败:$message") }
|
||||
continuation.resume(DjiCommandResult.failed("开启直播失败:$message"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureListener() {
|
||||
if (listenerRegistered) return
|
||||
liveStreamManager().addLiveStreamStatusListener(statusListener)
|
||||
listenerRegistered = true
|
||||
}
|
||||
|
||||
private fun liveStreamManager() = MediaDataCenter.getInstance().liveStreamManager
|
||||
|
||||
private fun cameraStreamManager() = MediaDataCenter.getInstance().cameraStreamManager
|
||||
}
|
||||
|
||||
private fun JSONObject.toLiveStartConfig(): LiveStartConfig? {
|
||||
val url = optObjectString("url", "address")
|
||||
.ifBlank { optStringAny("rtmp_url", "rtmpUrl", "push_url", "pushUrl", "stream_url", "streamUrl") }
|
||||
val urlType = optIntAny(
|
||||
defaultValue = if (url.startsWith("rtmp", ignoreCase = true)) UrlTypeRtmp else -1,
|
||||
"url_type",
|
||||
"urlType",
|
||||
"type"
|
||||
)
|
||||
val normalizedUrlType = when {
|
||||
urlType > -1 -> urlType
|
||||
url.startsWith("rtmp", ignoreCase = true) -> UrlTypeRtmp
|
||||
url.contains("whip", ignoreCase = true) -> UrlTypeWhip
|
||||
else -> -1
|
||||
}
|
||||
if (url.isBlank() && normalizedUrlType != UrlTypeWhip) return null
|
||||
return LiveStartConfig(
|
||||
url = url,
|
||||
urlType = normalizedUrlType,
|
||||
videoId = optValueAsString("video_id", "videoId", "videoID"),
|
||||
quality = optIntAny(DefaultLiveQuality, "video_quality", "videoQuality", "quality")
|
||||
)
|
||||
}
|
||||
|
||||
private fun LiveStartConfig.isRtmpPush(): Boolean =
|
||||
urlType == UrlTypeRtmp || url.startsWith("rtmp", ignoreCase = true)
|
||||
|
||||
private fun LiveStartConfig.toMsdkRtmpConfig(): LiveStartConfig? {
|
||||
if (url.startsWith("rtmp", ignoreCase = true)) {
|
||||
return copy(urlType = UrlTypeRtmp)
|
||||
}
|
||||
if (urlType != UrlTypeWhip && !url.contains("whip", ignoreCase = true)) {
|
||||
return null
|
||||
}
|
||||
val rtmpUrl = url.toZlmRtmpUrl(videoId)
|
||||
return rtmpUrl.takeIf { it.isNotBlank() }?.let { copy(url = it, urlType = UrlTypeRtmp) }
|
||||
}
|
||||
|
||||
private fun String.toZlmRtmpUrl(videoId: String): String {
|
||||
val uri = runCatching { Uri.parse(this) }.getOrNull() ?: return ""
|
||||
val host = uri.host.orEmpty()
|
||||
val app = uri.getQueryParameter("app")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: uri.pathSegments.firstOrNull { it == ZlmDefaultApp }
|
||||
?: ZlmDefaultApp
|
||||
val stream = uri.getQueryParameter("stream")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?: uri.pathSegments.lastOrNull()
|
||||
?.removeSuffix(".live.flv")
|
||||
?.removeSuffix(".live.mp4")
|
||||
?.removeSuffix(".m3u8")
|
||||
?.takeIf { it.isNotBlank() && it != app }
|
||||
?: videoId.toZlmStreamName()
|
||||
if (host.isBlank() || stream.isBlank()) return ""
|
||||
return "rtmp://$host:$ZlmRtmpPort/$app/$stream"
|
||||
}
|
||||
|
||||
private fun String.toZlmStreamName(): String =
|
||||
trim()
|
||||
.replace("/", "_")
|
||||
|
||||
private fun JSONObject.optStringAny(vararg names: String): String {
|
||||
for (name in names) {
|
||||
val value = optValueAsString(name)
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optObjectString(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (!has(name) || isNull(name)) continue
|
||||
val raw = opt(name)
|
||||
when (raw) {
|
||||
is JSONObject -> {
|
||||
val value = raw.optStringAny("url", "rtmp_url", "rtmpUrl", "push_url", "pushUrl")
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
is JSONArray -> {
|
||||
val value = raw.arrayValueAsString()
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
else -> raw?.toString()?.takeIf { it.isNotBlank() }?.let { return it }
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONObject.optValueAsString(vararg names: String): String {
|
||||
for (name in names) {
|
||||
if (!has(name) || isNull(name)) continue
|
||||
val raw = opt(name)
|
||||
val value = when (raw) {
|
||||
is JSONArray -> raw.arrayValueAsString()
|
||||
is JSONObject -> raw.optStringAny("value", "id", "url")
|
||||
else -> raw?.toString().orEmpty()
|
||||
}
|
||||
if (value.isNotBlank()) return value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun JSONArray.arrayValueAsString(): String {
|
||||
if (length() == 0) return ""
|
||||
for (i in length() - 1 downTo 0) {
|
||||
val value = opt(i)
|
||||
if (value is String && value.isNotBlank()) return value
|
||||
}
|
||||
return optString(length() - 1, "")
|
||||
}
|
||||
|
||||
private fun JSONObject.optIntAny(defaultValue: Int, vararg names: String): Int {
|
||||
for (name in names) {
|
||||
if (!has(name) || isNull(name)) continue
|
||||
val raw = opt(name)
|
||||
val value = when (raw) {
|
||||
is Number -> raw.toInt()
|
||||
is String -> raw.toIntOrNull() ?: raw.liveEnumValueOrNull()
|
||||
is JSONArray -> raw.arrayValueAsString().toIntOrNull() ?: raw.arrayValueAsString().liveEnumValueOrNull()
|
||||
else -> null
|
||||
}
|
||||
if (value != null) return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
private fun String.liveEnumValueOrNull(): Int? =
|
||||
when (trim().uppercase()) {
|
||||
"RTMP" -> UrlTypeRtmp
|
||||
"WHIP" -> UrlTypeWhip
|
||||
"AUTO" -> 0
|
||||
"SMOOTH" -> 1
|
||||
"STANDARD_DEFINITION", "SD" -> 2
|
||||
"HIGH_DEFINITION", "HD" -> 3
|
||||
"ULTRA_HD", "FULL_HD", "FHD" -> 4
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun Int.toStreamQuality(): StreamQuality =
|
||||
when (this) {
|
||||
1, 2 -> StreamQuality.SD
|
||||
3 -> StreamQuality.HD
|
||||
4 -> StreamQuality.FULL_HD
|
||||
100 -> StreamQuality.ORIGINAL
|
||||
else -> StreamQuality.HD
|
||||
}
|
||||
|
||||
private fun String.toCameraIndex(): ComponentIndexType =
|
||||
when {
|
||||
contains("/FPV", ignoreCase = true) -> ComponentIndexType.FPV
|
||||
contains("/right", ignoreCase = true) -> ComponentIndexType.RIGHT
|
||||
else -> ComponentIndexType.LEFT_OR_MAIN
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.zklh.dronecontroller.core.media
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType
|
||||
import dji.sdk.keyvalue.value.common.CameraLensType
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.set
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class CameraControlService(
|
||||
private val cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||
) {
|
||||
suspend fun setVisibleZoomRatio(zoomRatio: Double): DjiCommandResult =
|
||||
setDoubleKey(
|
||||
KeyTools.createCameraKey(CameraKey.KeyCameraZoomRatios, cameraIndex, CameraLensType.CAMERA_LENS_ZOOM),
|
||||
zoomRatio,
|
||||
"可见光变焦已设置:${"%.1f".format(zoomRatio)}x"
|
||||
)
|
||||
|
||||
suspend fun setThermalZoomRatio(zoomRatio: Double): DjiCommandResult =
|
||||
setDoubleKey(
|
||||
KeyTools.createCameraKey(CameraKey.KeyThermalZoomRatios, cameraIndex, CameraLensType.CAMERA_LENS_THERMAL),
|
||||
zoomRatio,
|
||||
"红外变焦已设置:${"%.1f".format(zoomRatio)}x"
|
||||
)
|
||||
|
||||
suspend fun setLens(lens: String): DjiCommandResult {
|
||||
val source = when (lens.normalizeLensName()) {
|
||||
"wide", "normal", "visible", "visible_wide", "camera_lens_wide" -> CameraVideoStreamSourceType.WIDE_CAMERA
|
||||
"zoom", "visible_zoom", "camera_lens_zoom" -> CameraVideoStreamSourceType.ZOOM_CAMERA
|
||||
"ir", "infrared", "thermal", "camera_lens_thermal" -> CameraVideoStreamSourceType.INFRARED_CAMERA
|
||||
else -> return DjiCommandResult.failed("未知镜头类型:$lens")
|
||||
}
|
||||
return setValueKey(
|
||||
KeyTools.createKey(CameraKey.KeyCameraVideoStreamSource, cameraIndex),
|
||||
source,
|
||||
"镜头已切换:$lens"
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setDoubleKey(
|
||||
key: DJIKey<Double>,
|
||||
value: Double,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
setValueKey(key, value, successMessage)
|
||||
|
||||
private suspend fun <T> setValueKey(
|
||||
key: DJIKey<T>,
|
||||
value: T,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.set(
|
||||
value,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.normalizeLensName(): String =
|
||||
trim()
|
||||
.replace("-", "_")
|
||||
.lowercase()
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
package com.zklh.dronecontroller.core.media
|
||||
|
||||
import android.util.Log
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.DJICameraKey
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.sdk.keyvalue.value.camera.CameraFlatMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraWorkMode
|
||||
import dji.sdk.keyvalue.value.camera.GeneratedMediaFileInfo
|
||||
import dji.sdk.keyvalue.value.camera.PhotoPanoramaMode
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.et.set
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.datacenter.media.MediaFile
|
||||
import dji.v5.manager.datacenter.media.MediaFileListDataSource
|
||||
import dji.v5.manager.datacenter.media.PullMediaFileListParam
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
private const val CameraMediaLogTag = "ZklhCameraMedia"
|
||||
|
||||
class CameraMediaService(
|
||||
defaultCameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN
|
||||
) {
|
||||
private var activeCameraIndex: ComponentIndexType = defaultCameraIndex
|
||||
|
||||
suspend fun takePhoto(): DjiCommandResult {
|
||||
var lastFailure: DjiCommandResult? = null
|
||||
Log.d(CameraMediaLogTag, "takePhoto candidates=${candidateCameraIndexes()}")
|
||||
for (index in candidateCameraIndexes()) {
|
||||
val previousMedia = readLatestMediaFile(index)
|
||||
val previousSnapshot = readCachedMediaSnapshot("before photo")
|
||||
val modeResult = setPhotoCaptureMode(index)
|
||||
if (!modeResult.success) {
|
||||
lastFailure = modeResult
|
||||
Log.w(CameraMediaLogTag, "set photo mode failed camera=$index message=${modeResult.message}")
|
||||
continue
|
||||
}
|
||||
delay(700)
|
||||
|
||||
Log.d(CameraMediaLogTag, "start shoot photo camera=$index")
|
||||
val shootResult = runEmptyAction(
|
||||
CameraKey.KeyStartShootPhoto.create(index),
|
||||
"拍照指令已下发,正在等待文件写入"
|
||||
)
|
||||
if (!shootResult.success) {
|
||||
lastFailure = shootResult
|
||||
Log.w(CameraMediaLogTag, "start shoot photo failed camera=$index message=${shootResult.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
val saved = waitForGeneratedMediaFile(
|
||||
cameraIndex = index,
|
||||
previousMedia = previousMedia,
|
||||
previousSnapshot = previousSnapshot,
|
||||
successMessage = "拍照完成,文件已写入飞机存储",
|
||||
timeoutMessage = "拍照指令已下发,但没有检测到新照片文件。请检查飞机 SD 卡/内置存储、相机存储位置和电量告警。"
|
||||
)
|
||||
if (saved.success) {
|
||||
activeCameraIndex = index
|
||||
Log.d(CameraMediaLogTag, "photo saved camera=$index message=${saved.message}")
|
||||
return saved
|
||||
}
|
||||
lastFailure = saved
|
||||
Log.w(CameraMediaLogTag, "photo not saved camera=$index message=${saved.message}")
|
||||
}
|
||||
return lastFailure ?: DjiCommandResult.failed("拍照失败:未找到可用相机")
|
||||
}
|
||||
|
||||
suspend fun startRecord(): DjiCommandResult {
|
||||
var lastFailure: DjiCommandResult? = null
|
||||
for (index in candidateCameraIndexes()) {
|
||||
val modeResult = setVideoCaptureMode(index)
|
||||
if (!modeResult.success) {
|
||||
lastFailure = modeResult
|
||||
Log.w(CameraMediaLogTag, "set video mode failed camera=$index message=${modeResult.message}")
|
||||
continue
|
||||
}
|
||||
delay(900)
|
||||
|
||||
var startResult = runEmptyAction(
|
||||
CameraKey.KeyStartRecord.create(index),
|
||||
"录像已开始"
|
||||
)
|
||||
if (startResult.isRetryableStartRecordError()) {
|
||||
delay(1200)
|
||||
startResult = runEmptyAction(
|
||||
CameraKey.KeyStartRecord.create(index),
|
||||
"录像已开始"
|
||||
)
|
||||
}
|
||||
if (!startResult.success) {
|
||||
lastFailure = startResult
|
||||
Log.w(CameraMediaLogTag, "start record failed camera=$index message=${startResult.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
if (waitForRecordingState(index, recording = true, timeoutMs = 3_000L)) {
|
||||
activeCameraIndex = index
|
||||
Log.d(CameraMediaLogTag, "recording started camera=$index")
|
||||
return startResult
|
||||
}
|
||||
|
||||
lastFailure = DjiCommandResult.failed("录像指令已下发,但没有检测到相机进入录像状态")
|
||||
Log.w(CameraMediaLogTag, "recording state unchanged camera=$index")
|
||||
}
|
||||
return lastFailure ?: DjiCommandResult.failed("录像失败:未找到可用相机")
|
||||
}
|
||||
|
||||
suspend fun stopRecord(): DjiCommandResult {
|
||||
var lastFailure: DjiCommandResult? = null
|
||||
val recordingIndexes = candidateCameraIndexes().filter { isRecording(it) }
|
||||
val indexes = if (recordingIndexes.isNotEmpty()) recordingIndexes else candidateCameraIndexes()
|
||||
for (index in indexes) {
|
||||
val previousMedia = readLatestMediaFile(index)
|
||||
val previousSnapshot = readCachedMediaSnapshot("before stop record")
|
||||
val stopResult = runEmptyAction(
|
||||
CameraKey.KeyStopRecord.create(index),
|
||||
"录像停止指令已下发,正在等待文件写入"
|
||||
)
|
||||
if (!stopResult.success) {
|
||||
lastFailure = stopResult
|
||||
Log.w(CameraMediaLogTag, "stop record failed camera=$index message=${stopResult.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
if (!waitForRecordingState(index, recording = false, timeoutMs = 3_000L)) {
|
||||
lastFailure = DjiCommandResult.failed("停止录像指令已下发,但相机仍处于录像状态")
|
||||
Log.w(CameraMediaLogTag, "recording still true camera=$index")
|
||||
continue
|
||||
}
|
||||
|
||||
val saved = waitForGeneratedMediaFile(
|
||||
cameraIndex = index,
|
||||
previousMedia = previousMedia,
|
||||
previousSnapshot = previousSnapshot,
|
||||
successMessage = "录像已停止,文件已写入飞机存储",
|
||||
timeoutMessage = "录像已停止,但没有检测到新视频文件。请检查飞机 SD 卡/内置存储和相机存储状态。",
|
||||
timeoutMs = 12_000L
|
||||
)
|
||||
if (saved.success) {
|
||||
activeCameraIndex = index
|
||||
Log.d(CameraMediaLogTag, "recording stopped camera=$index message=${saved.message}")
|
||||
return saved
|
||||
}
|
||||
lastFailure = saved
|
||||
Log.w(CameraMediaLogTag, "record file not detected camera=$index message=${saved.message}")
|
||||
}
|
||||
return lastFailure ?: DjiCommandResult.failed("停止录像失败:未找到可用相机")
|
||||
}
|
||||
|
||||
suspend fun setCloudCameraMode(cameraMode: Int): DjiCommandResult =
|
||||
when (cameraMode) {
|
||||
0 -> setPhotoCaptureMode()
|
||||
1 -> setVideoCaptureMode()
|
||||
else -> DjiCommandResult.failed("未知相机模式:$cameraMode")
|
||||
}
|
||||
|
||||
suspend fun setPhotoCaptureMode(): DjiCommandResult =
|
||||
setPhotoCaptureMode(activeCameraIndex)
|
||||
|
||||
private suspend fun setPhotoCaptureMode(index: ComponentIndexType): DjiCommandResult {
|
||||
val cameraModeResult = setCameraMode(index, CameraMode.PHOTO_NORMAL)
|
||||
if (cameraModeResult.success) return cameraModeResult
|
||||
|
||||
val flatModeResult = setCameraFlatMode(index, CameraFlatMode.PHOTO_NORMAL)
|
||||
if (flatModeResult.success) return flatModeResult
|
||||
if (flatModeResult.isUnsupportedCameraFlatMode()) {
|
||||
val workModeResult = setCameraWorkMode(index, CameraWorkMode.SHOOT_PHOTO)
|
||||
return if (workModeResult.success || workModeResult.isUnsupportedCameraWorkMode()) {
|
||||
DjiCommandResult.ok("相机拍照模式已准备")
|
||||
} else {
|
||||
workModeResult
|
||||
}
|
||||
}
|
||||
return flatModeResult
|
||||
}
|
||||
|
||||
suspend fun setVideoCaptureMode(): DjiCommandResult =
|
||||
setVideoCaptureMode(activeCameraIndex)
|
||||
|
||||
private suspend fun setVideoCaptureMode(index: ComponentIndexType): DjiCommandResult {
|
||||
val cameraModeResult = setCameraMode(index, CameraMode.VIDEO_NORMAL)
|
||||
if (cameraModeResult.success) return cameraModeResult
|
||||
|
||||
val flatModeResult = setCameraFlatMode(index, CameraFlatMode.VIDEO_NORMAL)
|
||||
if (flatModeResult.success) return flatModeResult
|
||||
if (flatModeResult.isUnsupportedCameraFlatMode()) {
|
||||
val workModeResult = setCameraWorkMode(index, CameraWorkMode.RECORD_VIDEO)
|
||||
return if (workModeResult.success || workModeResult.isUnsupportedCameraWorkMode()) {
|
||||
DjiCommandResult.ok("相机录像模式已准备")
|
||||
} else {
|
||||
workModeResult
|
||||
}
|
||||
}
|
||||
return flatModeResult
|
||||
}
|
||||
|
||||
suspend fun toggleRecord(): DjiCommandResult =
|
||||
if (isRecording()) stopRecord() else startRecord()
|
||||
|
||||
suspend fun shootPanorama(
|
||||
mode: PhotoPanoramaMode = PhotoPanoramaMode.MODE_SPHERE
|
||||
): DjiCommandResult {
|
||||
val workModeResult = setCameraWorkMode(activeCameraIndex, CameraWorkMode.SHOOT_PHOTO)
|
||||
if (!workModeResult.success && !workModeResult.isUnsupportedCameraWorkMode()) return workModeResult
|
||||
|
||||
val panoramaModeResult = setPhotoPanoramaMode(activeCameraIndex, mode)
|
||||
if (!panoramaModeResult.success && !panoramaModeResult.isUnsupportedCameraPhotoMode()) return panoramaModeResult
|
||||
|
||||
return runEmptyAction(
|
||||
DJICameraKey.KeyStartShootPanoPhoto.create(activeCameraIndex),
|
||||
"全景拍照指令已下发"
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun toggleLaserFillLight(): DjiCommandResult {
|
||||
val next = !isLaserFillLightEnabled()
|
||||
return setLaserFillLightEnabled(next)
|
||||
}
|
||||
|
||||
suspend fun setLaserFillLightEnabled(enabled: Boolean): DjiCommandResult {
|
||||
return setBooleanKey(
|
||||
DJICameraKey.KeyLaserFillLightEnabled.create(activeCameraIndex),
|
||||
enabled,
|
||||
if (enabled) "蓝光/补光已开启" else "蓝光/补光已关闭"
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun toggleLaserMeasure(): DjiCommandResult {
|
||||
val next = !isLaserMeasureEnabled()
|
||||
return setLaserMeasureEnabled(next)
|
||||
}
|
||||
|
||||
suspend fun setLaserMeasureEnabled(enabled: Boolean): DjiCommandResult {
|
||||
return setBooleanKey(
|
||||
DJICameraKey.KeyLaserMeasureEnabled.create(activeCameraIndex),
|
||||
enabled,
|
||||
if (enabled) "激光测距已开启" else "激光测距已关闭"
|
||||
)
|
||||
}
|
||||
|
||||
fun isRecording(): Boolean =
|
||||
isRecording(activeCameraIndex)
|
||||
|
||||
private fun isRecording(index: ComponentIndexType): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyIsRecording.create(index).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun isLaserFillLightEnabled(): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyLaserFillLightEnabled.create(activeCameraIndex).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun isLaserMeasureEnabled(): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyLaserMeasureEnabled.create(activeCameraIndex).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
private fun candidateCameraIndexes(): List<ComponentIndexType> =
|
||||
listOf(
|
||||
activeCameraIndex,
|
||||
ComponentIndexType.LEFT_OR_MAIN,
|
||||
ComponentIndexType.RIGHT,
|
||||
ComponentIndexType.UP,
|
||||
ComponentIndexType.PORT_1,
|
||||
ComponentIndexType.PORT_2,
|
||||
ComponentIndexType.PORT_3,
|
||||
ComponentIndexType.PORT_4
|
||||
).distinct()
|
||||
|
||||
private suspend fun waitForGeneratedMediaFile(
|
||||
cameraIndex: ComponentIndexType,
|
||||
previousMedia: GeneratedMediaFileInfo?,
|
||||
previousSnapshot: MediaSnapshot?,
|
||||
successMessage: String,
|
||||
timeoutMessage: String,
|
||||
timeoutMs: Long = 8_000L
|
||||
): DjiCommandResult {
|
||||
val previousIdentity = previousMedia.mediaIdentity()
|
||||
var sawStoring = false
|
||||
val startAt = System.currentTimeMillis()
|
||||
|
||||
while (System.currentTimeMillis() - startAt < timeoutMs) {
|
||||
val currentMedia = readLatestMediaFile(cameraIndex)
|
||||
val currentIdentity = currentMedia.mediaIdentity()
|
||||
if (currentIdentity.isNotBlank() && currentIdentity != previousIdentity) {
|
||||
return DjiCommandResult.ok("$successMessage:${currentMedia.mediaDisplayName()}")
|
||||
}
|
||||
|
||||
val storing = isCameraStoringFile(cameraIndex)
|
||||
if (storing) sawStoring = true
|
||||
if (sawStoring && !storing && System.currentTimeMillis() - startAt > 800L) {
|
||||
val afterStoreMedia = readLatestMediaFile(cameraIndex)
|
||||
val afterStoreIdentity = afterStoreMedia.mediaIdentity()
|
||||
if (afterStoreIdentity.isNotBlank() && afterStoreIdentity != previousIdentity) {
|
||||
return DjiCommandResult.ok("$successMessage:${afterStoreMedia.mediaDisplayName()}")
|
||||
}
|
||||
val afterStoreSnapshot = fetchMediaSnapshot(cameraIndex, "after store")
|
||||
return if (afterStoreSnapshot.isNewerThan(previousSnapshot)) {
|
||||
DjiCommandResult.ok("$successMessage:${afterStoreSnapshot?.latestDisplayName ?: "飞机存储"}")
|
||||
} else {
|
||||
DjiCommandResult.ok(successMessage)
|
||||
}
|
||||
}
|
||||
|
||||
delay(300)
|
||||
}
|
||||
|
||||
val finalSnapshot = fetchMediaSnapshot(cameraIndex, "after timeout")
|
||||
return if (finalSnapshot.isNewerThan(previousSnapshot)) {
|
||||
DjiCommandResult.ok("$successMessage:${finalSnapshot?.latestDisplayName ?: "飞机存储"}")
|
||||
} else {
|
||||
DjiCommandResult.failed(timeoutMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLatestMediaFile(index: ComponentIndexType): GeneratedMediaFileInfo? =
|
||||
runCatching {
|
||||
DJICameraKey.KeyNewlyGeneratedMediaFile.create(index).get(GeneratedMediaFileInfo())
|
||||
}.getOrNull()
|
||||
|
||||
private fun readCachedMediaSnapshot(reason: String): MediaSnapshot? {
|
||||
val files = runCatching { MediaDataCenter.getInstance().mediaManager.mediaFileListData.data }.getOrNull().orEmpty()
|
||||
val latest = files.latestMediaFile()
|
||||
val snapshot = MediaSnapshot(
|
||||
count = files.size,
|
||||
latestIdentity = latest.mediaIdentity(),
|
||||
latestDisplayName = latest.mediaDisplayName()
|
||||
)
|
||||
Log.d(
|
||||
CameraMediaLogTag,
|
||||
"media cached snapshot reason=$reason count=${snapshot.count} latest=${snapshot.latestDisplayName}"
|
||||
)
|
||||
return snapshot.takeIf { it.latestIdentity.isNotBlank() || it.count > 0 }
|
||||
}
|
||||
|
||||
private suspend fun fetchMediaSnapshot(index: ComponentIndexType, reason: String): MediaSnapshot? {
|
||||
val manager = MediaDataCenter.getInstance().mediaManager
|
||||
val dataSourceResult = runCatching {
|
||||
val mediaSource = MediaFileListDataSource.Builder().setIndexType(index).build()
|
||||
manager.setMediaFileDataSource(mediaSource)
|
||||
}
|
||||
if (dataSourceResult.isFailure) {
|
||||
Log.w(CameraMediaLogTag, "set media data source failed camera=$index reason=$reason", dataSourceResult.exceptionOrNull())
|
||||
return null
|
||||
}
|
||||
|
||||
val enableResult = enableMediaManager()
|
||||
if (!enableResult.success) {
|
||||
Log.w(CameraMediaLogTag, "enable media manager failed camera=$index reason=$reason message=${enableResult.message}")
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
val pullResult = pullMediaFileListFromCamera()
|
||||
if (!pullResult.success) {
|
||||
Log.w(CameraMediaLogTag, "pull media list failed camera=$index reason=$reason message=${pullResult.message}")
|
||||
null
|
||||
} else {
|
||||
delay(350)
|
||||
val files = runCatching { manager.mediaFileListData.data }.getOrNull().orEmpty()
|
||||
val latest = files.latestMediaFile()
|
||||
val snapshot = MediaSnapshot(
|
||||
count = files.size,
|
||||
latestIdentity = latest.mediaIdentity(),
|
||||
latestDisplayName = latest.mediaDisplayName()
|
||||
)
|
||||
Log.d(
|
||||
CameraMediaLogTag,
|
||||
"media snapshot camera=$index reason=$reason count=${snapshot.count} latest=${snapshot.latestDisplayName}"
|
||||
)
|
||||
snapshot
|
||||
}
|
||||
} finally {
|
||||
val disableResult = disableMediaManager()
|
||||
if (!disableResult.success) {
|
||||
Log.w(CameraMediaLogTag, "disable media manager failed camera=$index reason=$reason message=${disableResult.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enableMediaManager(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
MediaDataCenter.getInstance().mediaManager.enable(
|
||||
object : dji.v5.common.callback.CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok("媒体文件管理已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun disableMediaManager(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
MediaDataCenter.getInstance().mediaManager.disable(
|
||||
object : dji.v5.common.callback.CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok("媒体文件管理已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun pullMediaFileListFromCamera(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val param = PullMediaFileListParam.Builder()
|
||||
.mediaFileIndex(-1)
|
||||
.count(-1)
|
||||
.build()
|
||||
MediaDataCenter.getInstance().mediaManager.pullMediaFileListFromCamera(
|
||||
param,
|
||||
object : dji.v5.common.callback.CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok("媒体文件列表已刷新"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun isCameraStoringFile(index: ComponentIndexType): Boolean =
|
||||
runCatching {
|
||||
DJICameraKey.KeyCameraStoringFile.create(index).get(false)
|
||||
}.getOrDefault(false)
|
||||
|
||||
private suspend fun waitForRecordingState(
|
||||
index: ComponentIndexType,
|
||||
recording: Boolean,
|
||||
timeoutMs: Long
|
||||
): Boolean {
|
||||
val startAt = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startAt < timeoutMs) {
|
||||
if (isRecording(index) == recording) return true
|
||||
delay(250)
|
||||
}
|
||||
return isRecording(index) == recording
|
||||
}
|
||||
|
||||
private suspend fun setCameraFlatMode(index: ComponentIndexType, mode: CameraFlatMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyCameraFlatMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("相机平铺模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setCameraMode(index: ComponentIndexType, mode: CameraMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
CameraKey.KeyCameraMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("相机模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setCameraWorkMode(index: ComponentIndexType, mode: CameraWorkMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyCameraWorkMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("相机模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setShootPhotoMode(index: ComponentIndexType, mode: CameraShootPhotoMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyShootPhotoMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("拍照模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setPhotoPanoramaMode(index: ComponentIndexType, mode: PhotoPanoramaMode): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
DJICameraKey.KeyPhotoPanoramaMode.create(index).set(
|
||||
mode,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok("全景模式已切换:${mode.name}"))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun setBooleanKey(
|
||||
key: DJIKey<Boolean>,
|
||||
value: Boolean,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.set(
|
||||
value,
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun runEmptyAction(
|
||||
key: DJIKey.ActionKey<EmptyMsg, EmptyMsg>,
|
||||
successMessage: String
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
key.action(
|
||||
{
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
},
|
||||
{ error: IDJIError ->
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun DjiCommandResult.isUnsupportedCameraWorkMode(): Boolean =
|
||||
!success &&
|
||||
message.contains("UNSUPPORTED_COMMAND", ignoreCase = true) &&
|
||||
message.contains("CameraWorkMode", ignoreCase = true)
|
||||
|
||||
private fun DjiCommandResult.isUnsupportedCameraFlatMode(): Boolean =
|
||||
!success &&
|
||||
message.contains("CameraFlatMode", ignoreCase = true) &&
|
||||
(
|
||||
message.contains("UNSUPPORTED_COMMAND", ignoreCase = true) ||
|
||||
message.contains("COMMAND_NOT_SUPPORT_NOW", ignoreCase = true)
|
||||
)
|
||||
|
||||
private fun DjiCommandResult.isUnsupportedCameraPhotoMode(): Boolean =
|
||||
!success &&
|
||||
message.contains("UNSUPPORTED_COMMAND", ignoreCase = true) &&
|
||||
(
|
||||
message.contains("ShootPhotoMode", ignoreCase = true) ||
|
||||
message.contains("PhotoPanoramaMode", ignoreCase = true)
|
||||
)
|
||||
|
||||
private fun DjiCommandResult.isRetryableStartRecordError(): Boolean =
|
||||
!success &&
|
||||
message.contains("StartRecord", ignoreCase = true) &&
|
||||
message.contains("-7", ignoreCase = true)
|
||||
|
||||
private fun GeneratedMediaFileInfo?.mediaIdentity(): String {
|
||||
if (this == null) return ""
|
||||
val identityParts = listOfNotNull(
|
||||
getType()?.name,
|
||||
getIndex()?.toString(),
|
||||
getDir_no()?.toString(),
|
||||
getFile_no()?.toString(),
|
||||
getFileSize()?.toString(),
|
||||
getVideo_time_ms()?.toString(),
|
||||
getCreateTime()?.toString()
|
||||
)
|
||||
return identityParts.joinToString("|")
|
||||
}
|
||||
|
||||
private fun GeneratedMediaFileInfo?.mediaDisplayName(): String {
|
||||
if (this == null) return "飞机存储"
|
||||
val type = getType()?.name ?: "MEDIA"
|
||||
val dirNo = getDir_no()
|
||||
val fileNo = getFile_no()
|
||||
return if (dirNo != null && fileNo != null) {
|
||||
"$type D$dirNo-F$fileNo"
|
||||
} else {
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
private data class MediaSnapshot(
|
||||
val count: Int,
|
||||
val latestIdentity: String,
|
||||
val latestDisplayName: String
|
||||
)
|
||||
|
||||
private fun MediaSnapshot?.isNewerThan(previous: MediaSnapshot?): Boolean {
|
||||
if (this == null) return false
|
||||
if (previous == null) return latestIdentity.isNotBlank()
|
||||
return count > previous.count ||
|
||||
(latestIdentity.isNotBlank() && latestIdentity != previous.latestIdentity)
|
||||
}
|
||||
|
||||
private fun List<MediaFile>.latestMediaFile(): MediaFile? =
|
||||
maxByOrNull { it.fileIndex }
|
||||
|
||||
private fun MediaFile?.mediaIdentity(): String {
|
||||
if (this == null) return ""
|
||||
return "${fileIndex}|${fileName.orEmpty()}"
|
||||
}
|
||||
|
||||
private fun MediaFile?.mediaDisplayName(): String {
|
||||
if (this == null) return "飞机存储"
|
||||
return fileName?.takeIf { it.isNotBlank() } ?: "Media#$fileIndex"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import java.io.File
|
||||
|
||||
object MissionFileStore {
|
||||
fun copyToMissionCache(context: Context, uri: Uri): File {
|
||||
val fileName = queryDisplayName(context, uri)
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let(::safeFileName)
|
||||
?: "mission-${System.currentTimeMillis()}.kmz"
|
||||
|
||||
val outDir = File(context.cacheDir, "missions").apply { mkdirs() }
|
||||
val outFile = File(outDir, fileName)
|
||||
context.contentResolver.openInputStream(uri).use { input ->
|
||||
requireNotNull(input) { "无法打开航线文件" }
|
||||
outFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return outFile
|
||||
}
|
||||
|
||||
private fun queryDisplayName(context: Context, uri: Uri): String? {
|
||||
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex >= 0 && cursor.moveToFirst()) cursor.getString(nameIndex) else null
|
||||
}
|
||||
}
|
||||
|
||||
private fun safeFileName(value: String): String =
|
||||
value.replace(Regex("[^A-Za-z0-9._-]"), "_")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
data class MissionPlan(
|
||||
val name: String,
|
||||
val waypoints: List<MissionWaypoint>,
|
||||
val globalSpeed: Double = 5.0,
|
||||
val takeoffHeight: Double = 20.0,
|
||||
val globalHeight: Double = 100.0,
|
||||
val finishAction: MissionFinishAction = MissionFinishAction.GoHome,
|
||||
val lostAction: MissionLostAction = MissionLostAction.GoBack
|
||||
)
|
||||
|
||||
data class MissionWaypoint(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val altitude: Double,
|
||||
val speed: Double? = null,
|
||||
val heading: Double? = null,
|
||||
val gimbalPitch: Double = -30.0,
|
||||
val actions: List<MissionAction> = emptyList()
|
||||
)
|
||||
|
||||
sealed interface MissionAction {
|
||||
data object TakePhoto : MissionAction
|
||||
data object StartRecord : MissionAction
|
||||
data object StopRecord : MissionAction
|
||||
data class Hover(val seconds: Int) : MissionAction
|
||||
data class GimbalPitch(val degrees: Double) : MissionAction
|
||||
data class CameraZoom(val factor: Int) : MissionAction
|
||||
}
|
||||
|
||||
enum class MissionFinishAction {
|
||||
NoAction,
|
||||
GoHome,
|
||||
AutoLand
|
||||
}
|
||||
|
||||
enum class MissionLostAction {
|
||||
GoBack,
|
||||
Landing,
|
||||
Hover
|
||||
}
|
||||
|
||||
data class MissionUploadUpdate(
|
||||
val progress: Double = 0.0,
|
||||
val completed: Boolean = false,
|
||||
val message: String = "",
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.waypoint3.WaypointMissionManager
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
class WaypointMissionService {
|
||||
fun uploadKmzFile(
|
||||
kmzPath: String,
|
||||
onUpdate: (MissionUploadUpdate) -> Unit
|
||||
) {
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
onUpdate(MissionUploadUpdate(error = SafetyInterlock.LOCKED_MESSAGE, message = SafetyInterlock.LOCKED_MESSAGE))
|
||||
return
|
||||
}
|
||||
WaypointMissionManager.getInstance().pushKMZFileToAircraft(
|
||||
kmzPath,
|
||||
object : CommonCallbacks.CompletionCallbackWithProgress<Double> {
|
||||
override fun onProgressUpdate(progress: Double) {
|
||||
onUpdate(MissionUploadUpdate(progress = progress, message = "航线上传中"))
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
onUpdate(
|
||||
MissionUploadUpdate(
|
||||
progress = 1.0,
|
||||
completed = true,
|
||||
message = "航线上传完成"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
onUpdate(MissionUploadUpdate(error = error.toString(), message = "航线上传失败"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun uploadKmzFile(kmzPath: String): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
var resumed = false
|
||||
uploadKmzFile(kmzPath) { update ->
|
||||
if (resumed) {
|
||||
return@uploadKmzFile
|
||||
}
|
||||
when {
|
||||
update.completed -> {
|
||||
resumed = true
|
||||
continuation.resume(DjiCommandResult.ok("航线上传完成"))
|
||||
}
|
||||
!update.error.isNullOrBlank() -> {
|
||||
resumed = true
|
||||
continuation.resume(DjiCommandResult.failed(update.error.orEmpty()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun startMission(
|
||||
missionId: String,
|
||||
waylineIds: List<Int> = listOf(0)
|
||||
): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().startMission(
|
||||
missionId,
|
||||
waylineIds,
|
||||
callback(continuation, "航线任务已启动")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun pauseMission(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().pauseMission(
|
||||
callback(continuation, "航线任务已暂停")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resumeMission(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().resumeMission(
|
||||
callback(continuation, "航线任务已继续")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopMission(missionId: String): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
WaypointMissionManager.getInstance().stopMission(
|
||||
missionId,
|
||||
callback(continuation, "航线任务已停止")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun missionIdFromPath(path: String): String = File(path).nameWithoutExtension
|
||||
|
||||
private fun callback(
|
||||
continuation: kotlinx.coroutines.CancellableContinuation<DjiCommandResult>,
|
||||
successMessage: String
|
||||
): CommonCallbacks.CompletionCallback {
|
||||
return object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
continuation.resume(DjiCommandResult.ok(successMessage))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.zklh.dronecontroller.core.mission
|
||||
|
||||
import com.dji.wpmzsdk.common.data.Template
|
||||
import com.dji.wpmzsdk.manager.WPMZManager
|
||||
import dji.sdk.wpmz.value.mission.ActionAircraftHoverParam
|
||||
import dji.sdk.wpmz.value.mission.ActionGimbalRotateParam
|
||||
import dji.sdk.wpmz.value.mission.ActionStartRecordParam
|
||||
import dji.sdk.wpmz.value.mission.ActionStopRecordParam
|
||||
import dji.sdk.wpmz.value.mission.ActionTakePhotoParam
|
||||
import dji.sdk.wpmz.value.mission.ActionZoomParam
|
||||
import dji.sdk.wpmz.value.mission.CameraLensType
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionGroup
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionNodeList
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionTreeNode
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionTrigger
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionTriggerType
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionType
|
||||
import dji.sdk.wpmz.value.mission.WaylineActionsRelationType
|
||||
import dji.sdk.wpmz.value.mission.WaylineAltitudeMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineCoordinateMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineCoordinateParam
|
||||
import dji.sdk.wpmz.value.mission.WaylineDroneInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylineExitOnRCLostAction
|
||||
import dji.sdk.wpmz.value.mission.WaylineExitOnRCLostBehavior
|
||||
import dji.sdk.wpmz.value.mission.WaylineFinishedAction
|
||||
import dji.sdk.wpmz.value.mission.WaylineFlyToWaylineMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineGimbalActuatorRotateMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineLocationCoordinate2D
|
||||
import dji.sdk.wpmz.value.mission.WaylineLocationCoordinate3D
|
||||
import dji.sdk.wpmz.value.mission.WaylineMission
|
||||
import dji.sdk.wpmz.value.mission.WaylineMissionConfig
|
||||
import dji.sdk.wpmz.value.mission.WaylinePayloadInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylinePayloadParam
|
||||
import dji.sdk.wpmz.value.mission.WaylinePositioningType
|
||||
import dji.sdk.wpmz.value.mission.WaylineTemplateWaypointInfo
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypoint
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointPitchMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointTurnMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointYawMode
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointYawParam
|
||||
import dji.sdk.wpmz.value.mission.WaylineWaypointYawPathMode
|
||||
import java.io.File
|
||||
|
||||
class WpmlKmzBuilder {
|
||||
fun generateKmz(plan: MissionPlan, outputFile: File): File {
|
||||
require(plan.waypoints.size >= 2) { "航线至少需要 2 个航点" }
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
val waylineMission = WaylineMission().apply {
|
||||
createTime = System.currentTimeMillis().toDouble()
|
||||
updateTime = System.currentTimeMillis().toDouble()
|
||||
}
|
||||
|
||||
WPMZManager.getInstance().generateKMZFile(
|
||||
outputFile.absolutePath,
|
||||
waylineMission,
|
||||
createMissionConfig(plan),
|
||||
createTemplate(plan)
|
||||
)
|
||||
return outputFile
|
||||
}
|
||||
|
||||
private fun createMissionConfig(plan: MissionPlan): WaylineMissionConfig {
|
||||
return WaylineMissionConfig().apply {
|
||||
flyToWaylineMode = WaylineFlyToWaylineMode.SAFELY
|
||||
finishAction = plan.finishAction.toDji()
|
||||
droneInfo = WaylineDroneInfo()
|
||||
securityTakeOffHeight = plan.takeoffHeight
|
||||
isSecurityTakeOffHeightSet = true
|
||||
exitOnRCLostBehavior = WaylineExitOnRCLostBehavior.EXCUTE_RC_LOST_ACTION
|
||||
exitOnRCLostType = plan.lostAction.toDji()
|
||||
globalTransitionalSpeed = plan.globalSpeed
|
||||
payloadInfo = ArrayList<WaylinePayloadInfo>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTemplate(plan: MissionPlan): Template {
|
||||
return Template().apply {
|
||||
waypointInfo = createWaypointInfo(plan)
|
||||
coordinateParam = createCoordinateParam()
|
||||
useGlobalTransitionalSpeed = true
|
||||
autoFlightSpeed = plan.globalSpeed
|
||||
payloadParam = ArrayList<WaylinePayloadParam>()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCoordinateParam(): WaylineCoordinateParam {
|
||||
return WaylineCoordinateParam().apply {
|
||||
coordinateMode = WaylineCoordinateMode.WGS84
|
||||
positioningType = WaylinePositioningType.GPS
|
||||
isWaylinePositioningTypeSet = true
|
||||
altitudeMode = WaylineAltitudeMode.RELATIVE_TO_START_POINT
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWaypointInfo(plan: MissionPlan): WaylineTemplateWaypointInfo {
|
||||
val waypoints = plan.waypoints.mapIndexed { index, point ->
|
||||
createWaypoint(index, point, plan)
|
||||
}
|
||||
val poi = plan.waypoints.first()
|
||||
|
||||
return WaylineTemplateWaypointInfo().apply {
|
||||
this.waypoints = waypoints
|
||||
actionGroups = createActionGroups(plan)
|
||||
globalFlightHeight = plan.globalHeight
|
||||
isGlobalFlightHeightSet = true
|
||||
globalTurnMode = WaylineWaypointTurnMode.TO_POINT_AND_STOP_WITH_DISCONTINUITY_CURVATURE
|
||||
useStraightLine = true
|
||||
isTemplateGlobalTurnModeSet = true
|
||||
globalYawParam = WaylineWaypointYawParam().apply {
|
||||
yawMode = WaylineWaypointYawMode.FOLLOW_WAYLINE
|
||||
poiLocation = WaylineLocationCoordinate3D(
|
||||
poi.latitude,
|
||||
poi.longitude,
|
||||
poi.altitude
|
||||
)
|
||||
}
|
||||
isTemplateGlobalYawParamSet = true
|
||||
pitchMode = WaylineWaypointPitchMode.USE_POINT_SETTING
|
||||
}
|
||||
}
|
||||
|
||||
private fun createWaypoint(
|
||||
index: Int,
|
||||
point: MissionWaypoint,
|
||||
plan: MissionPlan
|
||||
): WaylineWaypoint {
|
||||
return WaylineWaypoint().apply {
|
||||
waypointIndex = index
|
||||
location = WaylineLocationCoordinate2D(point.latitude, point.longitude)
|
||||
height = point.altitude
|
||||
ellipsoidHeight = point.altitude
|
||||
speed = point.speed ?: plan.globalSpeed
|
||||
useGlobalTurnParam = true
|
||||
gimbalPitchAngle = point.gimbalPitch
|
||||
yawParam = WaylineWaypointYawParam().apply {
|
||||
enableYawAngle = point.heading != null
|
||||
yawAngle = point.heading ?: 0.0
|
||||
yawMode = if (point.heading == null) {
|
||||
WaylineWaypointYawMode.FOLLOW_WAYLINE
|
||||
} else {
|
||||
WaylineWaypointYawMode.SMOOTH_TRANSITION
|
||||
}
|
||||
yawPathMode = WaylineWaypointYawPathMode.FOLLOW_BAD_ARC
|
||||
poiLocation = WaylineLocationCoordinate3D(
|
||||
point.latitude,
|
||||
point.longitude,
|
||||
point.altitude
|
||||
)
|
||||
}
|
||||
useGlobalYawParam = false
|
||||
isWaylineWaypointYawParamSet = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun createActionGroups(plan: MissionPlan): List<WaylineActionGroup> {
|
||||
val groups = ArrayList<WaylineActionGroup>()
|
||||
plan.waypoints.forEachIndexed { index, point ->
|
||||
val actionInfos = point.actions.mapNotNull { it.toDji() }
|
||||
if (actionInfos.isEmpty()) return@forEachIndexed
|
||||
|
||||
groups.add(WaylineActionGroup().apply {
|
||||
trigger = WaylineActionTrigger().apply {
|
||||
triggerType = WaylineActionTriggerType.REACH_POINT
|
||||
}
|
||||
groupId = groups.size
|
||||
startIndex = index
|
||||
endIndex = index
|
||||
actions = actionInfos
|
||||
nodeLists = createActionNodeLists(actionInfos.size)
|
||||
})
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
private fun createActionNodeLists(actionCount: Int): List<WaylineActionNodeList> {
|
||||
val root = WaylineActionNodeList().apply {
|
||||
nodes = listOf(WaylineActionTreeNode().apply {
|
||||
nodeType = WaylineActionsRelationType.SEQUENCE
|
||||
childrenNum = actionCount
|
||||
})
|
||||
}
|
||||
val children = WaylineActionNodeList().apply {
|
||||
nodes = (0 until actionCount).map { index ->
|
||||
WaylineActionTreeNode().apply {
|
||||
nodeType = WaylineActionsRelationType.LEAF
|
||||
actionIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
return listOf(root, children)
|
||||
}
|
||||
|
||||
private fun MissionAction.toDji(): WaylineActionInfo {
|
||||
return when (this) {
|
||||
MissionAction.TakePhoto -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.TAKE_PHOTO
|
||||
takePhotoParam = ActionTakePhotoParam(
|
||||
0,
|
||||
true,
|
||||
ArrayList<CameraLensType>(),
|
||||
"zklh"
|
||||
)
|
||||
}
|
||||
|
||||
MissionAction.StartRecord -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.START_RECORD
|
||||
startRecordParam = ActionStartRecordParam(
|
||||
0,
|
||||
true,
|
||||
ArrayList<CameraLensType>(),
|
||||
"zklh"
|
||||
)
|
||||
}
|
||||
|
||||
MissionAction.StopRecord -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.STOP_RECORD
|
||||
stopRecordParam = ActionStopRecordParam().apply {
|
||||
payloadPositionIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
is MissionAction.Hover -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.HOVER
|
||||
aircraftHoverParam = ActionAircraftHoverParam().apply {
|
||||
hoverTime = seconds.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
is MissionAction.GimbalPitch -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.GIMBAL_ROTATE
|
||||
gimbalRotateParam = ActionGimbalRotateParam().apply {
|
||||
enablePitch = true
|
||||
pitch = degrees
|
||||
rotateMode = WaylineGimbalActuatorRotateMode.ABSOLUTE_ANGLE
|
||||
payloadPositionIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
is MissionAction.CameraZoom -> WaylineActionInfo().apply {
|
||||
actionType = WaylineActionType.ZOOM
|
||||
zoomParam = ActionZoomParam().apply {
|
||||
payloadPositionIndex = 0
|
||||
focalFactor = factor.toDouble()
|
||||
isUseFocalFactor = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MissionFinishAction.toDji(): WaylineFinishedAction {
|
||||
return when (this) {
|
||||
MissionFinishAction.NoAction -> WaylineFinishedAction.NO_ACTION
|
||||
MissionFinishAction.GoHome -> WaylineFinishedAction.GO_HOME
|
||||
MissionFinishAction.AutoLand -> WaylineFinishedAction.AUTO_LAND
|
||||
}
|
||||
}
|
||||
|
||||
private fun MissionLostAction.toDji(): WaylineExitOnRCLostAction {
|
||||
return when (this) {
|
||||
MissionLostAction.GoBack -> WaylineExitOnRCLostAction.GO_BACK
|
||||
MissionLostAction.Landing -> WaylineExitOnRCLostAction.LANDING
|
||||
MissionLostAction.Hover -> WaylineExitOnRCLostAction.HOVER
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.zklh.dronecontroller.core.msdk
|
||||
|
||||
import dji.sdk.keyvalue.key.ComponentType
|
||||
import dji.v5.manager.capability.CapabilityManager
|
||||
|
||||
class CapabilityService {
|
||||
fun isProductSupported(productType: String): Boolean =
|
||||
CapabilityManager.getInstance().isProductSupported(productType)
|
||||
|
||||
fun isKeySupported(productType: String, keyName: String): Boolean =
|
||||
CapabilityManager.getInstance().isKeySupported(productType, keyName)
|
||||
|
||||
fun isComponentKeySupported(
|
||||
productType: String,
|
||||
componentType: ComponentType,
|
||||
keyName: String
|
||||
): Boolean =
|
||||
CapabilityManager.getInstance().isKeySupported(
|
||||
productType,
|
||||
"",
|
||||
componentType,
|
||||
keyName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.zklh.dronecontroller.core.msdk
|
||||
|
||||
import dji.v5.common.error.IDJIError
|
||||
|
||||
data class DjiCommandResult(
|
||||
val success: Boolean,
|
||||
val message: String
|
||||
) {
|
||||
companion object {
|
||||
fun ok(message: String): DjiCommandResult = DjiCommandResult(true, message)
|
||||
|
||||
fun failed(error: IDJIError): DjiCommandResult =
|
||||
DjiCommandResult(false, error.toString())
|
||||
|
||||
fun failed(message: String): DjiCommandResult =
|
||||
DjiCommandResult(false, message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
package com.zklh.dronecontroller.core.msdk
|
||||
|
||||
import android.app.Application
|
||||
import dji.sampleV5.aircraft.BuildConfig
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlightMode
|
||||
import dji.sdk.keyvalue.value.remotecontroller.RcMutilDeviceState
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.SDKManager
|
||||
import dji.v5.network.DJINetworkManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class DroneSdkState(
|
||||
val initEvent: String = "Idle",
|
||||
val initProgress: Int = 0,
|
||||
val isInitialized: Boolean = false,
|
||||
val isRegistered: Boolean = false,
|
||||
val registerError: String? = null,
|
||||
val productConnected: Boolean = false,
|
||||
val productLinkConnected: Boolean = false,
|
||||
val aircraftConnectionDetail: String = "未检测",
|
||||
val aircraftSerialNumber: String = "",
|
||||
val remoteControllerSerialNumber: String = "",
|
||||
val productId: Int? = null,
|
||||
val productChangedId: Int? = null,
|
||||
val networkAvailable: Boolean = false,
|
||||
val databaseProgress: String = ""
|
||||
)
|
||||
|
||||
object DroneSdkManager {
|
||||
private const val API_KEY_PLACEHOLDER = "Please add your DJI MSDK app key here."
|
||||
|
||||
private val _state = MutableStateFlow(DroneSdkState())
|
||||
val state: StateFlow<DroneSdkState> = _state.asStateFlow()
|
||||
|
||||
@Volatile
|
||||
private var initStarted = false
|
||||
@Volatile
|
||||
private var productConnectionListenerStarted = false
|
||||
|
||||
private val productConnectionListenerOwner = Any()
|
||||
|
||||
private data class AircraftConnectionProbe(
|
||||
val connected: Boolean,
|
||||
val detail: String
|
||||
)
|
||||
|
||||
fun reportNativeHelperError(error: Throwable) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "MSDK Helper 初始化失败",
|
||||
registerError = error.message ?: error.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onOfficialInitProcess(eventName: String, totalProcess: Int, initialized: Boolean) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = eventName,
|
||||
initProgress = totalProcess,
|
||||
isInitialized = initialized
|
||||
)
|
||||
}
|
||||
if (initialized) {
|
||||
syncOfficialRegistrationState()
|
||||
}
|
||||
}
|
||||
|
||||
fun onOfficialRegisterState(success: Boolean, errorMessage: String?) {
|
||||
if (success) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_SUCCESS",
|
||||
isInitialized = true,
|
||||
isRegistered = true,
|
||||
registerError = null
|
||||
)
|
||||
}
|
||||
startProductConnectionMonitor()
|
||||
refreshProductConnection()
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_FAILURE",
|
||||
isRegistered = false,
|
||||
registerError = errorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onOfficialProductConnection(connected: Boolean, productId: Int) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = if (connected) "PRODUCT_CONNECT" else "PRODUCT_DISCONNECT",
|
||||
productLinkConnected = connected,
|
||||
productId = productId
|
||||
)
|
||||
}
|
||||
refreshProductConnection()
|
||||
}
|
||||
|
||||
fun onOfficialDatabaseProgress(current: Long, total: Long) {
|
||||
_state.update { it.copy(databaseProgress = "$current/$total") }
|
||||
}
|
||||
|
||||
fun initMobileSdk(application: Application) {
|
||||
if (initStarted) return
|
||||
initStarted = true
|
||||
|
||||
if (BuildConfig.AIRCRAFT_API_KEY.isBlank() ||
|
||||
BuildConfig.AIRCRAFT_API_KEY == API_KEY_PLACEHOLDER
|
||||
) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "DJI App Key 未配置",
|
||||
registerError = "请先在 gradle.properties 填写 AIRCRAFT_API_KEY"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "等待官方 MSDK 注册",
|
||||
isInitialized = true
|
||||
)
|
||||
}
|
||||
syncOfficialRegistrationState()
|
||||
|
||||
DJINetworkManager.getInstance().addNetworkStatusListener { isAvailable ->
|
||||
_state.update { it.copy(networkAvailable = isAvailable) }
|
||||
if (
|
||||
isAvailable &&
|
||||
_state.value.isInitialized &&
|
||||
!SDKManager.getInstance().isRegistered
|
||||
) {
|
||||
SDKManager.getInstance().registerApp()
|
||||
} else {
|
||||
syncOfficialRegistrationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun retryRegister() {
|
||||
syncOfficialRegistrationState()
|
||||
if (!_state.value.isInitialized) return
|
||||
runCatching {
|
||||
SDKManager.getInstance().registerApp()
|
||||
refreshProductConnection()
|
||||
}.onFailure { error ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_RETRY_FAILED",
|
||||
registerError = error.message ?: error.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshProductConnection() {
|
||||
syncOfficialRegistrationState()
|
||||
if (!_state.value.isRegistered) {
|
||||
updateConnectionState(
|
||||
productLinkConnected = false,
|
||||
aircraftConnected = false,
|
||||
detail = "SDK 未注册"
|
||||
)
|
||||
return
|
||||
}
|
||||
val productLinkConnected = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(ProductKey.KeyConnection),
|
||||
false
|
||||
)
|
||||
}.getOrElse { error ->
|
||||
_state.update {
|
||||
it.copy(registerError = error.message ?: error.toString())
|
||||
}
|
||||
false
|
||||
}
|
||||
val aircraftProbe = readAircraftConnectionProbe()
|
||||
updateConnectionState(
|
||||
productLinkConnected = productLinkConnected == true,
|
||||
aircraftConnected = aircraftProbe.connected,
|
||||
detail = aircraftProbe.detail
|
||||
)
|
||||
}
|
||||
|
||||
private fun startProductConnectionMonitor() {
|
||||
if (productConnectionListenerStarted) return
|
||||
productConnectionListenerStarted = true
|
||||
runCatching {
|
||||
KeyManager.getInstance().listen(
|
||||
KeyTools.createKey(ProductKey.KeyConnection),
|
||||
productConnectionListenerOwner,
|
||||
true,
|
||||
object : CommonCallbacks.KeyListener<Boolean> {
|
||||
override fun onValueChange(oldValue: Boolean?, newValue: Boolean?) {
|
||||
val aircraftProbe = readAircraftConnectionProbe()
|
||||
updateConnectionState(
|
||||
productLinkConnected = newValue == true,
|
||||
aircraftConnected = aircraftProbe.connected,
|
||||
detail = aircraftProbe.detail
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
refreshProductConnection()
|
||||
}.onFailure { error ->
|
||||
productConnectionListenerStarted = false
|
||||
_state.update {
|
||||
it.copy(registerError = error.message ?: error.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncOfficialRegistrationState() {
|
||||
if (SDKManager.getInstance().isRegistered) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
initEvent = "REGISTER_SUCCESS",
|
||||
isInitialized = true,
|
||||
isRegistered = true,
|
||||
registerError = null
|
||||
)
|
||||
}
|
||||
startProductConnectionMonitor()
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAircraftConnectionProbe(): AircraftConnectionProbe {
|
||||
val rcAircraftState = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(RemoteControllerKey.KeyRcMultiDeviceAircraftState)
|
||||
)
|
||||
}.getOrNull()
|
||||
|
||||
if (rcAircraftState != null) {
|
||||
val mainStates = listOf(
|
||||
rcAircraftState.sdrConnectState,
|
||||
rcAircraftState.lteConnectState
|
||||
)
|
||||
val aircraftStates = rcAircraftState.aircraftStates.orEmpty().flatMap {
|
||||
listOf(it.sdrConnectState, it.lteConnectState)
|
||||
}
|
||||
val connectedByRc = (mainStates + aircraftStates).any(::isAircraftLinkActive)
|
||||
val detail = "RC链路 sdr=${rcAircraftState.sdrConnectState ?: "UNKNOWN"} lte=${rcAircraftState.lteConnectState ?: "UNKNOWN"}"
|
||||
if (!connectedByRc) {
|
||||
return AircraftConnectionProbe(false, detail)
|
||||
}
|
||||
return AircraftConnectionProbe(true, detail)
|
||||
}
|
||||
|
||||
val signals = mutableListOf<String>()
|
||||
val serialNumber = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeySerialNumber),
|
||||
""
|
||||
)
|
||||
}.getOrDefault("")
|
||||
if (serialNumber.isNotBlank()) signals += "飞控SN"
|
||||
|
||||
val flightMode = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeyFlightMode),
|
||||
FlightMode.UNKNOWN
|
||||
)
|
||||
}.getOrDefault(FlightMode.UNKNOWN)
|
||||
if (flightMode != FlightMode.UNKNOWN) signals += "模式:$flightMode"
|
||||
|
||||
val batteryPercent = runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeyBatteryPowerPercent),
|
||||
-1
|
||||
)
|
||||
}.getOrDefault(-1)
|
||||
if (batteryPercent in 0..100) signals += "电量:$batteryPercent%"
|
||||
|
||||
val connected = signals.size >= 2
|
||||
return AircraftConnectionProbe(
|
||||
connected = connected,
|
||||
detail = if (signals.isEmpty()) "未读到飞控实时信号" else signals.joinToString(" / ")
|
||||
)
|
||||
}
|
||||
|
||||
private fun isAircraftLinkActive(state: RcMutilDeviceState?): Boolean =
|
||||
state == RcMutilDeviceState.CONNECTED || state == RcMutilDeviceState.USING
|
||||
|
||||
private fun updateConnectionState(
|
||||
productLinkConnected: Boolean,
|
||||
aircraftConnected: Boolean,
|
||||
detail: String
|
||||
) {
|
||||
_state.update { current ->
|
||||
val event = when {
|
||||
current.productConnected == aircraftConnected &&
|
||||
current.productLinkConnected == productLinkConnected -> current.initEvent
|
||||
aircraftConnected -> "AIRCRAFT_CONNECTION_ON"
|
||||
productLinkConnected -> "PRODUCT_LINK_ON_AIRCRAFT_OFF"
|
||||
else -> "AIRCRAFT_CONNECTION_OFF"
|
||||
}
|
||||
current.copy(
|
||||
initEvent = event,
|
||||
productConnected = aircraftConnected,
|
||||
productLinkConnected = productLinkConnected,
|
||||
aircraftConnectionDetail = detail,
|
||||
aircraftSerialNumber = if (aircraftConnected) readAircraftSerialNumber() else "",
|
||||
remoteControllerSerialNumber = if (productLinkConnected) readRemoteControllerSerialNumber() else ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readAircraftSerialNumber(): String =
|
||||
runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(FlightControllerKey.KeySerialNumber),
|
||||
""
|
||||
)
|
||||
}.getOrDefault("").orEmpty()
|
||||
|
||||
private fun readRemoteControllerSerialNumber(): String =
|
||||
runCatching {
|
||||
KeyManager.getInstance().getValue(
|
||||
KeyTools.createKey(RemoteControllerKey.KeySerialNumber),
|
||||
""
|
||||
)
|
||||
}.getOrDefault("").orEmpty()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.zklh.dronecontroller.core.safety
|
||||
|
||||
object SafetyInterlock {
|
||||
const val FLIGHT_COMMANDS_ENABLED: Boolean = true
|
||||
const val LOCKED_MESSAGE: String = "飞控指令未启用"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.zklh.dronecontroller.core.simulator
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.simulator.InitializationSettings
|
||||
import dji.v5.manager.aircraft.simulator.SimulatorManager
|
||||
import dji.v5.manager.aircraft.simulator.SimulatorState
|
||||
import dji.v5.manager.aircraft.simulator.SimulatorStatusListener
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
data class SimulatorStatus(
|
||||
val enabled: Boolean = false,
|
||||
val motorsOn: Boolean = false,
|
||||
val flying: Boolean = false,
|
||||
val positionX: Float = 0f,
|
||||
val positionY: Float = 0f,
|
||||
val positionZ: Float = 0f,
|
||||
val yaw: Float = 0f,
|
||||
val message: String = "模拟器未开启"
|
||||
)
|
||||
|
||||
class SimulatorService {
|
||||
private val _status = MutableStateFlow(SimulatorStatus())
|
||||
val status: StateFlow<SimulatorStatus> = _status.asStateFlow()
|
||||
|
||||
private val listener = object : SimulatorStatusListener {
|
||||
override fun onUpdate(state: SimulatorState) {
|
||||
_status.update {
|
||||
it.copy(
|
||||
enabled = SimulatorManager.getInstance().isSimulatorEnabled,
|
||||
motorsOn = state.areMotorsOn(),
|
||||
flying = state.isFlying,
|
||||
positionX = state.positionX,
|
||||
positionY = state.positionY,
|
||||
positionZ = state.positionZ,
|
||||
yaw = state.yaw,
|
||||
message = "模拟器=${if (SimulatorManager.getInstance().isSimulatorEnabled) "开启" else "关闭"} 飞行=${if (state.isFlying) "是" else "否"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
SimulatorManager.getInstance().addSimulatorStateListener(listener)
|
||||
_status.update {
|
||||
it.copy(
|
||||
enabled = SimulatorManager.getInstance().isSimulatorEnabled,
|
||||
message = if (SimulatorManager.getInstance().isSimulatorEnabled) "模拟器已开启" else "模拟器未开启"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun enable(
|
||||
latitude: Double = DEFAULT_LATITUDE,
|
||||
longitude: Double = DEFAULT_LONGITUDE,
|
||||
satelliteCount: Int = 12
|
||||
): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val settings = InitializationSettings.createInstance(
|
||||
LocationCoordinate2D(latitude, longitude),
|
||||
satelliteCount
|
||||
)
|
||||
SimulatorManager.getInstance().enableSimulator(
|
||||
settings,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update {
|
||||
it.copy(enabled = true, message = "模拟器已开启")
|
||||
}
|
||||
continuation.resume(DjiCommandResult.ok("模拟器已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun disable(): DjiCommandResult =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
SimulatorManager.getInstance().disableSimulator(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update {
|
||||
it.copy(enabled = false, flying = false, motorsOn = false, message = "模拟器已关闭")
|
||||
}
|
||||
continuation.resume(DjiCommandResult.ok("模拟器已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
SimulatorManager.getInstance().removeSimulatorStateListener(listener)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DEFAULT_LATITUDE = 31.2304
|
||||
const val DEFAULT_LONGITUDE = 121.4737
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
package com.zklh.dronecontroller.core.telemetry
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.location.LocationListener
|
||||
import android.location.LocationManager
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import dji.rtk.CoordinateSystem
|
||||
import dji.sdk.keyvalue.key.BatteryKey
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.DJICameraKey
|
||||
import dji.sdk.keyvalue.key.DJIGimbalKey
|
||||
import dji.sdk.keyvalue.key.DJIKey
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.key.KeyTools
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.sdk.keyvalue.key.RtkMobileStationKey
|
||||
import dji.sdk.keyvalue.value.camera.CameraWorkMode
|
||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||
import dji.sdk.keyvalue.value.camera.LaserMeasureInformation
|
||||
import dji.sdk.keyvalue.value.camera.PhotoState
|
||||
import dji.sdk.keyvalue.value.camera.RecordingState
|
||||
import dji.sdk.keyvalue.value.common.Attitude
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.sdk.keyvalue.value.common.Velocity3D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FCFlightMode
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlightMode
|
||||
import dji.sdk.keyvalue.value.flightcontroller.GPSSignalLevel
|
||||
import dji.sdk.keyvalue.value.flightcontroller.HeightAboveSeaLevelMsg
|
||||
import dji.sdk.keyvalue.value.flightcontroller.RemoteControllerFlightMode
|
||||
import dji.sdk.keyvalue.value.flightcontroller.WindDirection
|
||||
import dji.sdk.keyvalue.value.remotecontroller.BatteryInfo
|
||||
import dji.sdk.keyvalue.value.remotecontroller.RcGPSInfo
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKServiceState
|
||||
import dji.sdk.keyvalue.value.rtkmobilestation.RTKLocation
|
||||
import dji.sdk.keyvalue.value.rtkmobilestation.RTKSatelliteInfo
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.aircraft.rtk.RTKCenter
|
||||
import dji.v5.manager.aircraft.rtk.RTKLocationInfo
|
||||
import dji.v5.manager.aircraft.rtk.RTKLocationInfoListener
|
||||
import dji.v5.manager.aircraft.rtk.RTKSystemState
|
||||
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
|
||||
import dji.v5.manager.aircraft.rtk.network.INetworkServiceInfoListener
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
private const val TelemetryLogTag = "ZklhTelemetry"
|
||||
private const val RTK_START_RETRY_INTERVAL_MS = 20_000L
|
||||
|
||||
data class TelemetrySnapshot(
|
||||
val latitude: Double = 0.0,
|
||||
val longitude: Double = 0.0,
|
||||
val locationValid: Boolean = false,
|
||||
val homeLatitude: Double = 0.0,
|
||||
val homeLongitude: Double = 0.0,
|
||||
val homeLocationValid: Boolean = false,
|
||||
val rcLatitude: Double = 0.0,
|
||||
val rcLongitude: Double = 0.0,
|
||||
val rcAltitude: Double = 0.0,
|
||||
val rcLocationValid: Boolean = false,
|
||||
val rcGpsSatelliteCount: Int = 0,
|
||||
val rcGpsAccuracy: Double = 0.0,
|
||||
val rtkLatitude: Double = 0.0,
|
||||
val rtkLongitude: Double = 0.0,
|
||||
val rtkAltitude: Double = 0.0,
|
||||
val rtkLocationValid: Boolean = false,
|
||||
val rtkEnabled: Boolean = false,
|
||||
val rtkHealthy: Boolean = false,
|
||||
val rtkWorking: Boolean = false,
|
||||
val rtkBeingUsed: Boolean = false,
|
||||
val rtkFusionDataUsable: Boolean = false,
|
||||
val rtkSatelliteCount: Int = 0,
|
||||
val rtkPositioningSolution: String = "",
|
||||
val altitude: Double = 0.0,
|
||||
val elevation: Double = 0.0,
|
||||
val heading: Double = 0.0,
|
||||
val attitudePitch: Double = 0.0,
|
||||
val attitudeRoll: Double = 0.0,
|
||||
val speedX: Double = 0.0,
|
||||
val speedY: Double = 0.0,
|
||||
val speedZ: Double = 0.0,
|
||||
val flightMode: String = "UNKNOWN",
|
||||
val flightModeString: String = "",
|
||||
val fcFlightMode: String = "UNKNOWN",
|
||||
val isFlying: Boolean = false,
|
||||
val motorsOn: Boolean = false,
|
||||
val landingMode: Boolean = false,
|
||||
val landingConfirmationNeeded: Boolean = false,
|
||||
val simulatorStarted: Boolean = false,
|
||||
val firmwareVersion: String = "",
|
||||
val gear: Int = 1,
|
||||
val gpsSatelliteCount: Int = 0,
|
||||
val gpsSignalLevel: Int = 0,
|
||||
val gpsValid: Boolean = false,
|
||||
val heightLimit: Int = 500,
|
||||
val distanceLimit: Int = 5000,
|
||||
val distanceLimitEnabled: Boolean = true,
|
||||
val totalFlightDistance: Double = 0.0,
|
||||
val totalFlightTime: Double = 0.0,
|
||||
val totalFlightSorties: Int = 0,
|
||||
val windDirection: Int = 0,
|
||||
val windSpeed: Int = 0,
|
||||
val remainingFlightTime: Int = 0,
|
||||
val batteryPercentNeededToLand: Int = 7,
|
||||
val batteryPercentNeededToLandKnown: Boolean = false,
|
||||
val batteryPercentNeededToGoHome: Int = 14,
|
||||
val batteryPercentNeededToGoHomeKnown: Boolean = false,
|
||||
val batteryPercent: Int = -1,
|
||||
val batteryVoltageMv: Int = 0,
|
||||
val batteryVoltageKnown: Boolean = false,
|
||||
val batteryFirmwareVersion: String = "",
|
||||
val batterySerialNumber: String = "",
|
||||
val batteryTemperature: Double = 0.0,
|
||||
val batteryTemperatureKnown: Boolean = false,
|
||||
val batteryLoopTimes: Int = 0,
|
||||
val batteryLoopTimesKnown: Boolean = false,
|
||||
val batteryHighVoltageStorageSeconds: Long = 0L,
|
||||
val batteryHighVoltageStorageKnown: Boolean = false,
|
||||
val remainingFlightTimeKnown: Boolean = false,
|
||||
val rcBatteryPercent: Int = -1,
|
||||
val cameraWorkMode: String = "",
|
||||
val photoState: Int = 0,
|
||||
val recordingState: Int = 0,
|
||||
val recordingTime: Int = 0,
|
||||
val remainPhotoNum: Int = 0,
|
||||
val remainRecordDuration: Int = 0,
|
||||
val storageTotal: Int = 0,
|
||||
val storageRemain: Int = 0,
|
||||
val zoomFactor: Double = 1.0,
|
||||
val irZoomFactor: Double = 1.0,
|
||||
val gimbalPitch: Double = 0.0,
|
||||
val gimbalRoll: Double = 0.0,
|
||||
val gimbalYaw: Double = 0.0,
|
||||
val laserDistance: Double = 0.0,
|
||||
val laserTargetLatitude: Double = 0.0,
|
||||
val laserTargetLongitude: Double = 0.0,
|
||||
val laserTargetAltitude: Double = 0.0,
|
||||
val thermalMaxTemperature: Double = 0.0,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
class TelemetryRepository(
|
||||
context: Context? = null
|
||||
) {
|
||||
private val listenHolder = Any()
|
||||
private val appContext = context?.applicationContext
|
||||
private val locationManager = appContext?.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
|
||||
private val androidLocationListener = LocationListener { location -> updateRcLocationFromAndroid(location) }
|
||||
private val rtkCenter = RTKCenter.getInstance()
|
||||
private val rtkLocationInfoListener = RTKLocationInfoListener { info -> updateRtkLocationInfo(info) }
|
||||
private val rtkSystemStateListener = RTKSystemStateListener { state -> updateRtkSystemState(state) }
|
||||
private val rtkNetworkServiceInfoListener = object : INetworkServiceInfoListener {
|
||||
override fun onServiceStateUpdate(state: RTKServiceState?) {
|
||||
Log.d(TelemetryLogTag, "RTKNetworkServiceState=$state")
|
||||
when (state) {
|
||||
RTKServiceState.TRANSMITTING,
|
||||
RTKServiceState.RTCM_NORMAL,
|
||||
-> {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = true
|
||||
}
|
||||
RTKServiceState.DISABLED,
|
||||
RTKServiceState.UNKNOWN,
|
||||
RTKServiceState.RTCM_AUTH_FAILED,
|
||||
RTKServiceState.NETWORK_NOT_REACHABLE,
|
||||
RTKServiceState.SERVER_NOT_REACHABLE,
|
||||
RTKServiceState.ACCOUNT_EXPIRED,
|
||||
-> {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = false
|
||||
ensureRtkServiceStarted(lastRtkSystemState)
|
||||
}
|
||||
else -> {
|
||||
// READY/CONNECTING/PROCESSING are transitional states; keep the current flags.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onErrorCodeUpdate(error: IDJIError?) {
|
||||
Log.w(TelemetryLogTag, "RTKNetworkServiceError=$error")
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = false
|
||||
ensureRtkServiceStarted(lastRtkSystemState)
|
||||
}
|
||||
}
|
||||
private val _state = MutableStateFlow(TelemetrySnapshot())
|
||||
val state: StateFlow<TelemetrySnapshot> = _state.asStateFlow()
|
||||
|
||||
private var started = false
|
||||
private var rtkStartInProgress = false
|
||||
private var rtkServiceStarted = false
|
||||
private var rtkEnableInProgress = false
|
||||
private var rtkSourceSetInProgress = false
|
||||
private var lastRtkStartAtMs = 0L
|
||||
private var lastRtkSystemState: RTKSystemState? = null
|
||||
|
||||
fun start() {
|
||||
if (started) return
|
||||
started = true
|
||||
Log.d(TelemetryLogTag, "start telemetry listeners")
|
||||
listenSafely("KeyAircraftLocation", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation)) { location: LocationCoordinate2D? ->
|
||||
location ?: return@listenSafely
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyAircraftLocation lat=${location.latitude} lon=${location.longitude}"
|
||||
)
|
||||
updateAircraftLocation(location.latitude, location.longitude)
|
||||
}
|
||||
listenSafely("KeyAircraftLocation3D", KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D)) { location: LocationCoordinate3D? ->
|
||||
location ?: return@listenSafely
|
||||
val altitude = location.altitude ?: _state.value.altitude
|
||||
val latitude = location.latitude ?: 0.0
|
||||
val longitude = location.longitude ?: 0.0
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyAircraftLocation3D lat=$latitude lon=$longitude altitude=$altitude valid=$valid"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
locationValid = valid,
|
||||
altitude = altitude,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyAltitude", KeyTools.createKey(FlightControllerKey.KeyAltitude)) { altitude: Double? ->
|
||||
_state.update { it.copy(altitude = altitude ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyHeightAboveSeaLevel", KeyTools.createKey(FlightControllerKey.KeyHeightAboveSeaLevel)) { height: HeightAboveSeaLevelMsg? ->
|
||||
_state.update { it.copy(elevation = height?.height ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyCompassHeading", KeyTools.createKey(FlightControllerKey.KeyCompassHeading)) { heading: Double? ->
|
||||
_state.update { it.copy(heading = heading ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftAttitude", KeyTools.createKey(FlightControllerKey.KeyAircraftAttitude)) { attitude: Attitude? ->
|
||||
attitude ?: return@listenSafely
|
||||
_state.update {
|
||||
it.copy(
|
||||
attitudePitch = attitude.pitch ?: 0.0,
|
||||
attitudeRoll = attitude.roll ?: 0.0,
|
||||
heading = attitude.yaw ?: it.heading,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyAircraftVelocity", KeyTools.createKey(FlightControllerKey.KeyAircraftVelocity)) { velocity: Velocity3D? ->
|
||||
velocity ?: return@listenSafely
|
||||
_state.update {
|
||||
it.copy(speedX = velocity.x, speedY = velocity.y, speedZ = velocity.z, error = null)
|
||||
}
|
||||
}
|
||||
listenSafely("ProductKeyFirmwareVersion", KeyTools.createKey(ProductKey.KeyFirmwareVersion)) { firmware: String? ->
|
||||
val value = firmware.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update { it.copy(firmwareVersion = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyFirmwareVersion", KeyTools.createKey(FlightControllerKey.KeyFirmwareVersion)) { firmware: String? ->
|
||||
val value = firmware.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update {
|
||||
if (it.firmwareVersion.isBlank()) {
|
||||
it.copy(firmwareVersion = value, error = null)
|
||||
} else {
|
||||
it.copy(error = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRemoteControllerFlightMode", KeyTools.createKey(FlightControllerKey.KeyRemoteControllerFlightMode)) { mode: RemoteControllerFlightMode? ->
|
||||
_state.update { it.copy(gear = mode?.value() ?: it.gear, error = null) }
|
||||
}
|
||||
listenSafely("KeyGPSSatelliteCount", KeyTools.createKey(FlightControllerKey.KeyGPSSatelliteCount)) { count: Int? ->
|
||||
val value = count ?: 0
|
||||
Log.d(TelemetryLogTag, "KeyGPSSatelliteCount=$value")
|
||||
_state.update { it.copy(gpsSatelliteCount = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyGPSSignalLevel", KeyTools.createKey(FlightControllerKey.KeyGPSSignalLevel)) { level: GPSSignalLevel? ->
|
||||
val value = level?.value() ?: 0
|
||||
Log.d(TelemetryLogTag, "KeyGPSSignalLevel=$level value=$value")
|
||||
_state.update { it.copy(gpsSignalLevel = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyGPSIsValid", KeyTools.createKey(FlightControllerKey.KeyGPSIsValid)) { valid: Boolean? ->
|
||||
Log.d(TelemetryLogTag, "KeyGPSIsValid=$valid")
|
||||
_state.update { it.copy(gpsValid = valid == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyHomeLocation", KeyTools.createKey(FlightControllerKey.KeyHomeLocation)) { location: LocationCoordinate2D? ->
|
||||
location ?: return@listenSafely
|
||||
val latitude = location.latitude
|
||||
val longitude = location.longitude
|
||||
_state.update {
|
||||
if (isValidCoordinate(latitude, longitude)) {
|
||||
it.copy(
|
||||
homeLatitude = latitude,
|
||||
homeLongitude = longitude,
|
||||
homeLocationValid = true,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
it.copy(homeLocationValid = false, error = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyFlightMode", KeyTools.createKey(FlightControllerKey.KeyFlightMode)) { mode: FlightMode? ->
|
||||
_state.update { it.copy(flightMode = mode?.name ?: "UNKNOWN", error = null) }
|
||||
}
|
||||
listenSafely("KeyFlightModeString", KeyTools.createKey(FlightControllerKey.KeyFlightModeString)) { modeString: String? ->
|
||||
val value = modeString.orEmpty()
|
||||
Log.d(TelemetryLogTag, "KeyFlightModeString=$value")
|
||||
_state.update { it.copy(flightModeString = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyFCFlightMode", KeyTools.createKey(FlightControllerKey.KeyFCFlightMode)) { mode: FCFlightMode? ->
|
||||
val value = mode?.name ?: "UNKNOWN"
|
||||
Log.d(TelemetryLogTag, "KeyFCFlightMode=$value")
|
||||
_state.update { it.copy(fcFlightMode = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsFlying", KeyTools.createKey(FlightControllerKey.KeyIsFlying)) { flying: Boolean? ->
|
||||
_state.update { it.copy(isFlying = flying == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyAreMotorsOn", KeyTools.createKey(FlightControllerKey.KeyAreMotorsOn)) { motorsOn: Boolean? ->
|
||||
_state.update { it.copy(motorsOn = motorsOn == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsInLandingMode", KeyTools.createKey(FlightControllerKey.KeyIsInLandingMode)) { landingMode: Boolean? ->
|
||||
val value = landingMode == true
|
||||
Log.d(TelemetryLogTag, "KeyIsInLandingMode=$value")
|
||||
_state.update { it.copy(landingMode = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsLandingConfirmationNeeded", KeyTools.createKey(FlightControllerKey.KeyIsLandingConfirmationNeeded)) { needed: Boolean? ->
|
||||
val value = needed == true
|
||||
Log.d(TelemetryLogTag, "KeyIsLandingConfirmationNeeded=$value")
|
||||
_state.update { it.copy(landingConfirmationNeeded = value, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsSimulatorStarted", KeyTools.createKey(FlightControllerKey.KeyIsSimulatorStarted)) { started: Boolean? ->
|
||||
_state.update { it.copy(simulatorStarted = started == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyHeightLimit", KeyTools.createKey(FlightControllerKey.KeyHeightLimit)) { limit: Int? ->
|
||||
_state.update { it.copy(heightLimit = limit ?: it.heightLimit, error = null) }
|
||||
}
|
||||
listenSafely("KeyDistanceLimit", KeyTools.createKey(FlightControllerKey.KeyDistanceLimit)) { limit: Int? ->
|
||||
_state.update { it.copy(distanceLimit = limit ?: it.distanceLimit, error = null) }
|
||||
}
|
||||
listenSafely("KeyDistanceLimitEnabled", KeyTools.createKey(FlightControllerKey.KeyDistanceLimitEnabled)) { enabled: Boolean? ->
|
||||
_state.update { it.copy(distanceLimitEnabled = enabled == true, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftTotalFlightDistance", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightDistance)) { distance: Double? ->
|
||||
_state.update { it.copy(totalFlightDistance = distance ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftTotalFlightDuration", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightDuration)) { duration: Double? ->
|
||||
_state.update { it.copy(totalFlightTime = duration ?: 0.0, error = null) }
|
||||
}
|
||||
listenSafely("KeyAircraftTotalFlightTimes", KeyTools.createKey(FlightControllerKey.KeyAircraftTotalFlightTimes)) { times: Int? ->
|
||||
_state.update { it.copy(totalFlightSorties = times ?: 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyRemainingFlightTime", KeyTools.createKey(FlightControllerKey.KeyRemainingFlightTime)) { seconds: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
remainingFlightTime = seconds ?: 0,
|
||||
remainingFlightTimeKnown = seconds != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyBatteryPercentNeededToLand", KeyTools.createKey(FlightControllerKey.KeyBatteryPercentNeededToLand)) { percent: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryPercentNeededToLand = percent ?: it.batteryPercentNeededToLand,
|
||||
batteryPercentNeededToLandKnown = percent != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyBatteryPercentNeededToGoHome", KeyTools.createKey(FlightControllerKey.KeyBatteryPercentNeededToGoHome)) { percent: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryPercentNeededToGoHome = percent ?: it.batteryPercentNeededToGoHome,
|
||||
batteryPercentNeededToGoHomeKnown = percent != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyWindDirection", KeyTools.createKey(FlightControllerKey.KeyWindDirection)) { direction: WindDirection? ->
|
||||
_state.update { it.copy(windDirection = direction?.value() ?: 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyWindSpeed", KeyTools.createKey(FlightControllerKey.KeyWindSpeed)) { speed: Int? ->
|
||||
_state.update { it.copy(windSpeed = speed ?: 0, error = null) }
|
||||
}
|
||||
listenSafely(
|
||||
"KeyChargeRemainingInPercent",
|
||||
KeyTools.createKey(BatteryKey.KeyChargeRemainingInPercent, ComponentIndexType.AGGREGATION)
|
||||
) { percent: Int? ->
|
||||
val value = percent ?: return@listenSafely
|
||||
if (value in 0..100) {
|
||||
_state.update { it.copy(batteryPercent = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyBatteryPowerPercent", KeyTools.createKey(FlightControllerKey.KeyBatteryPowerPercent)) { percent: Int? ->
|
||||
val value = percent ?: return@listenSafely
|
||||
if (value in 0..100 && _state.value.batteryPercent !in 0..100) {
|
||||
_state.update { it.copy(batteryPercent = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyVoltageMain", KeyTools.createKey(BatteryKey.KeyVoltage, ComponentIndexType.LEFT_OR_MAIN)) { voltage: Int? ->
|
||||
val value = voltage ?: return@listenSafely
|
||||
if (value > 0) {
|
||||
_state.update { it.copy(batteryVoltageMv = value, batteryVoltageKnown = true, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely("KeyVoltageRight", KeyTools.createKey(BatteryKey.KeyVoltage, ComponentIndexType.RIGHT)) { voltage: Int? ->
|
||||
val value = voltage ?: return@listenSafely
|
||||
if (value > 0 && _state.value.batteryVoltageMv <= 0) {
|
||||
_state.update { it.copy(batteryVoltageMv = value, batteryVoltageKnown = true, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatteryFirmwareVersion",
|
||||
KeyTools.createKey(BatteryKey.KeyFirmwareVersion, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { version: String? ->
|
||||
val value = version.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update { it.copy(batteryFirmwareVersion = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatterySerialNumber",
|
||||
KeyTools.createKey(BatteryKey.KeySerialNumber, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { serialNumber: String? ->
|
||||
val value = serialNumber.orEmpty()
|
||||
if (value.isNotBlank()) {
|
||||
_state.update { it.copy(batterySerialNumber = value, error = null) }
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatteryTemperature",
|
||||
KeyTools.createKey(BatteryKey.KeyBatteryTemperature, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { temperature: Double? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryTemperature = temperature ?: 0.0,
|
||||
batteryTemperatureKnown = temperature != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyNumberOfDischarges",
|
||||
KeyTools.createKey(BatteryKey.KeyNumberOfDischarges, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { times: Int? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryLoopTimes = times ?: 0,
|
||||
batteryLoopTimesKnown = times != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely(
|
||||
"KeyBatteryHighVoltageStorageTime",
|
||||
KeyTools.createKey(BatteryKey.KeyBatteryHighVoltageStorageTime, ComponentIndexType.LEFT_OR_MAIN)
|
||||
) { seconds: Long? ->
|
||||
_state.update {
|
||||
it.copy(
|
||||
batteryHighVoltageStorageSeconds = seconds ?: 0L,
|
||||
batteryHighVoltageStorageKnown = seconds != null,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenRemoteControllerTelemetry()
|
||||
startAndroidLocationFallback()
|
||||
listenCameraTelemetry()
|
||||
listenGimbalTelemetry()
|
||||
listenRtkTelemetry()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
runCatching { KeyManager.getInstance().cancelListen(listenHolder) }
|
||||
runCatching { locationManager?.removeUpdates(androidLocationListener) }
|
||||
runCatching { rtkCenter.removeRTKLocationInfoListener(rtkLocationInfoListener) }
|
||||
runCatching { rtkCenter.removeRTKSystemStateListener(rtkSystemStateListener) }
|
||||
runCatching { rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||
runCatching { rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||
runCatching { rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener) }
|
||||
started = false
|
||||
}
|
||||
|
||||
private fun <T> listenSafely(name: String, key: DJIKey<T>, onUpdate: (T?) -> Unit) {
|
||||
runCatching {
|
||||
KeyManager.getInstance().listen(key, listenHolder, true) { _, newValue ->
|
||||
onUpdate(newValue)
|
||||
}
|
||||
Log.d(TelemetryLogTag, "registered $name")
|
||||
}.onFailure { error ->
|
||||
_state.update { it.copy(error = error.message ?: error.toString()) }
|
||||
Log.e(TelemetryLogTag, "register $name failed", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenCameraTelemetry(cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
|
||||
listenSafely("KeyCameraWorkMode", KeyTools.createKey(DJICameraKey.KeyCameraWorkMode, cameraIndex)) { mode: CameraWorkMode? ->
|
||||
_state.update { it.copy(cameraWorkMode = mode?.name.orEmpty(), error = null) }
|
||||
}
|
||||
listenSafely("KeyCameraMode", KeyTools.createKey(CameraKey.KeyCameraMode, cameraIndex)) { mode: CameraMode? ->
|
||||
_state.update { it.copy(cameraWorkMode = mode?.name.orEmpty(), error = null) }
|
||||
}
|
||||
listenSafely("KeyShootPhotoState", KeyTools.createKey(DJICameraKey.KeyShootPhotoState, cameraIndex)) { shooting: Boolean? ->
|
||||
if (shooting == true) _state.update { it.copy(photoState = 1, error = null) }
|
||||
}
|
||||
listenSafely("KeyPhotoState", KeyTools.createKey(DJICameraKey.KeyPhotoState, cameraIndex)) { state: PhotoState? ->
|
||||
_state.update { it.copy(photoState = if (state == PhotoState.IN_SHOOTING) 1 else 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyIsRecording", KeyTools.createKey(DJICameraKey.KeyIsRecording, cameraIndex)) { recording: Boolean? ->
|
||||
_state.update { it.copy(recordingState = if (recording == true) 1 else 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyRecordingState", KeyTools.createKey(DJICameraKey.KeyRecordingState, cameraIndex)) { state: RecordingState? ->
|
||||
_state.update { it.copy(recordingState = if (state == RecordingState.RECORDING) 1 else 0, error = null) }
|
||||
}
|
||||
listenSafely("KeyRecordingTime", KeyTools.createKey(DJICameraKey.KeyRecordingTime, cameraIndex)) { seconds: Int? ->
|
||||
_state.update { it.copy(recordingTime = seconds ?: 0, error = null) }
|
||||
}
|
||||
listenSafely("KeySDCardAvailablePhotoCount", KeyTools.createKey(DJICameraKey.KeySDCardAvailablePhotoCount, cameraIndex)) { count: Int? ->
|
||||
_state.update { it.copy(remainPhotoNum = count ?: it.remainPhotoNum, error = null) }
|
||||
}
|
||||
listenSafely("KeySDCardAvailableVideoDuration", KeyTools.createKey(DJICameraKey.KeySDCardAvailableVideoDuration, cameraIndex)) { seconds: Int? ->
|
||||
_state.update { it.copy(remainRecordDuration = seconds ?: it.remainRecordDuration, error = null) }
|
||||
}
|
||||
listenSafely("KeySDCardTotalSpace", KeyTools.createKey(DJICameraKey.KeySDCardTotalSpace, cameraIndex)) { total: Int? ->
|
||||
updateStorage(total = total, remain = null)
|
||||
}
|
||||
listenSafely("KeySDCardRemainSpace", KeyTools.createKey(DJICameraKey.KeySDCardRemainSpace, cameraIndex)) { remain: Int? ->
|
||||
updateStorage(total = null, remain = remain)
|
||||
}
|
||||
listenSafely("KeyInternalStorageTotalSpace", KeyTools.createKey(DJICameraKey.KeyInternalStorageTotalSpace, cameraIndex)) { total: Int? ->
|
||||
if ((total ?: 0) > 0) updateStorage(total = total, remain = null)
|
||||
}
|
||||
listenSafely("KeyInternalStorageRemainSpace", KeyTools.createKey(DJICameraKey.KeyInternalStorageRemainSpace, cameraIndex)) { remain: Int? ->
|
||||
if ((remain ?: 0) > 0) updateStorage(total = null, remain = remain)
|
||||
}
|
||||
listenSafely("KeyCameraZoomRatios", KeyTools.createKey(CameraKey.KeyCameraZoomRatios, cameraIndex)) { zoom: Double? ->
|
||||
_state.update { it.copy(zoomFactor = zoom?.takeIf { value -> value > 0.0 } ?: it.zoomFactor, error = null) }
|
||||
}
|
||||
listenSafely("KeyThermalZoomRatios", KeyTools.createKey(CameraKey.KeyThermalZoomRatios, cameraIndex)) { zoom: Double? ->
|
||||
_state.update { it.copy(irZoomFactor = zoom?.takeIf { value -> value > 0.0 } ?: it.irZoomFactor, error = null) }
|
||||
}
|
||||
listenSafely("KeyLaserMeasureInformation", KeyTools.createKey(DJICameraKey.KeyLaserMeasureInformation, cameraIndex)) { info: LaserMeasureInformation? ->
|
||||
val location = info?.location3D
|
||||
_state.update {
|
||||
it.copy(
|
||||
laserDistance = info?.distance ?: 0.0,
|
||||
laserTargetLatitude = location?.latitude ?: 0.0,
|
||||
laserTargetLongitude = location?.longitude ?: 0.0,
|
||||
laserTargetAltitude = location?.altitude ?: 0.0,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyThermalGlobalMaxTemperature", KeyTools.createKey(DJICameraKey.KeyThermalGlobalMaxTemperature, cameraIndex)) { temperature: Double? ->
|
||||
_state.update { it.copy(thermalMaxTemperature = temperature ?: 0.0, error = null) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenRemoteControllerTelemetry() {
|
||||
listenSafely("KeyRcGPSInfo", KeyTools.createKey(RemoteControllerKey.KeyRcGPSInfo)) { info: RcGPSInfo? ->
|
||||
val location = info?.location
|
||||
val latitude = location?.latitude ?: 0.0
|
||||
val longitude = location?.longitude ?: 0.0
|
||||
val valid = info?.isValid == true && isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyRcGPSInfo valid=${info?.isValid} lat=$latitude lon=$longitude satellites=${info?.satelliteCount} accuracy=${info?.accuracy}"
|
||||
)
|
||||
_state.update {
|
||||
if (valid) {
|
||||
it.copy(
|
||||
rcLatitude = latitude,
|
||||
rcLongitude = longitude,
|
||||
rcLocationValid = true,
|
||||
rcGpsSatelliteCount = info?.satelliteCount ?: 0,
|
||||
rcGpsAccuracy = info?.accuracy ?: 0.0,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
it.copy(
|
||||
rcGpsSatelliteCount = info?.satelliteCount ?: 0,
|
||||
rcGpsAccuracy = info?.accuracy ?: it.rcGpsAccuracy,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRcBatteryInfo", KeyTools.createKey(RemoteControllerKey.KeyBatteryInfo)) { info: BatteryInfo? ->
|
||||
val percent = info?.batteryPercent ?: return@listenSafely
|
||||
if (percent in 0..100) {
|
||||
_state.update { it.copy(rcBatteryPercent = percent, error = null) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenGimbalTelemetry(gimbalIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN) {
|
||||
listenSafely("KeyGimbalAttitude", KeyTools.createKey(DJIGimbalKey.KeyGimbalAttitude, gimbalIndex)) { attitude: Attitude? ->
|
||||
attitude ?: return@listenSafely
|
||||
_state.update {
|
||||
it.copy(
|
||||
gimbalPitch = attitude.pitch ?: 0.0,
|
||||
gimbalRoll = attitude.roll ?: 0.0,
|
||||
gimbalYaw = attitude.yaw ?: 0.0,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenRtkTelemetry() {
|
||||
runCatching {
|
||||
rtkCenter.addRTKLocationInfoListener(rtkLocationInfoListener)
|
||||
Log.d(TelemetryLogTag, "registered RTKLocationInfoListener")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RTKLocationInfoListener failed: ${error.message}")
|
||||
}
|
||||
runCatching {
|
||||
rtkCenter.addRTKSystemStateListener(rtkSystemStateListener)
|
||||
Log.d(TelemetryLogTag, "registered RTKSystemStateListener")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RTKSystemStateListener failed: ${error.message}")
|
||||
}
|
||||
runCatching {
|
||||
rtkCenter.qxrtkManager.addNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener)
|
||||
rtkCenter.customRTKManager.addNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener)
|
||||
rtkCenter.cmccrtkManager.addNetworkRTKServiceInfoListener(rtkNetworkServiceInfoListener)
|
||||
Log.d(TelemetryLogTag, "registered RTK network service listeners")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RTK network service listeners failed: ${error.message}")
|
||||
}
|
||||
listenRtkMobileStationKeys()
|
||||
}
|
||||
|
||||
private fun listenRtkMobileStationKeys() {
|
||||
listenSafely("KeyRTKEnable", KeyTools.createKey(RtkMobileStationKey.KeyRTKEnable)) { enabled: Boolean? ->
|
||||
_state.update { it.copy(rtkEnabled = enabled == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyRTKEnable=$enabled")
|
||||
}
|
||||
listenSafely("KeyIsRTKWorking", KeyTools.createKey(RtkMobileStationKey.KeyIsRTKWorking)) { working: Boolean? ->
|
||||
_state.update { it.copy(rtkWorking = working == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyIsRTKWorking=$working")
|
||||
}
|
||||
listenSafely("KeyIsRTKBeingUsed", KeyTools.createKey(RtkMobileStationKey.KeyIsRTKBeingUsed)) { beingUsed: Boolean? ->
|
||||
_state.update { it.copy(rtkBeingUsed = beingUsed == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyIsRTKBeingUsed=$beingUsed")
|
||||
}
|
||||
listenSafely("KeyIsRTKFusionDataUsable", KeyTools.createKey(RtkMobileStationKey.KeyIsRTKFusionDataUsable)) { usable: Boolean? ->
|
||||
_state.update { it.copy(rtkFusionDataUsable = usable == true, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyIsRTKFusionDataUsable=$usable")
|
||||
}
|
||||
listenSafely("KeyRTKSatelliteCount", KeyTools.createKey(RtkMobileStationKey.KeyRTKSatelliteCount)) { count: Int? ->
|
||||
val value = count ?: 0
|
||||
_state.update { it.copy(rtkSatelliteCount = value, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyRTKSatelliteCount=$value")
|
||||
}
|
||||
listenSafely(
|
||||
"KeyRTKFusionMobileStationLocation",
|
||||
KeyTools.createKey(RtkMobileStationKey.KeyRTKFusionMobileStationLocation)
|
||||
) { location: LocationCoordinate3D? ->
|
||||
val latitude = location?.latitude ?: 0.0
|
||||
val longitude = location?.longitude ?: 0.0
|
||||
val altitude = location?.altitude ?: 0.0
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(TelemetryLogTag, "KeyRTKFusionMobileStationLocation lat=$latitude lon=$longitude alt=$altitude valid=$valid")
|
||||
_state.update {
|
||||
it.copy(
|
||||
rtkLatitude = latitude,
|
||||
rtkLongitude = longitude,
|
||||
rtkAltitude = altitude,
|
||||
rtkLocationValid = valid,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRTKLocation", KeyTools.createKey(RtkMobileStationKey.KeyRTKLocation)) { location: RTKLocation? ->
|
||||
val mobileLocation = location?.mobileStationLocation
|
||||
val solution = location?.positioningSolution?.name.orEmpty()
|
||||
val latitude = mobileLocation?.latitude ?: 0.0
|
||||
val longitude = mobileLocation?.longitude ?: 0.0
|
||||
val altitude = mobileLocation?.altitude ?: 0.0
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"KeyRTKLocation mobile=$latitude,$longitude,$altitude solution=$solution valid=$valid"
|
||||
)
|
||||
_state.update {
|
||||
if (valid) {
|
||||
it.copy(
|
||||
rtkLatitude = latitude,
|
||||
rtkLongitude = longitude,
|
||||
rtkAltitude = altitude,
|
||||
rtkLocationValid = true,
|
||||
rtkPositioningSolution = solution,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
it.copy(rtkPositioningSolution = solution, error = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
listenSafely("KeyRTKSatelliteInfo", KeyTools.createKey(RtkMobileStationKey.KeyRTKSatelliteInfo)) { info: RTKSatelliteInfo? ->
|
||||
val count = info?.mobileStationReceiver1Info?.sumOf { it.count } ?: 0
|
||||
_state.update { it.copy(rtkSatelliteCount = count, error = null) }
|
||||
Log.d(TelemetryLogTag, "KeyRTKSatelliteInfo receiver1Count=$count")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateAircraftLocation(latitude: Double?, longitude: Double?) {
|
||||
val lat = latitude ?: 0.0
|
||||
val lon = longitude ?: 0.0
|
||||
val valid = isValidCoordinate(lat, lon)
|
||||
_state.update {
|
||||
it.copy(latitude = lat, longitude = lon, locationValid = valid, error = null)
|
||||
}
|
||||
Log.d(TelemetryLogTag, "KeyAircraftLocation raw lat=$lat lon=$lon valid=$valid")
|
||||
}
|
||||
|
||||
private fun updateRtkLocationInfo(info: RTKLocationInfo?) {
|
||||
val realLocation = info?.real3DLocation
|
||||
val mobileLocation = info?.rtkLocation?.mobileStationLocation
|
||||
val latitude = realLocation?.latitude ?: mobileLocation?.latitude ?: 0.0
|
||||
val longitude = realLocation?.longitude ?: mobileLocation?.longitude ?: 0.0
|
||||
val altitude = realLocation?.altitude ?: mobileLocation?.altitude ?: 0.0
|
||||
val solution = info?.rtkLocation?.positioningSolution?.name.orEmpty()
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"RTKLocationInfo real3D=${realLocation?.latitude},${realLocation?.longitude},${realLocation?.altitude} " +
|
||||
"mobile=${mobileLocation?.latitude},${mobileLocation?.longitude},${mobileLocation?.altitude} solution=$solution valid=$valid"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
rtkLatitude = latitude,
|
||||
rtkLongitude = longitude,
|
||||
rtkAltitude = altitude,
|
||||
rtkLocationValid = valid,
|
||||
rtkPositioningSolution = solution,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRtkSystemState(state: RTKSystemState?) {
|
||||
lastRtkSystemState = state
|
||||
val enabled = state?.isRTKEnabled == true
|
||||
val healthy = state?.rtkHealthy == true
|
||||
val satelliteCount = if (enabled) {
|
||||
state?.satelliteInfo?.mobileStationReceiver1Info?.map { it.count }?.sum() ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"RTKSystemState enabled=$enabled healthy=$healthy satellites=$satelliteCount source=${state?.rtkReferenceStationSource} error=${state?.error}"
|
||||
)
|
||||
_state.update {
|
||||
it.copy(
|
||||
rtkEnabled = enabled,
|
||||
rtkHealthy = healthy,
|
||||
rtkSatelliteCount = satelliteCount,
|
||||
rtkLocationValid = if (enabled) it.rtkLocationValid else false,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
ensureRtkServiceStarted(state)
|
||||
}
|
||||
|
||||
private fun ensureRtkServiceStarted(state: RTKSystemState?) {
|
||||
if (state == null) return
|
||||
if (state.rtkHealthy) {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = true
|
||||
return
|
||||
}
|
||||
if (!state.isRTKEnabled) {
|
||||
enableRtkModuleIfPossible()
|
||||
return
|
||||
}
|
||||
val source = state.rtkReferenceStationSource ?: RTKReferenceStationSource.UNKNOWN
|
||||
if (!source.isNetworkRtkSource()) {
|
||||
setPreferredNetworkRtkSource(source)
|
||||
return
|
||||
}
|
||||
if (rtkServiceStarted) return
|
||||
val now = System.currentTimeMillis()
|
||||
if (rtkStartInProgress || now - lastRtkStartAtMs < RTK_START_RETRY_INTERVAL_MS) return
|
||||
startNetworkRtkService(source)
|
||||
}
|
||||
|
||||
private fun enableRtkModuleIfPossible() {
|
||||
if (rtkEnableInProgress || _state.value.motorsOn) return
|
||||
rtkEnableInProgress = true
|
||||
Log.d(TelemetryLogTag, "enable RTK module")
|
||||
rtkCenter.setAircraftRTKModuleEnabled(true, object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
rtkEnableInProgress = false
|
||||
Log.d(TelemetryLogTag, "enable RTK module success")
|
||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
rtkEnableInProgress = false
|
||||
Log.w(TelemetryLogTag, "enable RTK module failed: $error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setPreferredNetworkRtkSource(currentSource: RTKReferenceStationSource) {
|
||||
if (rtkSourceSetInProgress || rtkStartInProgress) return
|
||||
rtkSourceSetInProgress = true
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"set RTK reference station source current=$currentSource target=$DefaultNetworkRtkSource"
|
||||
)
|
||||
rtkCenter.setRTKReferenceStationSource(
|
||||
DefaultNetworkRtkSource,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
rtkSourceSetInProgress = false
|
||||
rtkServiceStarted = false
|
||||
Log.d(TelemetryLogTag, "set RTK reference station source success target=$DefaultNetworkRtkSource")
|
||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||
startNetworkRtkService(DefaultNetworkRtkSource)
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
rtkSourceSetInProgress = false
|
||||
Log.w(TelemetryLogTag, "set RTK reference station source failed target=$DefaultNetworkRtkSource error=$error")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun startNetworkRtkService(source: RTKReferenceStationSource) {
|
||||
rtkStartInProgress = true
|
||||
rtkServiceStarted = false
|
||||
lastRtkStartAtMs = System.currentTimeMillis()
|
||||
rtkCenter.setRTKMaintainAccuracyEnabled(true, null)
|
||||
Log.d(TelemetryLogTag, "start RTK network service source=$source coordinate=$DefaultNetworkRtkCoordinateSystem")
|
||||
when (source) {
|
||||
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
|
||||
rtkCenter.qxrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("QX"))
|
||||
}
|
||||
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
|
||||
rtkCenter.cmccrtkManager.startNetworkRTKService(DefaultNetworkRtkCoordinateSystem, rtkStartCallback("NTRIP"))
|
||||
}
|
||||
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> {
|
||||
rtkCenter.customRTKManager.startNetworkRTKService(rtkStartCallback("CUSTOM"))
|
||||
}
|
||||
else -> {
|
||||
rtkStartInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rtkStartCallback(label: String): CommonCallbacks.CompletionCallback =
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = true
|
||||
Log.d(TelemetryLogTag, "start $label RTK service success")
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
rtkStartInProgress = false
|
||||
rtkServiceStarted = false
|
||||
Log.w(TelemetryLogTag, "start $label RTK service failed: $error")
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startAndroidLocationFallback() {
|
||||
val manager = locationManager ?: return
|
||||
val context = appContext ?: return
|
||||
if (
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Log.w(TelemetryLogTag, "Android location permission not granted, RC system location disabled")
|
||||
return
|
||||
}
|
||||
|
||||
(
|
||||
listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, LocationManager.PASSIVE_PROVIDER) +
|
||||
manager.allProviders
|
||||
)
|
||||
.distinct()
|
||||
.filter { provider ->
|
||||
provider == LocationManager.PASSIVE_PROVIDER ||
|
||||
runCatching { manager.isProviderEnabled(provider) }.getOrDefault(false)
|
||||
}
|
||||
.forEach { provider ->
|
||||
runCatching {
|
||||
manager.getLastKnownLocation(provider)?.let(::updateRcLocationFromAndroid)
|
||||
manager.requestLocationUpdates(
|
||||
provider,
|
||||
1_000L,
|
||||
0.5f,
|
||||
androidLocationListener,
|
||||
Looper.getMainLooper()
|
||||
)
|
||||
Log.d(TelemetryLogTag, "registered RC Android location provider=$provider")
|
||||
}.onFailure { error ->
|
||||
Log.w(TelemetryLogTag, "register RC Android location provider=$provider failed: ${error.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRcLocationFromAndroid(location: Location) {
|
||||
val latitude = location.latitude
|
||||
val longitude = location.longitude
|
||||
val valid = isValidCoordinate(latitude, longitude)
|
||||
Log.d(
|
||||
TelemetryLogTag,
|
||||
"RcAndroidLocation provider=${location.provider} lat=$latitude lon=$longitude accuracy=${location.accuracy} valid=$valid"
|
||||
)
|
||||
if (!valid) return
|
||||
_state.update {
|
||||
if (it.rcLocationValid && it.rcGpsSatelliteCount > 0) {
|
||||
it
|
||||
} else {
|
||||
it.copy(
|
||||
rcLatitude = latitude,
|
||||
rcLongitude = longitude,
|
||||
rcAltitude = if (location.hasAltitude()) location.altitude else it.rcAltitude,
|
||||
rcLocationValid = true,
|
||||
rcGpsAccuracy = location.accuracy.toDouble(),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStorage(total: Int?, remain: Int?) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
storageTotal = total ?: it.storageTotal,
|
||||
storageRemain = remain ?: it.storageRemain,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun isValidCoordinate(latitude: Double?, longitude: Double?): Boolean {
|
||||
val lat = latitude ?: return false
|
||||
val lon = longitude ?: return false
|
||||
if (lat.isNaN() || lon.isNaN()) return false
|
||||
if (lat == 0.0 && lon == 0.0) return false
|
||||
return lat in -90.0..90.0 && lon in -180.0..180.0
|
||||
}
|
||||
|
||||
private val DefaultNetworkRtkSource = RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
|
||||
private val DefaultNetworkRtkCoordinateSystem = CoordinateSystem.CGCS2000
|
||||
|
||||
private fun RTKReferenceStationSource.isNetworkRtkSource(): Boolean =
|
||||
this == RTKReferenceStationSource.QX_NETWORK_SERVICE ||
|
||||
this == RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE ||
|
||||
this == RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.zklh.dronecontroller.core.video
|
||||
|
||||
import android.view.Surface
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
data class VideoPreviewState(
|
||||
val cameraIndex: ComponentIndexType = ComponentIndexType.LEFT_OR_MAIN,
|
||||
val availableCameras: List<ComponentIndexType> = emptyList(),
|
||||
val surfaceBound: Boolean = false,
|
||||
val message: String = "等待图传画面"
|
||||
)
|
||||
|
||||
class DjiVideoPreviewService {
|
||||
private val _state = MutableStateFlow(VideoPreviewState())
|
||||
val state: StateFlow<VideoPreviewState> = _state.asStateFlow()
|
||||
|
||||
private var surface: Surface? = null
|
||||
private var surfaceWidth: Int = 0
|
||||
private var surfaceHeight: Int = 0
|
||||
private var listenerRegistered = false
|
||||
|
||||
private val cameraListener = object : ICameraStreamManager.AvailableCameraUpdatedListener {
|
||||
override fun onAvailableCameraUpdated(cameras: List<ComponentIndexType>) {
|
||||
val selected = selectCamera(cameras)
|
||||
_state.update {
|
||||
it.copy(
|
||||
cameraIndex = selected,
|
||||
availableCameras = cameras,
|
||||
message = if (cameras.isEmpty()) "未发现可用相机码流" else "相机码流可用:$selected"
|
||||
)
|
||||
}
|
||||
bindSurface()
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(enableMap: Map<ComponentIndexType, Boolean>) {
|
||||
_state.update {
|
||||
it.copy(message = "相机码流状态:$enableMap")
|
||||
}
|
||||
bindSurface()
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
runCatching {
|
||||
cameraStreamManager().setKeepAliveDecoding(true)
|
||||
if (!listenerRegistered) {
|
||||
cameraStreamManager().addAvailableCameraUpdatedListener(cameraListener)
|
||||
listenerRegistered = true
|
||||
}
|
||||
bindSurface()
|
||||
}.onFailure { error ->
|
||||
_state.update { it.copy(surfaceBound = false, message = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
fun attachSurface(surface: Surface, width: Int, height: Int) {
|
||||
this.surface = surface
|
||||
surfaceWidth = width
|
||||
surfaceHeight = height
|
||||
bindSurface()
|
||||
}
|
||||
|
||||
fun detachSurface(surface: Surface) {
|
||||
runCatching { cameraStreamManager().removeCameraStreamSurface(surface) }
|
||||
if (this.surface == surface) {
|
||||
this.surface = null
|
||||
_state.update { it.copy(surfaceBound = false, message = "图传 Surface 已释放") }
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
surface?.let { currentSurface ->
|
||||
runCatching { cameraStreamManager().removeCameraStreamSurface(currentSurface) }
|
||||
}
|
||||
if (listenerRegistered) {
|
||||
runCatching { cameraStreamManager().removeAvailableCameraUpdatedListener(cameraListener) }
|
||||
listenerRegistered = false
|
||||
}
|
||||
surface = null
|
||||
_state.update { it.copy(surfaceBound = false, message = "图传预览已停止") }
|
||||
}
|
||||
|
||||
private fun bindSurface() {
|
||||
val currentSurface = surface ?: return
|
||||
if (!currentSurface.isValid || surfaceWidth <= 0 || surfaceHeight <= 0) return
|
||||
|
||||
runCatching {
|
||||
val manager = cameraStreamManager()
|
||||
val cameraIndex = _state.value.cameraIndex
|
||||
manager.enableStream(cameraIndex, true)
|
||||
manager.putCameraStreamSurface(
|
||||
cameraIndex,
|
||||
currentSurface,
|
||||
surfaceWidth,
|
||||
surfaceHeight,
|
||||
ICameraStreamManager.ScaleType.CENTER_CROP
|
||||
)
|
||||
_state.update { it.copy(surfaceBound = true, message = "正在显示 $cameraIndex 图传") }
|
||||
}.onFailure { error ->
|
||||
_state.update { it.copy(surfaceBound = false, message = error.message ?: error.toString()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun cameraStreamManager(): ICameraStreamManager =
|
||||
MediaDataCenter.getInstance().cameraStreamManager
|
||||
|
||||
private fun selectCamera(cameras: List<ComponentIndexType>): ComponentIndexType =
|
||||
when {
|
||||
cameras.contains(ComponentIndexType.LEFT_OR_MAIN) -> ComponentIndexType.LEFT_OR_MAIN
|
||||
cameras.isNotEmpty() -> cameras.first()
|
||||
else -> ComponentIndexType.LEFT_OR_MAIN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.zklh.dronecontroller.core.virtualstick
|
||||
|
||||
import com.zklh.dronecontroller.core.msdk.DjiCommandResult
|
||||
import com.zklh.dronecontroller.core.safety.SafetyInterlock
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlightControlAuthorityChangeReason
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.virtualstick.VirtualStickManager
|
||||
import dji.v5.manager.aircraft.virtualstick.VirtualStickState
|
||||
import dji.v5.manager.aircraft.virtualstick.VirtualStickStateListener
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
data class StickPosition(
|
||||
val leftHorizontal: Int = 0,
|
||||
val leftVertical: Int = 0,
|
||||
val rightHorizontal: Int = 0,
|
||||
val rightVertical: Int = 0
|
||||
)
|
||||
|
||||
data class VirtualStickStatus(
|
||||
val rawState: String = "",
|
||||
val changeReason: String = "",
|
||||
val enabled: Boolean = false,
|
||||
val advancedEnabled: Boolean = false,
|
||||
val authorityOwner: String = ""
|
||||
)
|
||||
|
||||
class VirtualStickService {
|
||||
private val _status = MutableStateFlow(VirtualStickStatus())
|
||||
val status: StateFlow<VirtualStickStatus> = _status.asStateFlow()
|
||||
|
||||
private val listener = object : VirtualStickStateListener {
|
||||
override fun onVirtualStickStateUpdate(stickState: VirtualStickState) {
|
||||
val enabled = stickState.isVirtualStickEnable
|
||||
val advancedEnabled = stickState.isVirtualStickAdvancedModeEnabled
|
||||
val owner = stickState.currentFlightControlAuthorityOwner?.toString().orEmpty()
|
||||
_status.update {
|
||||
it.copy(
|
||||
rawState = "虚拟摇杆=${if (enabled) "开启" else "关闭"} 高级模式=${if (advancedEnabled) "开启" else "关闭"} 控制权=$owner",
|
||||
enabled = enabled,
|
||||
advancedEnabled = advancedEnabled,
|
||||
authorityOwner = owner
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeReasonUpdate(reason: FlightControlAuthorityChangeReason) {
|
||||
_status.update { it.copy(changeReason = reason.name) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching { VirtualStickManager.getInstance().init() }
|
||||
VirtualStickManager.getInstance().setVirtualStickStateListener(listener)
|
||||
}
|
||||
|
||||
suspend fun enable(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
VirtualStickManager.getInstance().enableVirtualStick(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update { it.copy(enabled = true, rawState = "虚拟摇杆=开启") }
|
||||
continuation.resume(DjiCommandResult.ok("虚拟摇杆已开启"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun disable(): DjiCommandResult =
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) {
|
||||
DjiCommandResult.failed(SafetyInterlock.LOCKED_MESSAGE)
|
||||
} else {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
VirtualStickManager.getInstance().disableVirtualStick(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
_status.update { it.copy(enabled = false, rawState = "虚拟摇杆=关闭") }
|
||||
continuation.resume(DjiCommandResult.ok("虚拟摇杆已关闭"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
continuation.resume(DjiCommandResult.failed(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun setSpeedLevel(speedLevel: Double) {
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) return
|
||||
VirtualStickManager.getInstance().speedLevel = speedLevel
|
||||
}
|
||||
|
||||
fun sendStickPosition(position: StickPosition) {
|
||||
if (!SafetyInterlock.FLIGHT_COMMANDS_ENABLED) return
|
||||
VirtualStickManager.getInstance().leftStick.horizontalPosition = position.leftHorizontal
|
||||
VirtualStickManager.getInstance().leftStick.verticalPosition = position.leftVertical
|
||||
VirtualStickManager.getInstance().rightStick.horizontalPosition = position.rightHorizontal
|
||||
VirtualStickManager.getInstance().rightStick.verticalPosition = position.rightVertical
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
VirtualStickManager.getInstance().clearAllVirtualStickStateListener()
|
||||
runCatching { VirtualStickManager.getInstance().destroy() }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
28
sample/src/main/java/com/zklh/dronecontroller/ui/Theme.kt
Normal file
28
sample/src/main/java/com/zklh/dronecontroller/ui/Theme.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.zklh.dronecontroller.ui
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val ControllerColors = lightColorScheme(
|
||||
primary = Color(0xFF1D4ED8),
|
||||
secondary = Color(0xFF047857),
|
||||
tertiary = Color(0xFFB45309),
|
||||
error = Color(0xFFB91C1C),
|
||||
background = Color(0xFFF7F8FA),
|
||||
surface = Color(0xFFFFFFFF),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun ZklhDroneTheme(content: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colorScheme = ControllerColors,
|
||||
typography = MaterialTheme.typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dji.sampleV5.aircraft.views.MSDKInfoFragment
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/12/16
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class AircraftMSDKInfoFragment : MSDKInfoFragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = inflater.inflate(R.layout.frag_aircraft_main_title, container, false)
|
||||
initView(v)
|
||||
return v
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import androidx.fragment.app.commit
|
||||
import dji.sampleV5.aircraft.data.AircraftFragmentPageInfoFactory
|
||||
import dji.sampleV5.aircraft.data.CommonFragmentPageInfoFactory
|
||||
import dji.sampleV5.aircraft.data.FragmentPageItemList
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/3/9
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class AircraftTestingToolsActivity : TestingToolsActivity() {
|
||||
|
||||
override fun loadPages() {
|
||||
msdkCommonOperateVm.apply {
|
||||
val itemList = LinkedHashSet<FragmentPageItemList>().also {
|
||||
it.add(CommonFragmentPageInfoFactory().createPageInfo())
|
||||
it.add(AircraftFragmentPageInfoFactory().createPageInfo())
|
||||
}
|
||||
loaderItem(itemList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadTitleView() {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.main_info_fragment_container, AircraftMSDKInfoFragment())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/3/2
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIAircraftApplication : DJIApplication() {
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
com.cySdkyc.clx.Helper.install(this)
|
||||
loadDjiJniLibraries()
|
||||
}
|
||||
|
||||
private fun loadDjiJniLibraries() {
|
||||
runCatching {
|
||||
System.loadLibrary("djisdk_jni")
|
||||
}.onFailure { error ->
|
||||
Log.w("DJIAircraftApplication", "load djisdk_jni failed", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import dji.v5.common.utils.GeoidManager
|
||||
import dji.v5.ux.core.communication.DefaultGlobalPreferences
|
||||
import dji.v5.ux.core.communication.GlobalPreferencesManager
|
||||
import dji.v5.ux.core.util.UxSharedPreferencesUtil
|
||||
import dji.v5.ux.sample.showcase.defaultlayout.DefaultLayoutActivity
|
||||
import dji.v5.ux.sample.showcase.widgetlist.WidgetsActivity
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/2/14
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIAircraftMainActivity : DJIMainActivity() {
|
||||
|
||||
override fun prepareUxActivity() {
|
||||
UxSharedPreferencesUtil.initialize(this)
|
||||
GlobalPreferencesManager.initialize(DefaultGlobalPreferences(this))
|
||||
GeoidManager.getInstance().init(this)
|
||||
|
||||
enableDefaultLayout(DefaultLayoutActivity::class.java)
|
||||
enableWidgetList(WidgetsActivity::class.java)
|
||||
}
|
||||
|
||||
override fun prepareTestingToolsActivity() {
|
||||
enableTestingTools(AircraftTestingToolsActivity::class.java)
|
||||
}
|
||||
}
|
||||
26
sample/src/main/java/dji/sampleV5/aircraft/DJIApplication.kt
Normal file
26
sample/src/main/java/dji/sampleV5/aircraft/DJIApplication.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.app.Application
|
||||
import dji.sampleV5.aircraft.models.MSDKManagerVM
|
||||
import dji.sampleV5.aircraft.models.globalViewModels
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/3/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
open class DJIApplication : Application() {
|
||||
|
||||
private val msdkManagerVM: MSDKManagerVM by globalViewModels()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Ensure initialization is called first
|
||||
msdkManagerVM.initMobileSDK(this)
|
||||
}
|
||||
|
||||
}
|
||||
233
sample/src/main/java/dji/sampleV5/aircraft/DJIMainActivity.kt
Normal file
233
sample/src/main/java/dji/sampleV5/aircraft/DJIMainActivity.kt
Normal file
@@ -0,0 +1,233 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import dji.sampleV5.aircraft.databinding.ActivityMainBinding
|
||||
import dji.sampleV5.aircraft.models.BaseMainActivityVm
|
||||
import dji.sampleV5.aircraft.models.MSDKInfoVm
|
||||
import dji.sampleV5.aircraft.models.MSDKManagerVM
|
||||
import dji.sampleV5.aircraft.models.globalViewModels
|
||||
import dji.sampleV5.aircraft.util.Helper
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import dji.v5.utils.common.PermissionUtil
|
||||
import dji.v5.utils.common.StringUtils
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/2/10
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
abstract class DJIMainActivity : AppCompatActivity() {
|
||||
|
||||
val tag: String = LogUtils.getTag(this)
|
||||
private val permissionArray = arrayListOf(
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.KILL_BACKGROUND_PROCESSES,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
)
|
||||
|
||||
init {
|
||||
permissionArray.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// add(Manifest.permission.READ_MEDIA_IMAGES)
|
||||
// add(Manifest.permission.READ_MEDIA_VIDEO)
|
||||
// add(Manifest.permission.READ_MEDIA_AUDIO)
|
||||
} else {
|
||||
add(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val baseMainActivityVm: BaseMainActivityVm by viewModels()
|
||||
private val msdkInfoVm: MSDKInfoVm by viewModels()
|
||||
private val msdkManagerVM: MSDKManagerVM by globalViewModels()
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
private val disposable = CompositeDisposable()
|
||||
|
||||
abstract fun prepareUxActivity()
|
||||
|
||||
abstract fun prepareTestingToolsActivity()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// 有一些手机从系统桌面进入的时候可能会重启main类型的activity
|
||||
// 需要校验这种情况,业界标准做法,基本所有app都需要这个
|
||||
if (!isTaskRoot && intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN == intent.action) {
|
||||
|
||||
finish()
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
window.decorView.apply {
|
||||
systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
|
||||
initMSDKInfoView()
|
||||
observeSDKManager()
|
||||
checkPermissionAndRequest()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (checkPermission()) {
|
||||
handleAfterPermissionPermitted()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (checkPermission()) {
|
||||
handleAfterPermissionPermitted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAfterPermissionPermitted() {
|
||||
prepareTestingToolsActivity()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun initMSDKInfoView() {
|
||||
msdkInfoVm.msdkInfo.observe(this) {
|
||||
binding.textViewVersion.text = StringUtils.getResStr(R.string.sdk_version, it.SDKVersion + " " + it.buildVer)
|
||||
binding.textViewProductName.text = StringUtils.getResStr(R.string.product_name, it.productType.name)
|
||||
binding.textViewPackageProductCategory.text = StringUtils.getResStr(R.string.package_product_category, it.packageProductCategory)
|
||||
binding.textViewIsDebug.text = StringUtils.getResStr(R.string.is_sdk_debug, it.isDebug)
|
||||
binding.textCoreInfo.text = it.coreInfo.toString()
|
||||
}
|
||||
|
||||
binding.iconSdkForum.setOnClickListener {
|
||||
Helper.startBrowser(this, StringUtils.getResStr(R.string.sdk_forum_url))
|
||||
}
|
||||
|
||||
binding.iconReleaseNode.setOnClickListener {
|
||||
Helper.startBrowser(this, StringUtils.getResStr(R.string.release_node_url))
|
||||
}
|
||||
binding.iconTechSupport.setOnClickListener {
|
||||
Helper.startBrowser(this, StringUtils.getResStr(R.string.tech_support_url))
|
||||
}
|
||||
binding.viewBaseInfo.setOnClickListener {
|
||||
baseMainActivityVm.doPairing {
|
||||
showToast(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeSDKManager() {
|
||||
msdkManagerVM.lvRegisterState.observe(this) { resultPair ->
|
||||
val statusText: String?
|
||||
if (resultPair.first) {
|
||||
ToastUtils.showToast("Register Success")
|
||||
statusText = StringUtils.getResStr(this, R.string.registered)
|
||||
msdkInfoVm.initListener()
|
||||
handler.postDelayed({
|
||||
prepareUxActivity()
|
||||
}, 5000)
|
||||
} else {
|
||||
showToast("Register Failure: ${resultPair.second}")
|
||||
statusText = StringUtils.getResStr(this, R.string.unregistered)
|
||||
}
|
||||
binding.textViewRegistered.text = StringUtils.getResStr(R.string.registration_status, statusText)
|
||||
}
|
||||
|
||||
msdkManagerVM.lvProductConnectionState.observe(this) { resultPair ->
|
||||
showToast("Product: ${resultPair.second} ,ConnectionState: ${resultPair.first}")
|
||||
}
|
||||
|
||||
msdkManagerVM.lvProductChanges.observe(this) { productId ->
|
||||
showToast("Product: $productId Changed")
|
||||
}
|
||||
|
||||
msdkManagerVM.lvInitProcess.observe(this) { processPair ->
|
||||
showToast("Init Process event: ${processPair.first.name}")
|
||||
}
|
||||
|
||||
msdkManagerVM.lvDBDownloadProgress.observe(this) { resultPair ->
|
||||
showToast("Database Download Progress current: ${resultPair.first}, total: ${resultPair.second}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(content: String) {
|
||||
ToastUtils.showToast(content)
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun <T> enableDefaultLayout(cl: Class<T>) {
|
||||
enableShowCaseButton(binding.defaultLayoutButton, cl)
|
||||
}
|
||||
|
||||
fun <T> enableWidgetList(cl: Class<T>) {
|
||||
enableShowCaseButton(binding.widgetListButton, cl)
|
||||
}
|
||||
|
||||
fun <T> enableTestingTools(cl: Class<T>) {
|
||||
enableShowCaseButton(binding.testingToolButton, cl)
|
||||
}
|
||||
|
||||
private fun <T> enableShowCaseButton(view: View, cl: Class<T>) {
|
||||
view.isEnabled = true
|
||||
view.setOnClickListener {
|
||||
Intent(this, cl).also {
|
||||
startActivity(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermissionAndRequest() {
|
||||
if (!checkPermission()) {
|
||||
requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermission(): Boolean {
|
||||
for (i in permissionArray.indices) {
|
||||
if (!PermissionUtil.isPermissionGranted(this, permissionArray[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
result?.entries?.forEach {
|
||||
if (!it.value) {
|
||||
requestPermission()
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermission() {
|
||||
requestPermissionLauncher.launch(permissionArray.toArray(arrayOf()))
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
disposable.dispose()
|
||||
}
|
||||
}
|
||||
18
sample/src/main/java/dji/sampleV5/aircraft/TestToolsVM.kt
Normal file
18
sample/src/main/java/dji/sampleV5/aircraft/TestToolsVM.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
|
||||
/**
|
||||
* Description :TestingToolsActivity对应的ViewModel,主要用于创建djiToastResult,用于统一发送和观察需要Toast的内容
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/6/16
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class TestToolsVM : ViewModel() {
|
||||
val djiToastResult = MutableLiveData<DJIToastResult>()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package dji.sampleV5.aircraft
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.navigation.Navigation
|
||||
import dji.sampleV5.aircraft.databinding.ActivityMainBinding
|
||||
import dji.sampleV5.aircraft.databinding.ActivityTestingToolsBinding
|
||||
import dji.sampleV5.aircraft.models.MSDKCommonOperateVm
|
||||
import dji.sampleV5.aircraft.util.DJIToastUtil
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.sampleV5.aircraft.views.MSDKInfoFragment
|
||||
import dji.v5.ux.core.util.ViewUtil
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/7/23
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
abstract class TestingToolsActivity : AppCompatActivity() {
|
||||
|
||||
protected lateinit var binding: ActivityTestingToolsBinding
|
||||
protected val msdkCommonOperateVm: MSDKCommonOperateVm by viewModels()
|
||||
|
||||
private val testToolsVM: TestToolsVM by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityTestingToolsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
window.decorView.apply {
|
||||
systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
|
||||
// 设置Listener防止系统UI获取焦点后进入到非全屏状态
|
||||
window.decorView.setOnSystemUiVisibilityChangeListener() {
|
||||
if (it and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
|
||||
}
|
||||
}
|
||||
|
||||
loadTitleView()
|
||||
|
||||
DJIToastUtil.dJIToastLD = testToolsVM.djiToastResult
|
||||
testToolsVM.djiToastResult.observe(this) { result ->
|
||||
result?.msg?.let {
|
||||
ToastUtils.showToast(it)
|
||||
}
|
||||
}
|
||||
|
||||
msdkCommonOperateVm.mainPageInfoList.observe(this) { list ->
|
||||
list.iterator().forEach {
|
||||
addDestination(it.vavGraphId)
|
||||
}
|
||||
}
|
||||
|
||||
loadPages()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ViewUtil.setKeepScreen(this, true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ViewUtil.setKeepScreen(this, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 本activity的NavController,都是基于nav_host_fragment_container的
|
||||
*/
|
||||
private fun addDestination(id: Int) {
|
||||
val v = Navigation.findNavController(binding.navHostFragmentContainer).navInflater.inflate(id)
|
||||
Navigation.findNavController(binding.navHostFragmentContainer).graph.addAll(v)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
DJIToastUtil.dJIToastLD = null
|
||||
}
|
||||
|
||||
open fun loadTitleView() {
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.main_info_fragment_container, MSDKInfoFragment())
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun loadPages()
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dji.sampleV5.aircraft;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.zklh.dronecontroller.MainActivity;
|
||||
|
||||
public class UsbAttachActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sampleV5.aircraft.R
|
||||
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/7
|
||||
*F
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class AircraftFragmentPageInfoFactory : IFragmentPageInfoFactory {
|
||||
|
||||
override fun createPageInfo(): FragmentPageItemList {
|
||||
return FragmentPageItemList(R.navigation.nav_aircraft).apply {
|
||||
items.add(FragmentPageItem(R.id.virtual_stick_page, R.string.item_virtual_stick_title, R.string.item_virtual_description))
|
||||
items.add(FragmentPageItem(R.id.flight_record_page, R.string.item_flight_record_title, R.string.item_flight_record_description))
|
||||
items.add(FragmentPageItem(R.id.flight_upgrade_page, R.string.item_upgrade_title, R.string.item_upgrade_description))
|
||||
items.add(FragmentPageItem(R.id.flight_simulator_page, R.string.item_simulator_title, R.string.item_simulator_description))
|
||||
items.add(FragmentPageItem(R.id.psdk_center_page, R.string.item_psdk_title, R.string.item_psdk_description))
|
||||
items.add(FragmentPageItem(R.id.megaphone_page, R.string.item_megaphone_title, R.string.item_megaphone_description))
|
||||
items.add(FragmentPageItem(R.id.waypoint_v3_page, R.string.item_waypoint_title, R.string.item_waypoint_description))
|
||||
items.add(FragmentPageItem(R.id.waypoint_v3_page, R.string.item_waypoint_title, R.string.item_waypoint_description))
|
||||
items.add(FragmentPageItem(R.id.rtk_center_page, R.string.item_trk_center_title, R.string.item_trk_center_description))
|
||||
items.add(FragmentPageItem(R.id.perception_page, R.string.item_perception_title, R.string.item_perception_description))
|
||||
items.add(FragmentPageItem(R.id.uas_page, R.string.item_uas_title, R.string.item_uas_description))
|
||||
items.add(FragmentPageItem(R.id.lte_page, R.string.item_lte_title, R.string.item_lte_description))
|
||||
items.add(FragmentPageItem(R.id.fly_safe_page, R.string.item_fly_safe_title, R.string.item_fly_safe_description))
|
||||
items.add(FragmentPageItem(R.id.security_code_page, R.string.item_security_code_title, R.string.item_security_code_description))
|
||||
items.add(FragmentPageItem(R.id.mop_center_page, R.string.item_mop_title, R.string.item_mop_description))
|
||||
items.add(FragmentPageItem(R.id.look_at_page, R.string.item_look_at_title, R.string.item_look_at_description))
|
||||
items.add(FragmentPageItem(R.id.intelligent_flight_page, R.string.item_intelligent_flight_title, R.string.item_intelligent_flight__description))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sampleV5.aircraft.R
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/7
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class CommonFragmentPageInfoFactory : IFragmentPageInfoFactory {
|
||||
|
||||
override fun createPageInfo(): FragmentPageItemList {
|
||||
return FragmentPageItemList(R.navigation.nav_common).apply {
|
||||
items.add(FragmentPageItem(R.id.key_value_page, R.string.item_key_value_title, R.string.item_key_value_description))
|
||||
items.add(FragmentPageItem(R.id.key_multi_camera_stream_page, R.string.item_multi_camera_stream_title, R.string.item_multi_camera_stream_description))
|
||||
items.add(FragmentPageItem(R.id.key_multi_video_decoding_page, R.string.item_multi_video_decoding_title, R.string.item_multi_video_decoding_description, true))
|
||||
items.add(FragmentPageItem(R.id.key_data_protection_page, R.string.item_data_protection_title, R.string.item_data_protection_description))
|
||||
items.add(FragmentPageItem(R.id.key_diagnostic_page, R.string.item_diagnostic_title, R.string.item_diagnostic_description))
|
||||
items.add(FragmentPageItem(R.id.key_media_playback_page, R.string.item_media_playback_title, R.string.item_media_playback_description))
|
||||
items.add(FragmentPageItem(R.id.key_live_stream_page, R.string.item_live_stream_title, R.string.item_live_stream_description))
|
||||
items.add(FragmentPageItem(R.id.key_login_account_page, R.string.item_login_account_title, R.string.item_login_account_description))
|
||||
items.add(FragmentPageItem(R.id.key_log_info_page, R.string.item_log_info_title, R.string.item_log_info_description))
|
||||
items.add(FragmentPageItem(R.id.key_diagnostic_page, R.string.item_diagnostic_title, R.string.item_diagnostic_description))
|
||||
items.add(FragmentPageItem(R.id.key_app_silently_upgrade_page, R.string.item_app_silently_upgrade_title, R.string.item_app_silently_upgrade_description))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.BaseStationDeviceType
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationInfo
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
|
||||
|
||||
/**
|
||||
* Description :驱动UI的数据模型,基于RTKBaseStationConnectInfo新增一个连接状态属性
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/3/6
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIRTKBaseStationConnectInfo(
|
||||
var baseStationId: Int,
|
||||
var signalLevel: Int,
|
||||
var rtkStationName: String,
|
||||
var connectStatus: RTKStationConnetState = RTKStationConnetState.IDLE
|
||||
) : RTKStationInfo(baseStationId, signalLevel, rtkStationName, BaseStationDeviceType.BS_RTK2) {
|
||||
constructor() : this(0, 0, "")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
|
||||
/**
|
||||
* Description :面向Model使用的执行结果封装类,简单封装了外部需要Toast的信息
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/2/22
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class DJIToastResult(var isSuccess: Boolean, var msg: String? = null) {
|
||||
|
||||
companion object {
|
||||
fun success(msg: String? = null): DJIToastResult {
|
||||
return DJIToastResult(true, "success ${msg ?: ""}")
|
||||
}
|
||||
|
||||
fun failed(msg: String): DJIToastResult {
|
||||
return DJIToastResult(false, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.flightcontroller.AccessLockerDeviceType
|
||||
|
||||
/**
|
||||
* Description :设备的锁状态
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/8/10
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 设备的锁状态
|
||||
*/
|
||||
data class DeviceLockStatus(
|
||||
var deviceIndex: AccessLockerDeviceType = AccessLockerDeviceType.UNKNOWN,
|
||||
//功能是否支持
|
||||
var isFeatureSupported: Boolean = false,
|
||||
//功能是否开启
|
||||
var isFeatureEnabled: Boolean = false,
|
||||
//功能是否需要验证
|
||||
var isFeatureNeedToBeVerified: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* 修改密码数据类
|
||||
*/
|
||||
data class ModifyPasswordBean(val currentPassword: String, val newPassword: String){
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/19 6:30 下午
|
||||
* @description: 飞机位置信息,坐标,朝向
|
||||
*/
|
||||
data class FlightControlState(
|
||||
var longtitude : Double = 0.0
|
||||
, var latitude: Double = 0.0
|
||||
, val head : Float = 0.0f
|
||||
, val height : Double = 0.0
|
||||
, val distance: Double= 0.0
|
||||
, val speed :Double = 0.0
|
||||
, val homeLocation: LocationCoordinate2D
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
data class FragmentPageItemList(
|
||||
val vavGraphId: Int = DEFAULT_RES_ID,
|
||||
val items: LinkedHashSet<FragmentPageItem> = LinkedHashSet()
|
||||
)
|
||||
|
||||
data class FragmentPageItem(
|
||||
val id: Int = DEFAULT_RES_ID,
|
||||
val title: Int = DEFAULT_RES_ID,
|
||||
val description: Int = DEFAULT_RES_ID,
|
||||
val isStrike: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/7
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
interface IFragmentPageInfoFactory {
|
||||
fun createPageInfo(): FragmentPageItemList
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/09/14 2:32 下午
|
||||
* @description: key的检查结果信息
|
||||
*/
|
||||
data class KeyCheckInfo(
|
||||
var keyName:String ,
|
||||
var isPass : Boolean ,
|
||||
var failedReson: String
|
||||
) {}
|
||||
@@ -0,0 +1,543 @@
|
||||
package dji.sampleV5.aircraft.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
|
||||
import dji.v5.manager.mop.DataResult;
|
||||
import dji.v5.manager.mop.Pipeline;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
|
||||
/**
|
||||
* 利用MOP通道,实现自定的文件上传下载协议
|
||||
*/
|
||||
public class MOPCmdHelper {
|
||||
private static final byte CMD_REQ = 0x50;
|
||||
private static final byte CMD_ACK = 0x51;
|
||||
private static final byte CMD_TRANS_ACK = 0x52;
|
||||
private static final byte CMD_FILE_INFO = 0x60;
|
||||
private static final byte CMD_DOWNLOAD = 0x61;
|
||||
private static final byte CMD_FILE_DATA = 0x62;
|
||||
private static final byte CMD_FILE_TRANS_FAIL = 0x63;
|
||||
private static final byte CMD_FILE_TRANS_FAIL_ACK = 0x64;
|
||||
private static final byte CMD_0 = 0x00;
|
||||
private static final byte CMD_1 = 0x01;
|
||||
public static final int PACK_HEADER_SIZE = 8;
|
||||
public static final int PACK_FILE_INFO_SIZE = 53;
|
||||
public static final String UPLOAD_FILE = "uploadFile: ";
|
||||
|
||||
private static final int FILE_NAME_LENGTH = 32;
|
||||
|
||||
private static final String TAG = MOPCmdHelper.class.getSimpleName();
|
||||
|
||||
public static byte[] getUploadFileHeader() {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_REQ;
|
||||
cmd[1] = CMD_0;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static byte[] getDownloadFileHeader() {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_REQ;
|
||||
cmd[1] = CMD_1;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static byte[] getDownloadFile() {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_DOWNLOAD;
|
||||
cmd[1] = (byte) 0xFF;
|
||||
cmd[4] = (byte) 0x20;
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static byte[] getFileDataHeader(int size, int flag, int seq) {
|
||||
byte[] cmd = new byte[PACK_HEADER_SIZE];
|
||||
cmd[0] = CMD_FILE_DATA;
|
||||
cmd[1] = (byte) flag;
|
||||
cmd[2] = (byte) seq;
|
||||
cmd[4] = (byte) (size & 0xff);
|
||||
cmd[5] = (byte) (size >> 8 & 0xff);
|
||||
cmd[6] = (byte) (size >> 16 & 0xff);
|
||||
cmd[7] = (byte) (size >> 24 & 0xff);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static boolean sendDownloadCmd(Pipeline p) {
|
||||
byte[] cmd = getDownloadFileHeader();
|
||||
DataResult result = p.writeData(cmd);
|
||||
if (result.getLength() > 0) {
|
||||
return parseCommonAck(p);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static FileInfo sendDownloadFileReq(Pipeline p, String filename, PipelineAdapter.OnEventListener listener) {
|
||||
if (!sendDownloadCmd(p)) {
|
||||
return null;
|
||||
}
|
||||
byte[] header = getDownloadFile();
|
||||
byte[] chars = filename.getBytes();
|
||||
|
||||
byte[] req = new byte[header.length + FILE_NAME_LENGTH];
|
||||
System.arraycopy(header, 0, req, 0, header.length);
|
||||
System.arraycopy(chars, 0, req, header.length, chars.length);
|
||||
|
||||
if (chars.length < FILE_NAME_LENGTH) {
|
||||
req[header.length + chars.length] = '\0';
|
||||
}
|
||||
// 发送请求,要下载的文件
|
||||
DataResult result = p.writeData(req);
|
||||
if (result.getLength() > 0) {
|
||||
|
||||
LogUtils.i(TAG, "sendDownloadFileReq ack: Success", "/MOP");
|
||||
// 读取文件信息
|
||||
byte[] fileInfoBuff = new byte[MOPCmdHelper.PACK_HEADER_SIZE + MOPCmdHelper.PACK_FILE_INFO_SIZE];
|
||||
int sum = 0;
|
||||
while (sum < fileInfoBuff.length) {
|
||||
// 能获取MD5等信息
|
||||
int len = p.readData(fileInfoBuff).getLength();
|
||||
|
||||
if (len > 0) {
|
||||
// 由于read不支持offset,这里处理fileInfoBuff的拼接,应该使用临时的byte[]来copyArray
|
||||
sum += len;
|
||||
LogUtils.e(TAG, "sendDownloadFileReq download:" + sum, "/MOP");
|
||||
} else {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FileInfo.parse(fileInfoBuff);
|
||||
|
||||
} else {
|
||||
postResultTipEvent(TipEvent.DOWNLOAD, "request failure", null, listener);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int sendTransFileFailReq(Pipeline pipeline, long length) {
|
||||
byte[] buff = new byte[12];
|
||||
buff[0] = CMD_FILE_TRANS_FAIL;
|
||||
buff[1] = (byte) 0xFF;
|
||||
buff[7] = 0x04;
|
||||
buff[8] = (byte) (length >> 0 & 0xff);
|
||||
buff[9] = (byte) (length >> 8 & 0xff);
|
||||
buff[10] = (byte) (length >> 16 & 0xff);
|
||||
buff[11] = (byte) (length >> 24 & 0xff);
|
||||
|
||||
int len = getInt(buff, PACK_HEADER_SIZE, 4);
|
||||
int result = pipeline.writeData(buff).getLength();
|
||||
LogUtils.i(TAG, "sendTransFileFailReq:" + result, "/MOP");
|
||||
LogUtils.i(TAG, "len:" + len, "/MOP");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int sendTransFileFailAck(Pipeline pipeline, long length) {
|
||||
byte[] buff = new byte[12];
|
||||
buff[0] = CMD_FILE_TRANS_FAIL_ACK;
|
||||
buff[1] = (byte) 0xFF;
|
||||
buff[7] = 0x04;
|
||||
buff[8] = (byte) (length >> 0 & 0xff);
|
||||
buff[9] = (byte) (length >> 8 & 0xff);
|
||||
buff[10] = (byte) (length >> 16 & 0xff);
|
||||
buff[11] = (byte) (length >> 24 & 0xff);
|
||||
int result = pipeline.writeData(buff).getLength();
|
||||
LogUtils.i(TAG, "sendTransFileFailAck:" + result, "/MOP");
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int parseTransFileFailAck(Pipeline p) {
|
||||
byte[] buff = new byte[12];
|
||||
int size;
|
||||
// 读取回包
|
||||
while (((size = p.readData(buff).getLength()) < 0)) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
LogUtils.i(TAG, "parseTransFileFailAck wait ack: " + size, "/MOP");
|
||||
}
|
||||
return getInt(buff, PACK_HEADER_SIZE, 4);
|
||||
}
|
||||
|
||||
|
||||
public static boolean parseCommonAck(Pipeline p) {
|
||||
// 读取文件下载的信息
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
int size;
|
||||
// 读取回包
|
||||
while ((size = p.readData(buff).getLength()) < 0) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
LogUtils.e(TAG, "sendDownloadFileReq wait ack: " + size, "/MOP");
|
||||
}
|
||||
return (buff[0] == CMD_ACK) && (buff[1] == CMD_0);
|
||||
}
|
||||
|
||||
public static boolean parseUploadAck(Pipeline p) {
|
||||
// 读取文件下载的信息
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
int size;
|
||||
// 读取回包
|
||||
while ((size = p.readData(buff).getLength()) < 0) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
LogUtils.i(TAG, "upload ack: " + size, "/MOP");
|
||||
}
|
||||
return (buff[0] == CMD_TRANS_ACK) && (buff[1] == CMD_0);
|
||||
}
|
||||
|
||||
public static FileTransResult parseFileDataCmd(byte[] buff) {
|
||||
if (buff[0] == CMD_FILE_DATA) {
|
||||
int length = getInt(buff, 4, 4);
|
||||
return new FileTransResult(true, length);
|
||||
}
|
||||
if (buff[0] == CMD_FILE_TRANS_FAIL) {
|
||||
int length = getInt(buff, 4, 4);
|
||||
return new FileTransResult(false, length);
|
||||
}
|
||||
LogUtils.e(TAG, "parseFileDataCmd error", "/MOP");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static int sendTransAck(Pipeline p, boolean result) {
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
buff[0] = CMD_TRANS_ACK;
|
||||
buff[1] = result ? CMD_0 : CMD_1;
|
||||
return p.writeData(buff).getLength();
|
||||
}
|
||||
|
||||
public static int sendAck(Pipeline p, byte cmd) {
|
||||
byte[] buff = new byte[PACK_HEADER_SIZE];
|
||||
buff[0] = CMD_ACK;
|
||||
buff[1] = cmd;
|
||||
return p.writeData(buff).getLength();
|
||||
}
|
||||
|
||||
public static boolean isFileEnd(byte[] buff) {
|
||||
return buff[0] == CMD_FILE_DATA && buff[1] == CMD_1;
|
||||
}
|
||||
|
||||
public static int sendUploadFileReq(Pipeline data, String filename, byte[] buff, long time, byte[] md5,
|
||||
PipelineAdapter.OnEventListener listener) {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.filename = filename;
|
||||
fileInfo.fileLength = buff.length;
|
||||
fileInfo.md5 = md5;
|
||||
LogUtils.i(TAG, "sendUploadFileReq fileInfo:" + fileInfo, "/MOP");
|
||||
listener.onFileInfoEvent(fileInfo);
|
||||
|
||||
DataResult dataResult = data.writeData(getUploadFileHeader());
|
||||
int result = dataResult.getLength();
|
||||
if (result < 0) {
|
||||
postResultTipEvent(TipEvent.UPLOAD, "Upload Failure: " + dataResult.toString(), null, listener);
|
||||
return result;
|
||||
}
|
||||
if (parseCommonAck(data)) {
|
||||
// 发送md5等
|
||||
byte[] fileHeader = fileInfo.getHeader();
|
||||
DataResult writeData = data.writeData(fileHeader);
|
||||
result = writeData.getLength();
|
||||
LogUtils.i(TAG, "sendUploadFileReq send md5:" + writeData.toString(), "/MOP");
|
||||
if (result > 0) {
|
||||
if (parseCommonAck(data)) {
|
||||
// 上传文件
|
||||
uploadFile(data, buff, time, listener);
|
||||
// 上传完的ack
|
||||
if (parseUploadAck(data)) {
|
||||
postResultTipEvent(TipEvent.UPLOAD, "Upload Success", null, listener);
|
||||
}
|
||||
} else {
|
||||
postResultTipEvent(TipEvent.UPLOAD, "Upload Failure", null, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void postResultTipEvent(int type, String result, String progress, PipelineAdapter.OnEventListener listener) {
|
||||
TipEvent event = new TipEvent(type);
|
||||
event.result = result;
|
||||
event.progress = progress;
|
||||
listener.onTipEvent(event);
|
||||
}
|
||||
|
||||
private static int uploadFile(Pipeline pipeline, byte[] buff, long time, PipelineAdapter.OnEventListener listener) {
|
||||
int size = 3072;
|
||||
byte[] header;
|
||||
int hadWrote = 0;
|
||||
int seq = 0;
|
||||
if (buff.length <= size) {
|
||||
header = getFileDataHeader(buff.length, CMD_1, seq);
|
||||
int result = writeData(pipeline, buff, time, header, hadWrote, buff.length, listener);
|
||||
LogUtils.i(TAG, UPLOAD_FILE + result, "/MOP", ",seq:", seq);
|
||||
return result;
|
||||
} else {
|
||||
while (buff.length - hadWrote > size) {
|
||||
header = getFileDataHeader(size, CMD_0, seq);
|
||||
int result = writeData(pipeline, buff, time, header, hadWrote, size, listener);
|
||||
|
||||
hadWrote += size;
|
||||
LogUtils.i(TAG, UPLOAD_FILE + hadWrote + " result:" + result, ",seq:", seq);
|
||||
seq++;
|
||||
}
|
||||
int length = buff.length - hadWrote;
|
||||
header = getFileDataHeader(length, CMD_1, seq);
|
||||
int result = writeData(pipeline, buff, time, header, hadWrote, length, listener);
|
||||
hadWrote += result;
|
||||
LogUtils.i(TAG, UPLOAD_FILE + length, "/MOP");
|
||||
}
|
||||
|
||||
String progress = String.format("uploadSize:%d, useTime:%d(ms)", hadWrote, System.currentTimeMillis() - time);
|
||||
postResultTipEvent(TipEvent.UPLOAD, null, progress, listener);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int writeData(Pipeline pipeline, byte[] buff, long time, byte[] header, int hadWrote, int length,
|
||||
PipelineAdapter.OnEventListener listener) {
|
||||
byte[] d = new byte[length + header.length];
|
||||
System.arraycopy(header, 0, d, 0, header.length);
|
||||
System.arraycopy(buff, hadWrote, d, header.length, length);
|
||||
DataResult dataResult = pipeline.writeData(d);
|
||||
int result = dataResult.getLength();
|
||||
|
||||
String progress = String.format("uploadSize:%d, useTime:%d(ms)", hadWrote, System.currentTimeMillis() - time);
|
||||
postResultTipEvent(TipEvent.UPLOAD, null, progress, listener);
|
||||
|
||||
if (result < 0) {
|
||||
LogUtils.e(TAG, "writeData miss: " + dataResult.toString());
|
||||
// 发送上传出错的req
|
||||
sendTransFileFailReq(pipeline, hadWrote);
|
||||
// 读取对端返回的已读长度
|
||||
int len = parseTransFileFailAck(pipeline);
|
||||
if (len == hadWrote) {
|
||||
sendAck(pipeline, CMD_0);
|
||||
return writeData(pipeline, buff, time, header, hadWrote, length, listener);
|
||||
} else {
|
||||
LogUtils.e(TAG, "writeData error: " + len);
|
||||
sendAck(pipeline, CMD_1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int parseFileFailIndex(Pipeline pipeline) {
|
||||
byte[] buff = new byte[4];
|
||||
pipeline.readData(buff);
|
||||
return getInt(buff, 0, 4);
|
||||
}
|
||||
|
||||
|
||||
public static byte[] getMD5(File file) {
|
||||
byte[] buffer = new byte[8192];
|
||||
byte[] desc = new byte[16];
|
||||
try (InputStream ins = new FileInputStream(file)) {
|
||||
MessageDigest md5 = MessageDigest.getInstance("MD5");
|
||||
int len;
|
||||
while ((len = ins.read(buffer)) != -1) {
|
||||
md5.update(buffer, 0, len);
|
||||
}
|
||||
byte[] source = md5.digest();
|
||||
System.arraycopy(source, 0, desc, 0, desc.length);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
return desc;
|
||||
}
|
||||
|
||||
public static int getInt(byte[] bytes, final int offset, int length) {
|
||||
if (null == bytes) {
|
||||
return 0;
|
||||
}
|
||||
final int bytesLen = bytes.length;
|
||||
if (bytesLen == 0 || offset < 0 || bytesLen <= offset) {
|
||||
return 0;
|
||||
}
|
||||
if (length > bytesLen - offset) {
|
||||
length = bytesLen - offset;
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
for (int i = length + offset - 1; i >= offset; i--) {
|
||||
value = (value << 8 | (bytes[i] & 0xff));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static class FileInfo {
|
||||
private boolean isExist;
|
||||
private int fileLength;
|
||||
private String filename;
|
||||
private byte[] md5;
|
||||
|
||||
public static FileInfo parse(byte[] data) {
|
||||
if (data[0] != CMD_FILE_INFO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder("mop_cmd_file:");
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
sb.append(data[i] + ",");
|
||||
}
|
||||
|
||||
FileInfo info = new FileInfo();
|
||||
info.isExist = data[8] == 1;
|
||||
info.fileLength = getInt(data, 9, 4);
|
||||
byte[] d = new byte[32];
|
||||
System.arraycopy(data, 13, d, 0, 32);
|
||||
info.filename = getString(d);
|
||||
byte[] md5 = new byte[16];
|
||||
System.arraycopy(data, 45, md5, 0, 16);
|
||||
info.md5 = md5;
|
||||
return info;
|
||||
}
|
||||
|
||||
public byte[] getHeader() {
|
||||
byte[] buff = new byte[61];
|
||||
buff[0] = CMD_FILE_INFO;
|
||||
buff[1] = (byte) 0xFF;
|
||||
buff[4] = 0x35;
|
||||
buff[8] = 0;
|
||||
buff[9] = (byte) (fileLength & 0xff);
|
||||
buff[10] = (byte) (fileLength >> 8 & 0xff);
|
||||
buff[11] = (byte) (fileLength >> 16 & 0xff);
|
||||
buff[12] = (byte) (fileLength >> 24 & 0xff);
|
||||
|
||||
byte[] chars = filename.getBytes();
|
||||
System.arraycopy(chars, 0, buff, 13, chars.length);
|
||||
System.arraycopy(md5, 0, buff, 45, 16);
|
||||
if (chars.length < 32) {
|
||||
buff[13 + chars.length] = '\0';
|
||||
}
|
||||
return buff;
|
||||
}
|
||||
|
||||
public static String getString(byte[] bytes) {
|
||||
if (null == bytes) {
|
||||
return "";
|
||||
}
|
||||
// 去除NULL字符
|
||||
byte zero = 0x00;
|
||||
byte no = (byte) 0xFF;
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
if (bytes[i] == zero || bytes[i] == no) {
|
||||
bytes = readBytes(bytes, 0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new String(bytes, Charset.forName("GBK"));
|
||||
}
|
||||
|
||||
public static byte[] readBytes(byte[] source, int from, int length) {
|
||||
byte[] result = new byte[length];
|
||||
System.arraycopy(source, from, result, 0, length);
|
||||
/**
|
||||
for (int i = 0; i < length; i++) {
|
||||
result[i] = source[from + i];
|
||||
}
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FileInfo{" +
|
||||
"isExist=" + isExist +
|
||||
", fileLength=" + fileLength +
|
||||
", filename='" + filename + '\'' +
|
||||
", md5=" + Arrays.toString(md5) +
|
||||
'}';
|
||||
}
|
||||
|
||||
public boolean isExist() {
|
||||
return isExist;
|
||||
}
|
||||
|
||||
public int getFileLength() {
|
||||
return fileLength;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public byte[] getMd5() {
|
||||
return md5;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ProcessCallback {
|
||||
void callback(int length);
|
||||
}
|
||||
|
||||
public static class FileTransResult {
|
||||
// true代表读了多少数据(length),false重新读写,并根据之后length个字节锁代表的int值去移动游标到指定位置
|
||||
private boolean success;
|
||||
private int length;
|
||||
|
||||
public FileTransResult(boolean success, int length) {
|
||||
this.success = success;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TipEvent {
|
||||
public static final int UPLOAD = 0;
|
||||
public static final int DOWNLOAD = 1;
|
||||
|
||||
public TipEvent(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private int type;
|
||||
private String state;
|
||||
private String result;
|
||||
private String progress;
|
||||
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
sample/src/main/java/dji/sampleV5/aircraft/data/MSDKInfo.kt
Normal file
27
sample/src/main/java/dji/sampleV5/aircraft/data/MSDKInfo.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.product.ProductType
|
||||
import dji.v5.common.register.PackageProductCategory
|
||||
import dji.v5.utils.inner.SDKConfig
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/5/6
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
|
||||
data class MSDKInfo(val SDKVersion: String = DEFAULT_STR) {
|
||||
var buildVer: String = DEFAULT_STR
|
||||
var isDebug: Boolean = false
|
||||
var packageProductCategory: PackageProductCategory? = null
|
||||
var productType: ProductType = ProductType.UNKNOWN
|
||||
var networkInfo: String = DEFAULT_STR
|
||||
var countryCode: String = DEFAULT_STR
|
||||
var firmwareVer: String = DEFAULT_STR
|
||||
var isLDMLicenseLoaded: String = DEFAULT_STR
|
||||
var isLDMEnabled: String = DEFAULT_STR
|
||||
var coreInfo: SDKConfig.CoreInfo? = null
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.v5.common.error.IDJIError
|
||||
|
||||
|
||||
|
||||
|
||||
data class MissionUploadStateInfo(
|
||||
var tips: String = ""
|
||||
, var updateProgress: Double = 0.0
|
||||
, val error: IDJIError? = null
|
||||
) {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import dji.sampleV5.aircraft.R
|
||||
|
||||
|
||||
/**
|
||||
* Description :PayloadWidgetIcon数据处理类
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/12/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class PayloadWidgetIconAdapter :
|
||||
ListAdapter<PayloadWidgetItem, PayloadWidgetIconAdapter.ViewHolder>(DiffCallback) {
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_payload_wdiget_item, parent, false)
|
||||
return ViewHolder(view, parent.context)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.bind(item)
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View, val context: Context) : RecyclerView.ViewHolder(itemView) {
|
||||
private val iconImage: ImageView = itemView.findViewById(R.id.iv_widget_icon)
|
||||
private val iconDesc: TextView = itemView.findViewById(R.id.tv_widget_desc)
|
||||
|
||||
fun bind(item: PayloadWidgetItem) {
|
||||
Glide.with(context).load(item.imgPath).into(iconImage)
|
||||
iconDesc.text = item.des
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
object DiffCallback : DiffUtil.ItemCallback<PayloadWidgetItem>() {
|
||||
override fun areItemsTheSame(oldItem: PayloadWidgetItem, newItem: PayloadWidgetItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: PayloadWidgetItem, newItem: PayloadWidgetItem): Boolean {
|
||||
return oldItem.des == newItem.des && oldItem.imgPath == newItem.imgPath
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
/**
|
||||
* Description :
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/12/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
data class PayloadWidgetItem(var des: String, var imgPath: String?)
|
||||
@@ -0,0 +1,564 @@
|
||||
package dji.sampleV5.aircraft.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import dji.sampleV5.aircraft.R;
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType;
|
||||
import dji.v5.common.error.DJIPipeLineError;
|
||||
import dji.v5.manager.mop.DataResult;
|
||||
import dji.v5.manager.mop.Pipeline;
|
||||
import dji.v5.manager.mop.PipelineManager;
|
||||
import dji.v5.utils.common.BytesUtil;
|
||||
import dji.v5.utils.common.DiskUtil;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
import dji.v5.ux.core.util.ToastUtils;
|
||||
|
||||
public class PipelineAdapter extends RecyclerView.Adapter<PipelineAdapter.ViewHolder> {
|
||||
private final List<Pair<ComponentIndexType, Pipeline>> data;
|
||||
private final LayoutInflater mInflater;
|
||||
private ViewHolder curHolder;
|
||||
|
||||
private final OnDisconnectListener listener = new OnDisconnectListener() {
|
||||
@Override
|
||||
public void onDisconnect(Pipeline d) {
|
||||
if (data.contains(d)) {
|
||||
data.remove(d);
|
||||
notifyItemRemoved(data.indexOf(d));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
public PipelineAdapter(Context context, List<Pair<ComponentIndexType, Pipeline>> data) {
|
||||
this.mInflater = LayoutInflater.from(context);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void addItem(ComponentIndexType indexType, Pipeline action) {
|
||||
if (action == null || data == null || data.contains(action)) {
|
||||
return;
|
||||
}
|
||||
data.add(new Pair<>(indexType, action));
|
||||
notifyItemInserted(getItemCount() - 1);
|
||||
}
|
||||
|
||||
public List<Pair<ComponentIndexType, Pipeline>> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
curHolder = new ViewHolder(mInflater.inflate(R.layout.adapter_pipeline_item, parent, false));
|
||||
return curHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
holder.setData(data.get(position));
|
||||
holder.setListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data == null ? 0 : data.size();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@Override
|
||||
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView);
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
ViewHolder viewholder = (ViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
|
||||
if (viewholder != null) {
|
||||
viewholder.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
holder.destroy();
|
||||
}
|
||||
|
||||
public static String getTime() {
|
||||
String patten = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
SimpleDateFormat format = new SimpleDateFormat(patten);
|
||||
return format.format(new Date());
|
||||
}
|
||||
|
||||
// @Override
|
||||
public void onDisconnect(Pipeline data) {
|
||||
this.data.remove(data);
|
||||
notifyItemRemoved(this.data.indexOf(data));
|
||||
}
|
||||
|
||||
public void onReset(Pipeline pipeline) {
|
||||
this.data.remove(pipeline);
|
||||
curHolder.destroy();
|
||||
notifyItemRemoved(this.data.indexOf(pipeline));
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private String tag = LogUtils.getTag(this);
|
||||
private final TextView nameTv;
|
||||
private final TextView downloadTv;
|
||||
private final TextView uploadTv;
|
||||
private final TextView downloadLogTv;
|
||||
private final TextView uploadLogTv;
|
||||
private final TextView disconnectTv;
|
||||
private final TextView filenameTv;
|
||||
private final Switch autoDownloadSwitch;
|
||||
|
||||
HandlerThread uploadThread;
|
||||
HandlerThread downloadThread;
|
||||
Handler uploadHandler;
|
||||
Handler downloadHandler;
|
||||
private WeakReference<OnDisconnectListener> listenerWeakReference;
|
||||
|
||||
private boolean uploading;
|
||||
private boolean downloading;
|
||||
|
||||
private String uploadFileInfoLog;
|
||||
private String uploadProgress;
|
||||
private String uploadResult;
|
||||
|
||||
private String downloadFileInfoLog;
|
||||
private String downloadProgress;
|
||||
private String downloadResult;
|
||||
private int downloadPackCount;
|
||||
private int downloadSize;
|
||||
|
||||
private int downloadSuccessCount;
|
||||
private int downloadCount;
|
||||
|
||||
private int uploadSuccessCount;
|
||||
private int uploadCount;
|
||||
|
||||
private String uploadFileName = "mopSample.log";
|
||||
private final OnEventListener listener = new OnEventListener() {
|
||||
@Override
|
||||
public void onTipEvent(MOPCmdHelper.TipEvent event) {
|
||||
handlerTipEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileInfoEvent(MOPCmdHelper.FileInfo event) {
|
||||
onEvent3BackgroundThread(event);
|
||||
}
|
||||
};
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
nameTv = itemView.findViewById(R.id.tv_name);
|
||||
downloadTv = itemView.findViewById(R.id.tv_download);
|
||||
uploadTv = itemView.findViewById(R.id.tv_upload);
|
||||
downloadLogTv = itemView.findViewById(R.id.tv_download_log);
|
||||
uploadLogTv = itemView.findViewById(R.id.tv_upload_log);
|
||||
disconnectTv = itemView.findViewById(R.id.tv_disconnect);
|
||||
filenameTv = itemView.findViewById(R.id.et_file_name);
|
||||
autoDownloadSwitch = itemView.findViewById(R.id.switch_auto_download);
|
||||
}
|
||||
|
||||
public void setData(Pair<ComponentIndexType, Pipeline> pair) {
|
||||
destroy();
|
||||
Pipeline pipeline = pair.second;
|
||||
uploadThread = new HandlerThread("upload");
|
||||
downloadThread = new HandlerThread("download");
|
||||
uploadThread.start();
|
||||
downloadThread.start();
|
||||
uploadHandler = new Handler(uploadThread.getLooper());
|
||||
downloadHandler = new Handler(downloadThread.getLooper());
|
||||
View.OnClickListener localListener = v -> {
|
||||
int id = v.getId();
|
||||
if (id == R.id.tv_upload) {
|
||||
showDialog(itemView.getContext(), pipeline);
|
||||
} else if (id == R.id.tv_download) {
|
||||
downloadHandler.post(() -> {
|
||||
resetDownInfo();
|
||||
downloadFile(pipeline, filenameTv.getText().toString());
|
||||
});
|
||||
} else if (id == R.id.tv_disconnect) {
|
||||
destroy();
|
||||
disconnect(pair.first, pipeline);
|
||||
}
|
||||
};
|
||||
|
||||
String title = String.format("Id=%d, MOPType = %s, trans_type=%s", pipeline.getId(), pipeline.getPipelineDeviceType(),
|
||||
pipeline.getTransmissionControlType());
|
||||
nameTv.setText(title);
|
||||
downloadTv.setOnClickListener(localListener);
|
||||
uploadTv.setOnClickListener(localListener);
|
||||
disconnectTv.setOnClickListener(localListener);
|
||||
}
|
||||
|
||||
public void setListener(OnDisconnectListener listener) {
|
||||
listenerWeakReference = new WeakReference<>(listener);
|
||||
}
|
||||
|
||||
private void disconnect(ComponentIndexType indexType, Pipeline pipeline) {
|
||||
PipelineManager.getInstance().disconnectPipeline(indexType, pipeline.getId(), pipeline.getPipelineDeviceType(),
|
||||
pipeline.getTransmissionControlType());
|
||||
if (listenerWeakReference != null && listenerWeakReference.get() != null) {
|
||||
listenerWeakReference.get().onDisconnect(pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
public void destroy() {
|
||||
if (uploadHandler != null && uploadThread != null) {
|
||||
uploadHandler.removeCallbacksAndMessages(null);
|
||||
uploadThread.quitSafely();
|
||||
}
|
||||
if (downloadHandler != null && downloadThread != null) {
|
||||
downloadHandler.removeCallbacksAndMessages(null);
|
||||
downloadThread.quitSafely();
|
||||
}
|
||||
resetDownInfo();
|
||||
resetUploadInfo();
|
||||
}
|
||||
|
||||
private void resetDownInfo() {
|
||||
downloadPackCount = 0;
|
||||
downloadSize = 0;
|
||||
|
||||
downloadFileInfoLog = "";
|
||||
downloadProgress = "";
|
||||
downloadResult = "";
|
||||
downloading = false;
|
||||
}
|
||||
|
||||
private void resetUploadInfo() {
|
||||
uploadFileInfoLog = "";
|
||||
uploadProgress = "";
|
||||
uploadResult = "";
|
||||
uploading = false;
|
||||
}
|
||||
|
||||
private void uploadFile(Pipeline data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (uploading) {
|
||||
toast("uploading");
|
||||
return;
|
||||
}
|
||||
uploading = true;
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
InputStream inputStream = null;
|
||||
FileOutputStream out = null;
|
||||
ByteArrayOutputStream outputStream = null;
|
||||
try {
|
||||
byte[] buff = new byte[3072];
|
||||
inputStream = itemView.getContext().getAssets().open("mop/" + uploadFileName);
|
||||
File tmp = new File(itemView.getContext().getCacheDir(), "mop.tmp");
|
||||
out = new FileOutputStream(tmp);
|
||||
outputStream = new ByteArrayOutputStream();
|
||||
int len;
|
||||
while ((len = inputStream.read(buff, 0, 3072)) > 0) {
|
||||
outputStream.write(buff, 0, len);
|
||||
out.write(buff, 0, len);
|
||||
}
|
||||
MOPCmdHelper.sendUploadFileReq(data, uploadFileName, outputStream.toByteArray(), time, MOPCmdHelper.getMD5(tmp), listener);
|
||||
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
if (outputStream != null) {
|
||||
outputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
uploadCount++;
|
||||
updateUploadUI();
|
||||
uploading = false;
|
||||
}
|
||||
|
||||
private void downloadFile(Pipeline pipeline, String filename) {
|
||||
if (pipeline == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloading) {
|
||||
toast("downloading");
|
||||
return;
|
||||
}
|
||||
downloading = true;
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
// 获取文件信息
|
||||
MOPCmdHelper.FileInfo fileInfo = MOPCmdHelper.sendDownloadFileReq(pipeline, filename, listener);
|
||||
if (fileInfo == null || !fileInfo.isExist()) {
|
||||
LogUtils.e(tag, "downloadFile fail", "/MOP");
|
||||
downloading = false;
|
||||
return;
|
||||
}
|
||||
downloadFileInfoLog = fileInfo.toString();
|
||||
updateDownloadUI();
|
||||
|
||||
RandomAccessFile stream = null;
|
||||
try {
|
||||
LogUtils.i(tag, " fileInfo=" + fileInfo);
|
||||
File file = DiskUtil.getDiskCacheDir(itemView.getContext(), fileInfo.getFilename());
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
file.createNewFile();
|
||||
stream = new RandomAccessFile(file, "rw");
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// 开始读取文件数据
|
||||
byte[] headBuff = new byte[MOPCmdHelper.PACK_HEADER_SIZE];
|
||||
DataResult readData = pipeline.readData(headBuff);
|
||||
if (readData.getError() != null && readData.getError().errorCode().equals(DJIPipeLineError.CLOSING)) {
|
||||
LogUtils.e(tag, "Pipeline is closing,finish down");
|
||||
return;
|
||||
}
|
||||
if (readData.getLength() < MOPCmdHelper.PACK_HEADER_SIZE) {
|
||||
LogUtils.e(tag, "readData.getLength()=" + readData.getLength() + " <8,jump over!result=" + readData);
|
||||
continue;
|
||||
}
|
||||
// 这个包带有的文件字节
|
||||
MOPCmdHelper.FileTransResult result = MOPCmdHelper.parseFileDataCmd(headBuff);
|
||||
if (result == null) {
|
||||
LogUtils.e(tag, "FileTransResult=null ,finish down");
|
||||
downloading = false;
|
||||
return;
|
||||
}
|
||||
if (result.isSuccess()) {
|
||||
int length = result.getLength();
|
||||
int sum = 0;
|
||||
int readLength = length;
|
||||
while (sum < length) {
|
||||
byte[] dataBuff = new byte[readLength];
|
||||
DataResult dataResult = pipeline.readData(dataBuff);
|
||||
int len = dataResult.getLength();
|
||||
if (len > 0) {
|
||||
downloadPackCount++;
|
||||
downloadSize += len;
|
||||
byte[] subArray = BytesUtil.subArray(dataBuff, 0, 3);
|
||||
sum += len;
|
||||
readLength -= len;
|
||||
String tmp = BytesUtil.toHexStringLowercase(subArray);
|
||||
LogUtils.i(tag, "pack seq:" + downloadPackCount + ", data:" + tmp + ", length:" + len);
|
||||
LogUtils.i(tag, filename + " download : " + sum);
|
||||
downloadLogTv.post(() -> {
|
||||
downloadProgress = String.format("have downloadPack = %d, downloadSize:%d/%d, useTime:%d(ms)", downloadPackCount,
|
||||
downloadSize, fileInfo.getFileLength(), System.currentTimeMillis() - time);
|
||||
updateDownloadUI();
|
||||
});
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.write(dataBuff, 0, len);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
} else if (dataResult.getError() != null && dataResult.getError().errorCode().equals(DJIPipeLineError.CLOSING)) {
|
||||
LogUtils.e(tag, "Pipeline is closing,finish down");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 解析出错位置
|
||||
int position = MOPCmdHelper.parseFileFailIndex(pipeline);
|
||||
// ack
|
||||
MOPCmdHelper.sendTransFileFailAck(pipeline, position);
|
||||
// 确认是否能接着传
|
||||
if (MOPCmdHelper.parseCommonAck(pipeline)) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.seek(position);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
downloadLogTv.post(() -> downloadLogTv.setText("transfer failure"));
|
||||
downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
if (MOPCmdHelper.isFileEnd(headBuff)) {
|
||||
LogUtils.i(tag, "Download success ,updateDownloadUI");
|
||||
downloadResult = "Download success";
|
||||
updateDownloadUI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LogUtils.e(tag, e.getMessage());
|
||||
}
|
||||
downloading = false;
|
||||
|
||||
boolean result = verifyMd5(fileInfo, DiskUtil.getDiskCacheDir(itemView.getContext(), fileInfo.getFilename()));
|
||||
downloadResult = "verify md5 :" + result;
|
||||
downloadCount++;
|
||||
if (downloadSize == fileInfo.getFileLength() && result) {
|
||||
downloadSuccessCount++;
|
||||
MOPCmdHelper.sendTransAck(pipeline, true);
|
||||
} else {
|
||||
MOPCmdHelper.sendTransAck(pipeline, false);
|
||||
}
|
||||
updateDownloadUI();
|
||||
if (autoDownloadSwitch.isChecked()) {
|
||||
downloadTv.postDelayed(() -> downloadTv.performClick(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verifyMd5(MOPCmdHelper.FileInfo fileInfo, File file) {
|
||||
String md5 = BytesUtil.toHexStringLowercase(fileInfo.getMd5());
|
||||
String md5_1 = BytesUtil.toHexStringLowercase(MOPCmdHelper.getMD5(file));
|
||||
String log = String.format("MOPCmdHelper.FileInfo_md5: %s, file_md5:%s", md5, md5_1);
|
||||
LogUtils.e("PipelineAdapter", log, "/MOP");
|
||||
return md5.equals(md5_1);
|
||||
}
|
||||
|
||||
private void toast(String text) {
|
||||
itemView.post(() -> ToastUtils.INSTANCE.showShortToast(text));
|
||||
}
|
||||
|
||||
public void handlerTipEvent(MOPCmdHelper.TipEvent event) {
|
||||
switch (event.getType()) {
|
||||
case MOPCmdHelper.TipEvent.UPLOAD:
|
||||
if (event.getResult() != null) {
|
||||
uploadResult = event.getResult();
|
||||
if (uploadResult.contains("Success")) {
|
||||
// hard code
|
||||
uploadSuccessCount++;
|
||||
}
|
||||
}
|
||||
if (event.getProgress() != null) {
|
||||
uploadProgress = event.getProgress();
|
||||
}
|
||||
updateUploadUI();
|
||||
break;
|
||||
case MOPCmdHelper.TipEvent.DOWNLOAD:
|
||||
if (event.getResult() != null) {
|
||||
downloadResult = event.getResult();
|
||||
}
|
||||
if (event.getProgress() != null) {
|
||||
downloadProgress = event.getProgress();
|
||||
}
|
||||
downloadLogTv.post(() -> downloadLogTv.setText(downloadLogTv.getText().toString() + "\n" + event.getResult()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void onEvent3BackgroundThread(MOPCmdHelper.FileInfo event) {
|
||||
uploadFileInfoLog = event.toString();
|
||||
updateUploadUI();
|
||||
}
|
||||
|
||||
private void updateUploadUI() {
|
||||
StringBuilder sb = new StringBuilder("Upload:" + "\n")
|
||||
.append(uploadFileInfoLog == null ? "" : uploadFileInfoLog).append("\n")
|
||||
.append(uploadProgress == null ? "" : uploadProgress).append("\n")
|
||||
.append(uploadResult == null ? "" : uploadResult).append("\n")
|
||||
.append("成功/次数:" + uploadSuccessCount + "/" + uploadCount);
|
||||
|
||||
uploadLogTv.post(() -> uploadLogTv.setText(sb.toString()));
|
||||
}
|
||||
|
||||
private void updateDownloadUI() {
|
||||
StringBuilder sb = new StringBuilder("Download:" + "\n")
|
||||
.append(downloadFileInfoLog == null ? "" : downloadFileInfoLog).append("\n")
|
||||
.append(downloadProgress == null ? "" : downloadProgress).append("\n")
|
||||
.append(downloadResult == null ? "" : downloadResult).append("\n")
|
||||
.append("成功/次数:" + downloadSuccessCount + "/" + downloadCount);
|
||||
downloadLogTv.post(() -> downloadLogTv.setText(sb.toString()));
|
||||
}
|
||||
|
||||
|
||||
private void showDialog(Context context, Pipeline pipeline) {
|
||||
View root = LayoutInflater.from(context).inflate(R.layout.dialog_mop_upload, null, false);
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle("Select File Size")
|
||||
.setView(root)
|
||||
.setPositiveButton("Confirm", (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
RadioGroup group = root.findViewById(R.id.group_file);
|
||||
int checkedRadioButtonId = group.getCheckedRadioButtonId();
|
||||
if (checkedRadioButtonId == R.id.rb_1) {
|
||||
uploadFileName = "mopSample.log";
|
||||
} else if (checkedRadioButtonId == R.id.rb_2) {
|
||||
uploadFileName = "mopSample.jpeg";
|
||||
} else if (checkedRadioButtonId == R.id.rb_3) {
|
||||
uploadFileName = "mopSample.mp4";
|
||||
}
|
||||
uploadHandler.post(() -> {
|
||||
resetUploadInfo();
|
||||
uploadFile(pipeline);
|
||||
});
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface OnDisconnectListener {
|
||||
void onDisconnect(Pipeline data);
|
||||
}
|
||||
|
||||
public interface OnEventListener {
|
||||
void onTipEvent(MOPCmdHelper.TipEvent event);
|
||||
|
||||
void onFileInfoEvent(MOPCmdHelper.FileInfo event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.v5.manager.aircraft.lte.LTEPrivatizationServerInfo
|
||||
import dji.v5.manager.areacode.AreaCode
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.DjiSharedPreferencesManager
|
||||
import dji.v5.utils.common.JsonUtil
|
||||
|
||||
object QuickTestConfig {
|
||||
|
||||
private const val KEY_LTE_AUTHENTICATION_INFO = "key_lte_authentication_info"
|
||||
private const val KEY_AC_LTE_PRIVATIZATION_SERVER_INFO = "key_ac_lte_privatization_server_info"
|
||||
private const val KEY_RC_LTE_PRIVATIZATION_SERVER_INFO = "key_rc_lte_privatization_server_info"
|
||||
|
||||
val simulatorAreaList = listOf(
|
||||
SimulatorArea("中国", LocationCoordinate2D(22.5797650, 113.9411710), AreaCode.CHINA),
|
||||
SimulatorArea("美国", LocationCoordinate2D(34.063191, -118.121621), AreaCode.UNITED_STATES_OF_AMERICA),
|
||||
SimulatorArea("日本", LocationCoordinate2D(35.658890, 139.746074), AreaCode.JAPAN),
|
||||
SimulatorArea("法国", LocationCoordinate2D(48.860284, 2.336282), AreaCode.FRANCE),
|
||||
SimulatorArea("德国", LocationCoordinate2D(52.516294, 13.376631), AreaCode.GERMANY),
|
||||
SimulatorArea("禁飞区", LocationCoordinate2D(22.645945, 113.816311), AreaCode.CHINA),
|
||||
SimulatorArea("授权区", LocationCoordinate2D(22.395237, 114.203203), AreaCode.CHINA),
|
||||
SimulatorArea("加强警告区", LocationCoordinate2D(22.208262, 114.03056), AreaCode.CHINA),
|
||||
)
|
||||
|
||||
fun getCacheLTEAuthenticationInfo(): LTEAuthCacheInfo? {
|
||||
val str = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_LTE_AUTHENTICATION_INFO, "")
|
||||
if (str.isEmpty()) {
|
||||
return LTEAuthCacheInfo()
|
||||
}
|
||||
return JsonUtil.toBean(DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_LTE_AUTHENTICATION_INFO, ""), LTEAuthCacheInfo::class.java)
|
||||
}
|
||||
|
||||
fun updateCacheLTEAuthenticationInfo(info: LTEAuthCacheInfo) {
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), KEY_LTE_AUTHENTICATION_INFO, JsonUtil.toJson(info))
|
||||
}
|
||||
|
||||
fun getCacheACLTEPrivatizationServerInfo(): LTEPrivatizationServerInfo? {
|
||||
val str = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_AC_LTE_PRIVATIZATION_SERVER_INFO, "")
|
||||
if (str.isEmpty()) {
|
||||
return LTEPrivatizationServerInfo()
|
||||
}
|
||||
return JsonUtil.toBean(DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_AC_LTE_PRIVATIZATION_SERVER_INFO, ""), LTEPrivatizationServerInfo::class.java)
|
||||
}
|
||||
|
||||
fun updateCacheACLTEPrivatizationServerInfo(info: LTEPrivatizationServerInfo) {
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), KEY_AC_LTE_PRIVATIZATION_SERVER_INFO, JsonUtil.toJson(info))
|
||||
}
|
||||
|
||||
fun getCacheRCLTEPrivatizationServerInfo(): LTEPrivatizationServerInfo? {
|
||||
val str = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_RC_LTE_PRIVATIZATION_SERVER_INFO, "")
|
||||
if (str.isEmpty()) {
|
||||
return LTEPrivatizationServerInfo()
|
||||
}
|
||||
return JsonUtil.toBean(DjiSharedPreferencesManager.getString(ContextUtil.getContext(), KEY_RC_LTE_PRIVATIZATION_SERVER_INFO, ""), LTEPrivatizationServerInfo::class.java)
|
||||
}
|
||||
|
||||
fun updateCacheRCLTEPrivatizationServerInfo(info: LTEPrivatizationServerInfo) {
|
||||
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), KEY_RC_LTE_PRIVATIZATION_SERVER_INFO, JsonUtil.toJson(info))
|
||||
}
|
||||
|
||||
data class SimulatorArea(
|
||||
val name: String, val location: LocationCoordinate2D, val areaCode: AreaCode
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
data class LTEAuthCacheInfo(
|
||||
val phoneAreaCode: String = "86",
|
||||
val phoneNumber: String = "1234567890",
|
||||
val verificationCode: String = "123456"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dji.sampleV5.aircraft.R
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
|
||||
import dji.v5.utils.common.LogUtils
|
||||
|
||||
/**
|
||||
* Description :基站RTK的Adapter,展示扫码到的基站情况
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/3/6
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class RtkStationScanAdapter(val context: Context, list: List<DJIRTKBaseStationConnectInfo>?) :
|
||||
RecyclerView.Adapter<RtkStationScanAdapter.RtkViewHolder>() {
|
||||
|
||||
private val LEVEL_0 = 0
|
||||
private val LEVEL_1 = 1
|
||||
private val LEVEL_2 = 2
|
||||
private val LEVEL_3 = 3
|
||||
private val LEVEL_4 = 4
|
||||
private var baseStationInfoList: List<DJIRTKBaseStationConnectInfo>? = list
|
||||
private val TAG = "RtkStationScanAdapter"
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RtkViewHolder {
|
||||
val view: View = LayoutInflater.from(context)
|
||||
.inflate(R.layout.adapter_rtk_connect_status_item, parent, false)
|
||||
return RtkViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RtkViewHolder, position: Int) {
|
||||
baseStationInfoList?.get(position)?.let { info ->
|
||||
holder.mRtkStationNameTv.text = info.rtkStationName
|
||||
holder.mConnectSignalIv.setBackgroundResource(getSignalLevelDrawable(info.signalLevel))
|
||||
holder.itemView.setOnClickListener {
|
||||
val pos = holder.layoutPosition
|
||||
val isConnecting = checkConnecting()
|
||||
val hasConnected = checkConnected(pos)
|
||||
if (!isConnecting && !hasConnected) {//上一笔连接还没结束或者基站已连接则不响应新的连接请求
|
||||
mOnItemClickListener?.onItemClick(holder.itemView, pos)
|
||||
} else if (checkConnecting()) {
|
||||
ToastUtils.showToast("The station is currently connecting, please try to connect later!")
|
||||
} else {
|
||||
ToastUtils.showToast("The station has connected!")
|
||||
}
|
||||
}
|
||||
when (info.connectStatus) {
|
||||
RTKStationConnetState.IDLE -> {
|
||||
holder.mConnectStatusIv.gone()
|
||||
}
|
||||
RTKStationConnetState.CONNECTING -> {
|
||||
holder.mConnectStatusIv.visible()
|
||||
holder.mConnectStatusIv.setImageResource(R.drawable.ic_rotate_progress_circle)
|
||||
}
|
||||
RTKStationConnetState.CONNECTED -> {
|
||||
holder.mConnectStatusIv.visible()
|
||||
holder.mConnectStatusIv.setImageResource(R.drawable.ic_confirm)
|
||||
}
|
||||
else -> {
|
||||
holder.mConnectStatusIv.gone()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return if (baseStationInfoList == null) 0 else baseStationInfoList!!.size
|
||||
}
|
||||
|
||||
interface OnItemClickListener {
|
||||
fun onItemClick(view: View?, position: Int)
|
||||
}
|
||||
|
||||
private var mOnItemClickListener: OnItemClickListener? = null
|
||||
|
||||
fun setOnItemClickListener(onItemClickListener: OnItemClickListener?) {
|
||||
mOnItemClickListener = onItemClickListener
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getSignalLevelDrawable(signalLevel: Int): Int {
|
||||
LogUtils.i(TAG, "getSignalLevelDrawable,signalLevel=$signalLevel")
|
||||
return when (signalLevel) {
|
||||
LEVEL_0 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_0
|
||||
LEVEL_1 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_1
|
||||
LEVEL_2 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_2
|
||||
LEVEL_3 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_3
|
||||
LEVEL_4 -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_4
|
||||
else -> dji.v5.ux.R.drawable.uxsdk_ic_topbar_signal_level_5
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RtkViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var mConnectStatusIv: ImageView
|
||||
var mRtkStationNameTv: TextView
|
||||
var mConnectSignalIv: ImageView
|
||||
|
||||
init {
|
||||
mConnectStatusIv = itemView.findViewById(R.id.connect_status_iv)
|
||||
mRtkStationNameTv = itemView.findViewById(R.id.station_name_tv)
|
||||
mConnectSignalIv = itemView.findViewById(R.id.connect_signal_iv)
|
||||
mConnectStatusIv.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun View.visible() {
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.gone() {
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun checkConnecting(): Boolean {
|
||||
baseStationInfoList?.let {
|
||||
for (station in it) {
|
||||
if (station.connectStatus == RTKStationConnetState.CONNECTING) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun checkConnected(position: Int): Boolean {
|
||||
baseStationInfoList?.run {
|
||||
val stationInfo = get(position)
|
||||
if (stationInfo.connectStatus == RTKStationConnetState.CONNECTED) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import dji.sampleV5.aircraft.R
|
||||
import dji.sdk.errorcode.DJIErrorCode
|
||||
import dji.v5.utils.common.StringUtils
|
||||
|
||||
enum class SecurityCodeOperationResult(val resultCode: Int, @param:StringRes val desResId: Int) {
|
||||
NO_ERR(0, R.string.securitycode_operation_result_success),
|
||||
BUSY(DJIErrorCode.ACCESS_LOCKER_V1_BUSY.value(), R.string.securitycode_operation_result_busy),
|
||||
SET_FAILED(DJIErrorCode.ACCESS_LOCKER_V1_PW_SET_FAILED.value(), R.string.securitycode_operation_result_set_failed),
|
||||
VERIFY_FAILED(DJIErrorCode.ACCESS_LOCKER_V1_PW_VERIFY_FAILED.value(), R.string.securitycode_operation_result_verify_failed),
|
||||
NEW_PW_REPEAT(DJIErrorCode.ACCESS_LOCKER_V1_NEW_PW_REPEAT.value(), R.string.securitycode_operation_result_new_pw_repeat),
|
||||
RESET_FAILED(DJIErrorCode.ACCESS_LOCKER_V1_RESET_FAILED.value(), R.string.securitycode_operation_result_reset_failed),
|
||||
USERNAME_INVALID(DJIErrorCode.ACCESS_LOCKER_USER_NAME_FORMAT_INVALID.value(), R.string.securitycode_operation_result_user_name_format_invalid),
|
||||
CONTROL_NOT_SUPPORT(DJIErrorCode.ACCESS_LOCKER_V1_CONTROL_NOT_SUPPORT.value(), R.string.securitycode_operation_result_control_not_support),
|
||||
FEATURE_NOT_SUPPORT(DJIErrorCode.FEATURE_NOT_SUPPORTED.value(), R.string.securitycode_operation_result_feature_not_support),
|
||||
COMMAND_NOT_SUPPORT(DJIErrorCode.COMMAND_NOT_SUPPORT_NOW.value(), R.string.securitycode_operation_result_command_not_support),
|
||||
NOT_CURRENT_DEVICE(0xFFFF, R.string.securitycode_operation_result_not_current_device_result),
|
||||
UNKNOWN(DJIErrorCode.UNKNOWN.value(), R.string.securitycode_operation_result_unknown);
|
||||
|
||||
val resultDes: String
|
||||
get() = StringUtils.getResStr(desResId)
|
||||
|
||||
companion object {
|
||||
fun find(code: Int): SecurityCodeOperationResult {
|
||||
for (operationResult in values()) {
|
||||
if (operationResult.resultCode == code) {
|
||||
return operationResult
|
||||
}
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
import dji.v5.common.video.channel.VideoChannelState
|
||||
import dji.v5.common.video.channel.VideoChannelType
|
||||
import dji.v5.common.video.decoder.DecoderState
|
||||
import dji.v5.common.video.stream.StreamSource
|
||||
|
||||
data class VideoChannelInfo(var videoChannelState: VideoChannelState = VideoChannelState.CLOSE) {
|
||||
var streamSource: StreamSource? = null
|
||||
var videoChannelType: VideoChannelType = VideoChannelType.PRIMARY_STREAM_CHANNEL
|
||||
var decoderState: DecoderState = DecoderState.INITIALIZED
|
||||
var resolution: String = DEFAULT_STR
|
||||
var format: String = DEFAULT_STR
|
||||
var fps: Int = -1
|
||||
var bitRate: Int = -1
|
||||
var socket: String = DEFAULT_STR
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package dji.sampleV5.aircraft.data.source
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.network.DJIHttpCallback
|
||||
import dji.v5.network.DJIHttpRequest
|
||||
import dji.v5.network.DJIHttpResponse
|
||||
import dji.v5.network.DJINetworkManager
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.JsonUtil
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 该Repository负责简单的HTTP数据获取能力,以及数据存储和获取能力
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
class VersionInfoRepository {
|
||||
|
||||
|
||||
/**
|
||||
* 用于观察网路模块(DJINetworkManager)是否初始化完成,并不关心网络实际是否有效
|
||||
*/
|
||||
private val networkInitSubject: BehaviorSubject<Boolean> = BehaviorSubject.create()
|
||||
|
||||
private var hostConfig: HostConfig? = null
|
||||
|
||||
private var currentVersionInfo: VersionInfo? = null
|
||||
|
||||
init {
|
||||
Completable.create { emitter ->
|
||||
DJINetworkManager.getInstance().addNetworkStatusListener {
|
||||
if (it) {
|
||||
emitter.onComplete()
|
||||
}
|
||||
}
|
||||
}.mergeWith(Completable.fromAction {
|
||||
hostConfig = getHostConfig()
|
||||
}.observeOn(Schedulers.io())).subscribe {
|
||||
// 通知网络初始化完成
|
||||
networkInitSubject.onNext(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前版本信息
|
||||
*/
|
||||
fun fetchCurrentVersionInfo(versionName: String, language: String = getLanguageHeaderValue()): Single<VersionInfo> {
|
||||
val versionNameEx = getCurrentVersionExTest().ifEmpty { versionName }
|
||||
LogUtils.i(LOG_TAG, "getCurrentVersionInfo, $versionNameEx")
|
||||
|
||||
val fileName = "version_info_${versionNameEx}_${language}.json"
|
||||
return Single.create<VersionInfo> {
|
||||
if (currentVersionInfo == null) {
|
||||
val currentVersionInfo = loadVersionInfoFromLocal(fileName)
|
||||
this.currentVersionInfo = currentVersionInfo
|
||||
it.onSuccess(currentVersionInfo)
|
||||
return@create
|
||||
}
|
||||
it.onError(VersionInfoCacheFileNotFound())
|
||||
}.onErrorResumeNext { fetchVersionInfoAndSave(versionNameEx, fileName, false, language) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制拉去最新版本信息
|
||||
*/
|
||||
fun fetchLatestVersionInfo(versionName: String, language: String = getLanguageHeaderValue()): Observable<VersionInfo> {
|
||||
return Observable.create { emitter ->
|
||||
val fileName = "${FILE_NAME_LATEST_VERSION_INFO_CACHE}_${language}.json"
|
||||
// 1. 从本地文件加载并通知给客户端,用于尽快刷新UI
|
||||
Schedulers.io().scheduleDirect {
|
||||
try {
|
||||
val versionInfo = loadVersionInfoFromLocal(fileName)
|
||||
emitter.onNext(versionInfo)
|
||||
} catch (throwable: Throwable) {
|
||||
//忽略
|
||||
LogUtils.i(LOG_TAG, "error, $throwable")
|
||||
}
|
||||
}
|
||||
// 2. 从云端获取最新数据,需要则通知给客户端
|
||||
val fetchD = fetchVersionInfoAndSave(versionName, fileName, true, language)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
emitter.onNext(it)
|
||||
emitter.onComplete()
|
||||
}, {
|
||||
if (emitter.isDisposed) {
|
||||
return@subscribe
|
||||
}
|
||||
emitter.onError(it)
|
||||
})
|
||||
// 3. 请求被取消时取消云端请求
|
||||
emitter.setCancellable {
|
||||
fetchD.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchVersionInfoAndSave(
|
||||
versionName: String,
|
||||
fileNameForCache: String,
|
||||
isLatest: Boolean,
|
||||
language: String,
|
||||
): Single<VersionInfo> {
|
||||
return fetchVersionInfoFromRemote(versionName, isLatest, language)
|
||||
.delaySubscription(networkInitSubject.filter {
|
||||
it
|
||||
}.firstOrError())
|
||||
.observeOn(Schedulers.io())
|
||||
.doOnSuccess {
|
||||
saveVersionInfoToLocal(fileNameForCache, it)
|
||||
currentVersionInfo = it
|
||||
}
|
||||
.onErrorResumeNext { Single.error(FetchVersionInfoError()) }
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
private fun loadVersionInfoFromLocal(fileName: String): VersionInfo {
|
||||
// 获取文件
|
||||
val versionInfoFile = ContextUtil.getContext().getFileStreamPath(fileName)
|
||||
|
||||
// 判断本地是否有该文件,加载本地文件
|
||||
if (!versionInfoFile.exists()) {
|
||||
// 不存在,返回空列表
|
||||
throw VersionInfoCacheFileNotFound()
|
||||
}
|
||||
|
||||
// 存在,则加载本地文件
|
||||
return JsonUtil.toBean(versionInfoFile, VersionInfo::class.java) ?: throw VersionInfoCacheFileNotFound()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@Synchronized
|
||||
private fun saveVersionInfoToLocal(fileName: String, versionInfoList: VersionInfo) {
|
||||
// 获取文件
|
||||
val versionInfoFile = ContextUtil.getContext().getFileStreamPath(fileName)
|
||||
|
||||
// 重新创建文件
|
||||
FileUtils.delFile(versionInfoFile, false)
|
||||
FileUtils.createFile(versionInfoFile)
|
||||
|
||||
// 将版本信息写入文件
|
||||
versionInfoFile.writer().use {
|
||||
it.write(JsonUtil.toJson(versionInfoList) ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从云端拉取版本信息
|
||||
* @param versionName 当前版本信息
|
||||
* @param isLatest true:拉取当前大版本(major version)的最新版本信息;false:拉取versionName所指定版本信息(可能拉取到空数据)
|
||||
*/
|
||||
private fun fetchVersionInfoFromRemote(versionName: String, isLatest: Boolean, l: String): Single<VersionInfo> {
|
||||
return Single.create {
|
||||
DJINetworkManager.getInstance().enqueue(buildHttpRequestExTest(versionName, isLatest, l), object : DJIHttpCallback<DJIHttpResponse> {
|
||||
override fun onFailure(error: IDJIError) {
|
||||
LogUtils.i(LOG_TAG, "error, ${error.errorCode()} ${error.errorType()} ${error.description()}")
|
||||
it.onError(FetchVersionInfoError())
|
||||
}
|
||||
|
||||
override fun onResponse(response: DJIHttpResponse) {
|
||||
LogUtils.i(LOG_TAG, "success, ${response.code()}")
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
it.onError(FetchVersionInfoError())
|
||||
return
|
||||
}
|
||||
val httpResult = HttpResult.fromJson(response.body())
|
||||
if (httpResult == null) {
|
||||
it.onError(FetchVersionInfoError())
|
||||
return
|
||||
}
|
||||
LogUtils.i(LOG_TAG, "success, $httpResult")
|
||||
it.onSuccess(httpResult.data)
|
||||
}
|
||||
|
||||
override fun onLoading(current: Long, total: Long) {
|
||||
// super.onLoading(current, total)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据入参构建不同的请求体
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun buildHttpRequest(host: String, versionName: String, isLatest: Boolean, language: String): DJIHttpRequest {
|
||||
val versionSplits = versionName.split(".").toMutableList()
|
||||
|
||||
// 检查VersionName是否是三段式版本号,不是则补齐
|
||||
val diff = 3 - versionSplits.size
|
||||
for (i in 0 until diff) {
|
||||
versionSplits.add("0")
|
||||
}
|
||||
|
||||
val httpRequest = DJIHttpRequest.Builder.newBuilder()
|
||||
.requestType(DJIHttpRequest.RequestType.GET)
|
||||
.url(
|
||||
if (isLatest) {
|
||||
"$host$URL_PATH_TCH_LATEPATHST_VERSION_INFO"
|
||||
} else {
|
||||
"$host$URL_PATH_FETCH_CURRENT_VERSION_INFO"
|
||||
}
|
||||
)
|
||||
.params(
|
||||
if (isLatest) {
|
||||
mapOf(
|
||||
"sdk_name" to "mobile sdk",
|
||||
"platform" to "android",
|
||||
"major_version" to versionSplits[0],
|
||||
)
|
||||
} else {
|
||||
mapOf(
|
||||
"sdk_name" to "mobile sdk",
|
||||
"platform" to "android",
|
||||
"major_version" to versionSplits[0],
|
||||
"minor_version" to versionSplits[1],
|
||||
"patch_version" to versionSplits[2],
|
||||
)
|
||||
}
|
||||
)
|
||||
.headers(mapOf("Language" to language, "content-type" to "application/json"))
|
||||
.build()
|
||||
LogUtils.i(LOG_TAG, "request, $httpRequest")
|
||||
return httpRequest
|
||||
}
|
||||
|
||||
private fun getHostConfig(): HostConfig? {
|
||||
val hostConfigFile = getHostConfigFile() ?: return null
|
||||
JsonUtil.toBean(hostConfigFile, HostConfig::class.java)?.let {
|
||||
// 做一个简单的URL校验
|
||||
if (it.host.endsWith('/') && it.host.startsWith("https://")) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getHostConfigFile(): File? {
|
||||
val dir = ContextUtil.getContext().getExternalFilesDir("")
|
||||
val configFile = File(dir, FILE_NAME_HOST_VERSION_CONFIG)
|
||||
if (configFile.exists()) {
|
||||
return configFile
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun buildHttpRequestExTest(versionName: String, isLatest: Boolean, language: String): DJIHttpRequest {
|
||||
hostConfig?.let {
|
||||
return buildHttpRequest(it.host, it.versionName.ifEmpty { versionName }, isLatest, language)
|
||||
}
|
||||
return buildHttpRequest(URL_HOST, versionName, isLatest, language)
|
||||
}
|
||||
|
||||
/**
|
||||
* 目前MSDK只有中英文,经测试发现
|
||||
* - string.xml 只有在系统设置为简体中文(国家无所谓)时才会选择中文的string.xml
|
||||
* 所以该处添加两项判断
|
||||
*/
|
||||
private fun getLanguageHeaderValue(): String {
|
||||
//"ZH" means Chinese
|
||||
val currentLocale = Locale.getDefault()
|
||||
val country: String = currentLocale.country.lowercase(Locale.ROOT)
|
||||
LogUtils.i(LOG_TAG,currentLocale.language,currentLocale.script)
|
||||
return if ("zh".equals(currentLocale.language, true) && "cn".equals(country)) {
|
||||
"cn"
|
||||
} else {
|
||||
"en"
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentVersionExTest(): String {
|
||||
return hostConfig?.versionName ?: ""
|
||||
}
|
||||
|
||||
class VersionInfoCacheFileNotFound : Exception()
|
||||
class FetchVersionInfoError : Exception()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 服务器host
|
||||
*/
|
||||
private const val URL_HOST = "https://dev.dji.com/"
|
||||
|
||||
/**
|
||||
* 获取最新版本信息
|
||||
*/
|
||||
private const val URL_PATH_TCH_LATEPATHST_VERSION_INFO = "api/v1/release/latest"
|
||||
|
||||
/**
|
||||
* 获取指定版本信息
|
||||
*/
|
||||
private const val URL_PATH_FETCH_CURRENT_VERSION_INFO = "api/v1/release"
|
||||
|
||||
/**
|
||||
* 用于存储最新版本信息的文件
|
||||
*/
|
||||
private const val FILE_NAME_LATEST_VERSION_INFO_CACHE = "version_info_latest"
|
||||
|
||||
/**
|
||||
* 提供给测试使用的配置文件路径
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
private const val FILE_NAME_HOST_VERSION_CONFIG = "config_host_version.json"
|
||||
private const val LOG_TAG = "VersionInfoRepository"
|
||||
}
|
||||
}
|
||||
|
||||
private data class HostConfig(
|
||||
@SerializedName("host")
|
||||
val host: String = "",
|
||||
@SerializedName("version")
|
||||
val versionName: String = "",
|
||||
)
|
||||
|
||||
data class HttpResult(val code: Int, val message: String, val data: VersionInfo) {
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String): HttpResult? {
|
||||
return JsonUtil.toBean(json, HttpResult::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "HttpResult(code=$code, message='$message', data='$data')"
|
||||
}
|
||||
}
|
||||
|
||||
data class VersionInfo(
|
||||
val versionCode: Int = 0,
|
||||
@SerializedName("MajorVersion")
|
||||
val versionMajor: String = "",
|
||||
@SerializedName("MinorVersion")
|
||||
val versionMinor: String = "",
|
||||
@SerializedName("PatchVersion")
|
||||
val versionPatch: String = "",
|
||||
|
||||
/**
|
||||
* UTC时间戳(单位s)
|
||||
*/
|
||||
@SerializedName("ReleaseDate")
|
||||
val releaseTimeStamp: Long = 0,
|
||||
@SerializedName("SupportedProducts")
|
||||
val supportProducts: String = "",
|
||||
@SerializedName("Highlights")
|
||||
val releaseNode: String = "",
|
||||
) {
|
||||
val versionName: String
|
||||
get() {
|
||||
return "$versionMajor.$versionMinor.$versionPatch"
|
||||
}
|
||||
}
|
||||
10
sample/src/main/java/dji/sampleV5/aircraft/data/utils.kt
Normal file
10
sample/src/main/java/dji/sampleV5/aircraft/data/utils.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package dji.sampleV5.aircraft.data
|
||||
|
||||
const val DEFAULT_STR = "N/A"
|
||||
const val DEFAULT_RES_ID = -1
|
||||
const val IN_INNER_NETWORK_STR = "IN_INNER"
|
||||
const val IN_OUT_NETWORK_STR = "IN_OUT"
|
||||
const val NO_NETWORK_STR = "NO_NETWORK"
|
||||
const val ONLINE_STR = "ONLINE"
|
||||
const val MAIN_FRAGMENT_PAGE_TITLE = "MAIN_FRAGMENT_PAGE_TITLE"
|
||||
const val MEDIA_FILE_DETAILS_STR = "MEDIA_FILE_DETAILS"
|
||||
@@ -0,0 +1,173 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
|
||||
import dji.sdk.keyvalue.key.ComponentType
|
||||
import dji.v5.manager.capability.CapabilityManager
|
||||
import dji.v5.manager.capability.CapabilityParser
|
||||
import dji.v5.utils.common.*
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.CompletableObserver
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import java.lang.Exception
|
||||
import java.lang.StringBuilder
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/09/13 5:43 下午
|
||||
* @description: 能力集key 测试,结果中只展示未通过的key
|
||||
* 结果存储在包目录/keycheck/result.txt
|
||||
*/
|
||||
object CapabilityKeyChecker {
|
||||
|
||||
|
||||
private val TAG = LogUtils.getTag(this)
|
||||
|
||||
|
||||
/**
|
||||
* 根据key名称从能力集获取测试用例json集合 每个对象可反序列化为key的参数对象
|
||||
*/
|
||||
fun getKeyParamList(keyName: String): MutableList<String> {
|
||||
return CapabilityParser.getInstance().getValueParamList(keyName)
|
||||
}
|
||||
|
||||
|
||||
fun getKeyItem(keyName: String): KeyItem<*, *>? {
|
||||
val allList: MutableList<KeyItem<*, *>> = ArrayList()
|
||||
var item: KeyItem<*, *>? = null
|
||||
KeyItemDataUtil.getAllKeyList(allList)
|
||||
allList.forEach {
|
||||
if (it.toString() == keyName) {
|
||||
item = it;
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
data class ItemDecoder(
|
||||
var componetIndex: Int = 0, // LEFT_OR_MAIN
|
||||
var subComponetType: Int = 65534, // DEFAULT
|
||||
var subComponetIndex: Int = 0,
|
||||
var jsonString: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取枚举对应的Json list字符串
|
||||
* eg:CameraMode 对象 obj cameramodeMsg field[obj] CameraMode
|
||||
*/
|
||||
fun getDJIValueBeanStr(item: KeyItem<*, *>): String {
|
||||
var tagBegin = "{\"valueParamList\": ["
|
||||
var tagEnd = "]}"
|
||||
try {
|
||||
val pFields = item.param?.javaClass?.declaredFields
|
||||
if (pFields != null) {
|
||||
for (field in pFields) {
|
||||
|
||||
field.isAccessible = true
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
val itemList =
|
||||
(item.subItemMap as Map<String?, List<EnumItem>>)[clazz.canonicalName]!!
|
||||
var jsonList = StringBuilder(tagBegin)
|
||||
itemList.forEach {
|
||||
field[item.param] = KeyItemHelper.getEnumData(
|
||||
clazz as Class<Enum<*>>,
|
||||
it.getName().toString()
|
||||
)
|
||||
var jsonString =
|
||||
"\"" + item.param.toString().replace("\"", "\\\"") + "\""
|
||||
|
||||
var result = jsonString + ","
|
||||
if (!result.contains("65535")) {//过滤unknown
|
||||
jsonList.append(result)
|
||||
}
|
||||
}
|
||||
jsonList.deleteAt(jsonList.lastIndex)
|
||||
jsonList.append(tagEnd)
|
||||
return jsonList.toString()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(KeyItemHelper.TAG, e.message)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据item生成 枚举类型的json文件
|
||||
*/
|
||||
fun generateAllEnumList(productType: String) {
|
||||
val allList: MutableList<KeyItem<*, *>> = ArrayList()
|
||||
DJIExecutor.getExecutorFor(DJIExecutor.Purpose.IO).execute {
|
||||
KeyItemDataUtil.getAllKeyList(allList)
|
||||
allList
|
||||
.filter {
|
||||
val keyName = "Key$it"
|
||||
it.canSet()
|
||||
&& CapabilityManager.getInstance().isKeySupported(
|
||||
productType,
|
||||
"",
|
||||
ComponentType.find(it.getKeyInfo().componentType),
|
||||
keyName
|
||||
)
|
||||
|
||||
}
|
||||
.forEach() { item ->
|
||||
val jsonStr = getDJIValueBeanStr(item)
|
||||
if (jsonStr.isNotEmpty()) {
|
||||
var filePath = DiskUtil.getExternalCacheDirPath(
|
||||
ContextUtil.getContext(),
|
||||
"keycheck/$item.json"
|
||||
)
|
||||
FileUtils.writeFile(filePath, jsonStr, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkOneType(
|
||||
productType: String,
|
||||
componentTypeName: String,
|
||||
keyCheckType: KeyCheckType,
|
||||
componentIndex: Int
|
||||
): Completable {
|
||||
|
||||
var keyOperatorCommand = when (keyCheckType) {
|
||||
KeyCheckType.SET -> KeySetCommand(productType, componentTypeName, componentIndex)
|
||||
KeyCheckType.ACTION -> KeyActionCommand(productType, componentTypeName, componentIndex)
|
||||
KeyCheckType.GET -> KeyGetCommand(productType, componentTypeName, componentIndex)
|
||||
}
|
||||
return keyOperatorCommand.execute()
|
||||
}
|
||||
|
||||
fun check(
|
||||
productType: String,
|
||||
componentTypeName: String,
|
||||
componentIndex: Int
|
||||
) {
|
||||
checkOneType(productType, componentTypeName, KeyCheckType.SET, componentIndex)
|
||||
.andThen(checkOneType(productType, componentTypeName, KeyCheckType.SET, componentIndex))
|
||||
.andThen(checkOneType(productType, componentTypeName, KeyCheckType.ACTION, componentIndex))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(object : CompletableObserver {
|
||||
override fun onSubscribe(d: Disposable) {
|
||||
LogUtils.e(TAG, "begin check")
|
||||
ToastUtils.showToast("begin check")
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
LogUtils.e(TAG, "-check finish-")
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
LogUtils.e(TAG, "check error${e.message}")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/11 9:42 上午
|
||||
* @description:
|
||||
*/
|
||||
enum class ChannelType(name: String) {
|
||||
/**
|
||||
* 电池
|
||||
*/
|
||||
CHANNEL_TYPE_BATTERY("BATTERY"),
|
||||
|
||||
/**
|
||||
* 云台
|
||||
*/
|
||||
CHANNEL_TYPE_GIMBAL("GIMBAL"),
|
||||
|
||||
/**
|
||||
* 相机
|
||||
*/
|
||||
CHANNEL_TYPE_CAMERA("CAMERA"),
|
||||
|
||||
|
||||
/**
|
||||
* Airlink
|
||||
*/
|
||||
CHANNEL_TYPE_AIRLINK("AIRLINK"),
|
||||
|
||||
/**
|
||||
* Flight Assistant
|
||||
*/
|
||||
CHANNEL_TYPE_FLIGHT_ASSISTANT("ASSISTANT"),
|
||||
|
||||
/**
|
||||
* Flight Control
|
||||
*/
|
||||
CHANNEL_TYPE_FLIGHT_CONTROL("FLIGHT CONTROL"),
|
||||
|
||||
/**
|
||||
* Remote Controller
|
||||
*/
|
||||
CHANNEL_TYPE_REMOTE_CONTROLLER("REMOTE CONTROLLER"),
|
||||
|
||||
/**
|
||||
* BLE
|
||||
*/
|
||||
CHANNEL_TYPE_BLE("BLE"),
|
||||
|
||||
/**
|
||||
* RTK
|
||||
*/
|
||||
CHANNEL_TYPE_RTK_BASE_STATION("RTK BASE STATION"),
|
||||
|
||||
/**
|
||||
* RTK
|
||||
*/
|
||||
CHANNEL_TYPE_RTK_MOBILE_STATION("RTK MOBILE STATION"),
|
||||
|
||||
/**
|
||||
* Product
|
||||
*/
|
||||
CHANNEL_TYPE_PRODUCT("PRODUCT"),
|
||||
|
||||
/**
|
||||
* OcuSync
|
||||
*/
|
||||
CHANNEL_TYPE_OCU_SYNC("OCU SYNC"),
|
||||
|
||||
/**
|
||||
* Radar
|
||||
*/
|
||||
CHANNEL_TYPE_RADAR("RADAR"),
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Mobile Network
|
||||
*/
|
||||
CHANNEL_TYPE_MOBILE_NETWORK("MOBILE NETWORK"),
|
||||
|
||||
|
||||
/**
|
||||
* on board
|
||||
*/
|
||||
CHANNEL_TYPE_ON_BOARD("BOARD"),
|
||||
|
||||
/**
|
||||
* Payload
|
||||
*/
|
||||
CHANNEL_TYPE_ON_PAYLOAD("PAYLOAD"),
|
||||
|
||||
/**
|
||||
* lidar
|
||||
*/
|
||||
CHANNEL_TYPE_LIDAR("LIDAR"),
|
||||
|
||||
/**
|
||||
* IntelligentBox
|
||||
*/
|
||||
INTELLIGENT_BOX("INTELLIGENT BOX");
|
||||
|
||||
private val value: String
|
||||
override fun toString(): String {
|
||||
return value
|
||||
}
|
||||
|
||||
init {
|
||||
value = name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/10 9:58 上午
|
||||
* @description:
|
||||
*/
|
||||
class EnumItem : Serializable {
|
||||
/**
|
||||
* 需要显示的条目描述
|
||||
*/
|
||||
private var description: String? = null
|
||||
|
||||
/**
|
||||
* SDK对应的枚举
|
||||
*/
|
||||
private var name: String? = null
|
||||
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
private var selected = false
|
||||
fun getName(): String? {
|
||||
return name
|
||||
}
|
||||
|
||||
fun setName(name: String?) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun getDescription(): String? {
|
||||
return description
|
||||
}
|
||||
|
||||
fun setDescription(description: String?) {
|
||||
this.description = description
|
||||
}
|
||||
|
||||
fun isSelected(): Boolean {
|
||||
return selected
|
||||
}
|
||||
|
||||
fun setSelected(selected: Boolean) {
|
||||
this.selected = selected
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 876323262645176354L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:25 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeyActionCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) : KeyOperatorCommand(productType, componentTypeName, componentIndex) {
|
||||
|
||||
private val TAG_GET = "【ACTION】"
|
||||
private val TAG_ERROR = "ActionErrorMsg"
|
||||
|
||||
override fun filter(item: KeyItem<*, *>): Boolean {
|
||||
return item.canAction()
|
||||
}
|
||||
|
||||
override fun run(item: KeyItem<*, *>) {
|
||||
super.doKeyParam(item, KeyCheckType.ACTION)
|
||||
}
|
||||
|
||||
override fun getTAG(): String {
|
||||
return TAG_GET
|
||||
}
|
||||
|
||||
override fun getErrorTAG(): String {
|
||||
return TAG_ERROR
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import dji.sdk.keyvalue.key.DJIActionKeyInfo;
|
||||
import dji.sdk.keyvalue.key.DJIKey;
|
||||
import dji.sdk.keyvalue.key.DJIKeyInfo;
|
||||
import dji.sdk.keyvalue.key.KeyTools;
|
||||
import dji.v5.common.callback.CommonCallbacks;
|
||||
import dji.v5.manager.KeyManager;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
|
||||
public class KeyBaseStructure<P, R> {
|
||||
|
||||
private static final String TAG = KeyBaseStructure.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* 设置参数
|
||||
*/
|
||||
protected P param;
|
||||
|
||||
/**
|
||||
* 返回结果
|
||||
*/
|
||||
protected R result;
|
||||
|
||||
/**
|
||||
* 推送数据记录
|
||||
*/
|
||||
protected String listenRecord = "";
|
||||
|
||||
/**
|
||||
* 推送Listener宿主
|
||||
*/
|
||||
protected Object listenHolder;
|
||||
|
||||
public int getComponetIndex() {
|
||||
return componetIndex;
|
||||
}
|
||||
|
||||
public void setComponetIndex(int componetIndex) {
|
||||
this.componetIndex = componetIndex;
|
||||
}
|
||||
|
||||
public int getSubComponetType() {
|
||||
return subComponetType;
|
||||
}
|
||||
|
||||
public void setSubComponetType(int subComponetType) {
|
||||
this.subComponetType = subComponetType;
|
||||
}
|
||||
|
||||
public int getSubComponetIndex() {
|
||||
return subComponetIndex;
|
||||
}
|
||||
|
||||
public void setSubComponetIndex(int subComponetIndex) {
|
||||
this.subComponetIndex = subComponetIndex;
|
||||
}
|
||||
|
||||
protected int componetIndex = -1;
|
||||
|
||||
protected int subComponetType = -1;
|
||||
|
||||
protected int subComponetIndex = -1;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举列表
|
||||
*/
|
||||
protected Map<String, List<EnumItem>> subItemMap = new HashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* 通过反射获取泛型类型数据并实例化
|
||||
*/
|
||||
protected void initGenericInstance() {
|
||||
try {
|
||||
KeyItemHelper.INSTANCE.initClassData(param);
|
||||
KeyItemHelper.INSTANCE.initClassData(result);
|
||||
initSubItemData();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果为枚举或者枚举嵌套列表,则初始化该列表数据
|
||||
*/
|
||||
protected void initSubItemData() {
|
||||
if (param == null) {
|
||||
return;
|
||||
}
|
||||
subItemMap.clear();
|
||||
subItemMap.putAll(KeyItemHelper.INSTANCE.initSubItemData(param));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取需要设置的参数实例
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public P getParam() {
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数映射列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, List<EnumItem>> getSubItemMap() {
|
||||
return subItemMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推送数据记录
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getListenRecord() {
|
||||
return listenRecord;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 异步get
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param getCallback
|
||||
*/
|
||||
protected void get(DJIKeyInfo<R> keyInfo, CommonCallbacks.CompletionCallbackWithParam<R> getCallback) {
|
||||
DJIKey<R> key = createKey(keyInfo);
|
||||
KeyManager.getInstance().getValue(key, getCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步get
|
||||
*
|
||||
* @param keyInfo
|
||||
* @return
|
||||
*/
|
||||
protected R get(DJIKeyInfo<R> keyInfo) {
|
||||
DJIKey<R> key = createKey(keyInfo);
|
||||
return KeyManager.getInstance().getValue(key);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param setCallback
|
||||
*/
|
||||
protected void set(DJIKeyInfo<P> keyInfo, P param, CommonCallbacks.CompletionCallback setCallback) {
|
||||
|
||||
DJIKey<P> key = createKey(keyInfo);
|
||||
KeyManager.getInstance().setValue(key, param, setCallback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 设置Listener
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param listenHolder
|
||||
* @param listenCallback
|
||||
*/
|
||||
protected void listen(DJIKeyInfo<R> keyInfo, Object listenHolder, CommonCallbacks.KeyListener<R> listenCallback) {
|
||||
this.listenHolder = listenHolder;
|
||||
|
||||
DJIKey<R> key = createKey(keyInfo);
|
||||
KeyManager.getInstance().listen(key, listenHolder, listenCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消Listener
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param listenHolder
|
||||
*/
|
||||
protected void cancelListen(DJIKeyInfo<R> keyInfo, Object listenHolder) {
|
||||
|
||||
KeyManager.getInstance().cancelListen(createKey(keyInfo), listenHolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* 带参action
|
||||
*
|
||||
* @param keyInfo
|
||||
* @param param
|
||||
* @param actonCallback
|
||||
*/
|
||||
protected void action(DJIActionKeyInfo<P, R> keyInfo, P param, CommonCallbacks.CompletionCallbackWithParam<R> actonCallback) {
|
||||
|
||||
DJIKey.ActionKey<P,R> key = createActionKey(keyInfo);
|
||||
KeyManager.getInstance().performAction(key, param, actonCallback);
|
||||
}
|
||||
|
||||
protected DJIKey.ActionKey<P,R> createActionKey(DJIActionKeyInfo<P,R> keyInfo) {
|
||||
DJIKey.ActionKey<P,R> key = null;
|
||||
key = KeyTools.createKey(keyInfo, 0, getComponetIndex(),getSubComponetType(), getSubComponetIndex());
|
||||
return key;
|
||||
}
|
||||
|
||||
protected<Parame> DJIKey<Parame> createKey(DJIKeyInfo<Parame> keyInfo ) {
|
||||
DJIKey<Parame> key = null;
|
||||
key = KeyTools.createKey(keyInfo, 0 , getComponetIndex(),getSubComponetType(), getSubComponetIndex());
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:09 下午
|
||||
* @description:
|
||||
*/
|
||||
enum class KeyCheckType {
|
||||
GET,
|
||||
SET,
|
||||
ACTION
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:25 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeyGetCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) : KeyOperatorCommand(productType, componentTypeName, componentIndex) {
|
||||
|
||||
private val TAG_GET = "【GET】"
|
||||
private val TAG_ERROR = "GetErrorMsg"
|
||||
|
||||
override fun filter(item: KeyItem<*, *>): Boolean {
|
||||
return item.canGet()
|
||||
}
|
||||
|
||||
override fun run(item: KeyItem<*, *>) {
|
||||
super.doKeyParam(item, KeyCheckType.GET)
|
||||
}
|
||||
|
||||
override fun getTAG(): String {
|
||||
return TAG_GET
|
||||
}
|
||||
|
||||
override fun getErrorTAG(): String {
|
||||
return TAG_ERROR
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
415
sample/src/main/java/dji/sampleV5/aircraft/keyvalue/KeyItem.java
Normal file
415
sample/src/main/java/dji/sampleV5/aircraft/keyvalue/KeyItem.java
Normal file
@@ -0,0 +1,415 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import dji.v5.ux.core.util.ToastUtils;
|
||||
import dji.sampleV5.aircraft.util.Util;
|
||||
import dji.sdk.keyvalue.converter.DJIValueConverter;
|
||||
import dji.sdk.keyvalue.converter.EmptyValueConverter;
|
||||
import dji.sdk.keyvalue.converter.SingleValueConverter;
|
||||
import dji.sdk.keyvalue.key.DJIActionKeyInfo;
|
||||
import dji.sdk.keyvalue.key.DJIKeyInfo;
|
||||
import dji.sdk.keyvalue.value.base.DJIValue;
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg;
|
||||
import dji.v5.common.callback.CommonCallbacks;
|
||||
import dji.v5.common.error.IDJIError;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* KeyItem作为key能力和动作的载体来进行封装
|
||||
*/
|
||||
|
||||
public class KeyItem<P, R> extends KeyBaseStructure<P , R> implements Comparable<KeyItem<?,?>>{
|
||||
|
||||
private static final String TAG = KeyItem.class.getSimpleName();
|
||||
public KeyItem(DJIKeyInfo<?> keyInfo) {
|
||||
super();
|
||||
this.keyInfo = (DJIKeyInfo<R>)keyInfo;
|
||||
this.keyInfoSet = (DJIKeyInfo<P>)keyInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 属性展示名
|
||||
*/
|
||||
protected String name;
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(long count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用次数,用户排序
|
||||
*/
|
||||
private long count;
|
||||
public boolean isSingleDJIValue;
|
||||
|
||||
/**
|
||||
* 参数key的能力携带实体
|
||||
*/
|
||||
protected DJIKeyInfo<R> keyInfo;
|
||||
|
||||
protected DJIKeyInfo<P> keyInfoSet;
|
||||
|
||||
|
||||
/**
|
||||
* 需要调用者注入的回调接口,用于结果通知
|
||||
*/
|
||||
protected KeyItemActionListener<Object> keyOperateCallBack;
|
||||
private boolean isItemSelected ;
|
||||
public boolean isItemSelected() {
|
||||
return isItemSelected;
|
||||
}
|
||||
|
||||
public void setItemSelected(boolean itemSelected) {
|
||||
isItemSelected = itemSelected;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 推送数据回调(需要调用者注入)
|
||||
*/
|
||||
protected KeyItemActionListener<String> pushCallBack;
|
||||
|
||||
public String getName() {
|
||||
return Util.isBlank(name) ? keyInfo.getIdentifier() : name;
|
||||
}
|
||||
public void setName(String name){
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public DJIKeyInfo<P> getKeyInfo() {
|
||||
return keyInfoSet;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取listen宿主
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Object getListenHolder() {
|
||||
return listenHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以Get
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canGet() {
|
||||
return keyInfo.isCanGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以Set
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canSet() {
|
||||
return keyInfo.isCanSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可以Listen
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canListen() {
|
||||
return keyInfo.isCanListen();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为action
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean canAction() {
|
||||
return keyInfo.isCanPerformAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要调用者注入的回调接口,用于结果通知
|
||||
*
|
||||
* @param keyOperateCallBack
|
||||
*/
|
||||
public void setKeyOperateCallBack(KeyItemActionListener<Object> keyOperateCallBack) {
|
||||
this.keyOperateCallBack = keyOperateCallBack;
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送回调
|
||||
*
|
||||
* @param pushCallBack
|
||||
*/
|
||||
public void setPushCallBack(KeyItemActionListener<String> pushCallBack) {
|
||||
this.pushCallBack = pushCallBack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get请求
|
||||
*/
|
||||
public void doGet() {
|
||||
try {
|
||||
|
||||
get(keyInfo, new CommonCallbacks.CompletionCallbackWithParam<R>() {
|
||||
@Override
|
||||
public void onSuccess(R data) {
|
||||
|
||||
if (keyOperateCallBack != null && data != null) {
|
||||
keyOperateCallBack.actionChange(getName()+"【GET】 == success " + data.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull IDJIError error) {
|
||||
if (keyOperateCallBack != null) {
|
||||
keyOperateCallBack.actionChange(getName() + "【GET】 GetErrorMsg ==" + error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SyncGet请求
|
||||
*/
|
||||
public void doSyncGet() {
|
||||
try {
|
||||
DJIValue getResult = (DJIValue) get(keyInfo);
|
||||
if(keyOperateCallBack != null){
|
||||
keyOperateCallBack.actionChange(null == getResult ? "" : getResult.toJson());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set请求
|
||||
*/
|
||||
public void doSet(String jsonStr) {
|
||||
try {
|
||||
final P p = validPrams(jsonStr);
|
||||
if (p == null) {
|
||||
keyOperateCallBack.actionChange(getName() + "【SET】SetErrorMsg== json error");
|
||||
return;
|
||||
}
|
||||
|
||||
set(keyInfoSet, p, new CommonCallbacks.CompletionCallback() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
LogUtils.e(TAG, "set success : " + getName());
|
||||
if (keyOperateCallBack != null) {
|
||||
// 保存上一次设置成功的对象,下次set时可使用保存过的对象 序列化json
|
||||
if (getKeyInfo().getTypeConverter() instanceof DJIValueConverter) {
|
||||
param = p;
|
||||
}
|
||||
keyOperateCallBack.actionChange(getName() + "【SET】==" + p.toString() + " | " + " success");
|
||||
}
|
||||
ToastUtils.INSTANCE.showToast("set " + p.getClass().getSimpleName() + " success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull IDJIError error) {
|
||||
LogUtils.e(TAG, "set error : " + error);
|
||||
if (keyOperateCallBack != null) {
|
||||
keyOperateCallBack.actionChange(getName() + "【SET】SetErrorMsg== " + p.toString() + "|" + error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
ToastUtils.INSTANCE.showToast("输入参数出错");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* action请求
|
||||
*/
|
||||
public void doAction(String jsonStr) {
|
||||
|
||||
DJIActionKeyInfo<P,R> actionKeyInfo = (DJIActionKeyInfo<P,R>) keyInfo;
|
||||
P p = null;
|
||||
if(jsonStr != null && !jsonStr.isEmpty()){
|
||||
p = validPrams(jsonStr);
|
||||
}
|
||||
if (p == null && actionKeyInfo.getTypeConverter()!= EmptyValueConverter.converter) {
|
||||
return;
|
||||
}
|
||||
|
||||
P pRes = p;
|
||||
action(actionKeyInfo, p, new CommonCallbacks.CompletionCallbackWithParam<R>() {
|
||||
@Override
|
||||
public void onSuccess(Object data) {
|
||||
if (keyOperateCallBack != null) {
|
||||
if (data != null && !(data instanceof EmptyMsg)) {
|
||||
keyOperateCallBack.actionChange(getName() + "【ACTION】== " + getActionTipsStr(pRes) + " success: " + data.toString());
|
||||
} else {
|
||||
keyOperateCallBack.actionChange(getName() + "【ACTION】== " + getActionTipsStr(pRes) + " result: success");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull IDJIError error) {
|
||||
if (keyOperateCallBack != null) {
|
||||
keyOperateCallBack.actionChange(getName() +"【ACTION】 ActionErrorMsg==" + error.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getActionTipsStr(P pRes){
|
||||
return pRes == null ? "" : pRes.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送回调
|
||||
*/
|
||||
private CommonCallbacks.KeyListener<R> listenSDKCallback =
|
||||
( oldValue ,newValue) -> {
|
||||
StringBuffer sb = new StringBuffer("【LISTEN】");
|
||||
sb.append(getName());
|
||||
sb.append(" result:");
|
||||
sb.append("oldValue:").append(oldValue);
|
||||
sb.append(" newValue:").append(newValue);
|
||||
|
||||
if (pushCallBack != null) {
|
||||
pushCallBack.actionChange(sb.toString());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 注册Listen(新接口)
|
||||
*
|
||||
* @param listenHolder
|
||||
*/
|
||||
public void listen(Object listenHolder) {
|
||||
listen(keyInfo, listenHolder, listenSDKCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消Listen(新接口)
|
||||
*
|
||||
* @param listenHolder
|
||||
*/
|
||||
public void cancelListen(Object listenHolder) {
|
||||
if (this.listenHolder == listenHolder) {
|
||||
this.listenHolder = null;
|
||||
cancelListen(keyInfo, listenHolder);
|
||||
pushCallBack = null;
|
||||
listenRecord = "";
|
||||
}
|
||||
//listenSDKCallback = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证参数
|
||||
*
|
||||
* @param jsonStr
|
||||
* @return
|
||||
*/
|
||||
private P validPrams(String jsonStr) {
|
||||
if (Util.isBlank(jsonStr)) {
|
||||
ToastUtils.INSTANCE.showToast("请先设置参数");
|
||||
return null;
|
||||
}
|
||||
final P p = buildParamFromJsonStr(jsonStr);
|
||||
if (p == null) {
|
||||
ToastUtils.INSTANCE.showToast("请先设置" + jsonStr + " 参数");
|
||||
return null;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化:根据JSON串获取对象
|
||||
*
|
||||
* @param jsonStr
|
||||
* @return
|
||||
*/
|
||||
public P buildParamFromJsonStr(String jsonStr) {
|
||||
P p;
|
||||
if (keyInfo.getTypeConverter() instanceof SingleValueConverter && !isSingleDJIValue) {
|
||||
p = (P) keyInfo.getTypeConverter().fromStr(getSingleJsonValue(jsonStr));
|
||||
} else {
|
||||
p = (P) keyInfo.getTypeConverter().fromStr(jsonStr);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SingleValue 中原始类型包装类的value值
|
||||
* @param jsonStr
|
||||
* @return
|
||||
*/
|
||||
private String getSingleJsonValue(String jsonStr) {
|
||||
String value = "";
|
||||
try {
|
||||
JSONObject jsonObj = new JSONObject(jsonStr);
|
||||
value = jsonObj.getString("value");
|
||||
}catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化:获取默认JSON串
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getParamJsonStr() {
|
||||
String jsonStr = null;
|
||||
try {
|
||||
jsonStr = param.toString();
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG ,e.getMessage());
|
||||
}
|
||||
return jsonStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除回调
|
||||
*/
|
||||
public void removeCallBack() {
|
||||
cancelListen(listenHolder);
|
||||
keyOperateCallBack = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(KeyItem keyItem) {
|
||||
if (keyItem.count - this.count > 0) {
|
||||
return 1;
|
||||
} else if (keyItem.count - this.count < 0) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isSingleDJIValue() {
|
||||
return isSingleDJIValue;
|
||||
}
|
||||
|
||||
public void setSingleDJIValue(boolean singleDJIValue) {
|
||||
isSingleDJIValue = singleDJIValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return Util.isBlank(name) ? keyInfo.getIdentifier() : name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/11 9:55 上午
|
||||
* @description:
|
||||
*/
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
*/
|
||||
public interface KeyItemActionListener<T> {
|
||||
|
||||
void actionChange(@Nullable T t);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.collection.SparseArrayCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import dji.sampleV5.aircraft.R;
|
||||
|
||||
|
||||
public class KeyItemAdapter extends RecyclerView.Adapter<KeyItemAdapter.ComViewHolder> implements Filterable {
|
||||
|
||||
private KeyItemActionListener<KeyItem<?, ?>> callback;
|
||||
|
||||
protected List<KeyItem<?, ?>> dataList;
|
||||
protected List<KeyItem<?, ?>> mFilterList;
|
||||
protected Context context;
|
||||
|
||||
|
||||
public KeyItemAdapter(Context context, List<KeyItem<?, ?>> dataList, KeyItemActionListener<KeyItem<?, ?>> callback) {
|
||||
this.context = context;
|
||||
this.dataList = dataList;
|
||||
this.mFilterList = dataList;
|
||||
this.callback = callback;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ComViewHolder(LayoutInflater.from(context).inflate(R.layout.item_camera_key_list, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ComViewHolder holder, int position) {
|
||||
if (mFilterList.size() <= position) {
|
||||
return;
|
||||
}
|
||||
convert(holder, mFilterList.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mFilterList == null ? 0 : mFilterList.size();
|
||||
}
|
||||
|
||||
|
||||
public void convert(ComViewHolder viewHolder, final KeyItem<?, ?> keyItem) {
|
||||
if (viewHolder == null || keyItem == null) {
|
||||
return;
|
||||
}
|
||||
TextView textView = viewHolder.getView(R.id.tv_item_name);
|
||||
textView.setText(keyItem.getName());
|
||||
if (keyItem.isItemSelected()) {
|
||||
textView.setBackgroundColor(Color.GRAY);
|
||||
} else {
|
||||
textView.setBackgroundColor(Color.TRANSPARENT);
|
||||
}
|
||||
textView.setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.actionChange(keyItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return new Filter() {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence charSequence) {
|
||||
String charString = charSequence.toString();
|
||||
if (charString.isEmpty()) {
|
||||
mFilterList = dataList;
|
||||
} else {
|
||||
List<KeyItem<?, ?>> filteredList = new ArrayList<>();
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0, length = charSequence.length(); i < length; i++) {
|
||||
if (stringBuilder.length() > 0) {
|
||||
stringBuilder.append("+.*");
|
||||
}
|
||||
stringBuilder.append(charSequence.charAt(i));
|
||||
}
|
||||
Pattern pattern = Pattern.compile(stringBuilder.toString(), Pattern.CASE_INSENSITIVE);
|
||||
for (KeyItem<?, ?> item : dataList) {
|
||||
if (pattern.matcher(item.keyInfo.getIdentifier()).find()) {
|
||||
filteredList.add(item);
|
||||
}
|
||||
|
||||
}
|
||||
mFilterList = filteredList;
|
||||
}
|
||||
|
||||
FilterResults filterResults = new FilterResults();
|
||||
filterResults.values = mFilterList;
|
||||
return filterResults;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
|
||||
mFilterList = (ArrayList<KeyItem<?, ?>>) filterResults.values;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 缓存容器
|
||||
*/
|
||||
public class ComViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private View convertView;
|
||||
private SparseArrayCompat<View> views;
|
||||
|
||||
public ComViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.convertView = itemView;
|
||||
this.views = new SparseArrayCompat<>();
|
||||
}
|
||||
|
||||
|
||||
public <T extends View> T getView(int layoutId) {
|
||||
View view = views.get(layoutId);
|
||||
if (view == null) {
|
||||
view = convertView.findViewById(layoutId);
|
||||
views.put(layoutId, view);
|
||||
}
|
||||
return (T) view;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import dji.sdk.keyvalue.converter.DJIValueConverter;
|
||||
import dji.sdk.keyvalue.converter.IDJIValueConverter;
|
||||
import dji.sdk.keyvalue.converter.SingleValueConverter;
|
||||
import dji.sdk.keyvalue.key.AIBoxKey;
|
||||
import dji.sdk.keyvalue.key.AirLinkKey;
|
||||
import dji.sdk.keyvalue.key.BatteryKey;
|
||||
import dji.sdk.keyvalue.key.BleKey;
|
||||
import dji.sdk.keyvalue.key.CameraKey;
|
||||
import dji.sdk.keyvalue.key.DJIKeyInfo;
|
||||
import dji.sdk.keyvalue.key.FlightAssistantKey;
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey;
|
||||
import dji.sdk.keyvalue.key.GimbalKey;
|
||||
import dji.sdk.keyvalue.key.LidarKey;
|
||||
import dji.sdk.keyvalue.key.MobileNetworkKey;
|
||||
import dji.sdk.keyvalue.key.MobileNetworkLinkRCKey;
|
||||
import dji.sdk.keyvalue.key.OcuSyncKey;
|
||||
import dji.sdk.keyvalue.key.OnboardKey;
|
||||
import dji.sdk.keyvalue.key.PayloadKey;
|
||||
import dji.sdk.keyvalue.key.ProductKey;
|
||||
import dji.sdk.keyvalue.key.RadarKey;
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey;
|
||||
import dji.sdk.keyvalue.key.RtkBaseStationKey;
|
||||
import dji.sdk.keyvalue.key.RtkMobileStationKey;
|
||||
import dji.sdk.keyvalue.value.base.DJIValue;
|
||||
import dji.v5.utils.common.LogUtils;
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/16 3:36 下午
|
||||
* @description:
|
||||
*/
|
||||
public class KeyItemDataUtil {
|
||||
private static final String TAG = KeyItemDataUtil.class.getSimpleName();
|
||||
private static final List<KeyItem<?, ?>> allKeyList = new ArrayList<>();
|
||||
|
||||
private KeyItemDataUtil() {
|
||||
//do something
|
||||
}
|
||||
|
||||
public static void initBatteryKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, BatteryKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initAirlinkKeyList(List<KeyItem<?, ?>> keylist) {
|
||||
initList(keylist, AirLinkKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initGimbalKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, GimbalKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initCameraKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, CameraKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initWiFiKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
// initList(keyList , WiFiKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initFlightAssistantKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, FlightAssistantKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initFlightControllerKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, FlightControllerKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRemoteControllerKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RemoteControllerKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initBleKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, BleKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initProductKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, ProductKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRtkBaseStationKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RtkBaseStationKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRtkMobileStationKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RtkMobileStationKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initOcuSyncKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, OcuSyncKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initRadarKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, RadarKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initIntelligentBoxList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, AIBoxKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initMobileNetworkKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, MobileNetworkKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initMobileNetworkLinkRCKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, MobileNetworkLinkRCKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initOnboardKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, OnboardKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initPayloadKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, PayloadKey.getKeyList());
|
||||
}
|
||||
|
||||
public static void initLidarKeyList(List<KeyItem<?, ?>> keyList) {
|
||||
initList(keyList, LidarKey.getKeyList());
|
||||
}
|
||||
|
||||
private static void initList(List<KeyItem<?, ?>> keyList, List<DJIKeyInfo<?>> keyInfoList) {
|
||||
if (keyList == null || !keyList.isEmpty()){
|
||||
return;
|
||||
}
|
||||
for (DJIKeyInfo<?> info : keyInfoList) {
|
||||
KeyItem<DJIValue, DJIValue> item = new KeyItem<>(info);
|
||||
genericItem(item, info);
|
||||
keyList.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public static <P extends DJIValue, R extends DJIValue> void genericItem(KeyItem<P, R> item, DJIKeyInfo<?> keyInfo) {
|
||||
|
||||
Class<?> tdClazz;
|
||||
Field field = null;
|
||||
boolean isDjiValue = false;
|
||||
try {
|
||||
IDJIValueConverter<P, R> clazzConvert = keyInfo.getTypeConverter();
|
||||
if (clazzConvert instanceof SingleValueConverter) {
|
||||
field = clazzConvert.getClass().getDeclaredField("dClass");
|
||||
Field tmp = clazzConvert.getClass().getDeclaredField("isDJIValue");
|
||||
tmp.setAccessible(true);
|
||||
isDjiValue = tmp.getBoolean(clazzConvert);
|
||||
} else if (clazzConvert instanceof DJIValueConverter) {
|
||||
field = clazzConvert.getClass().getDeclaredField("tClass");
|
||||
}
|
||||
|
||||
if (field != null) {
|
||||
field.setAccessible(true);
|
||||
tdClazz = (Class<?>) field.get(clazzConvert);
|
||||
item.param = (P) tdClazz.newInstance();
|
||||
item.result = (R) tdClazz.newInstance();
|
||||
item.setSingleDJIValue(isDjiValue);
|
||||
item.initGenericInstance();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static int getAllKeyListCount() {
|
||||
List<KeyItem<?, ?>> allKeyList = new ArrayList<>();
|
||||
getAllKeyList(allKeyList);
|
||||
return allKeyList.size();
|
||||
}
|
||||
|
||||
public static void getAllKeyList(List<KeyItem<?, ?>> keylist) {
|
||||
if (!allKeyList.isEmpty()) {
|
||||
keylist.addAll(allKeyList);
|
||||
return;
|
||||
}
|
||||
List<KeyItem<?, ?>> keyList = new ArrayList<>();
|
||||
initBatteryKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initAirlinkKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initGimbalKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initCameraKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initFlightAssistantKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initFlightControllerKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRemoteControllerKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initBleKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initProductKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRtkBaseStationKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRtkMobileStationKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initOcuSyncKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initRadarKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initMobileNetworkKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initMobileNetworkLinkRCKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initOnboardKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initPayloadKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initLidarKeyList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
|
||||
initIntelligentBoxList(keyList);
|
||||
allKeyList.addAll(keyList);
|
||||
keyList.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
|
||||
|
||||
import dji.sampleV5.aircraft.util.Util
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import java.lang.Exception
|
||||
import java.lang.StringBuilder
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/03/10 11:23 上午
|
||||
* @description:
|
||||
*/
|
||||
object KeyItemHelper {
|
||||
|
||||
val TAG = LogUtils.getTag(this)
|
||||
val LISTEN_RECORD_MAX_LENGTH = 2000
|
||||
val FILED_CHANGE = "\$change"
|
||||
/**
|
||||
* 通过反射给字段赋值
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
fun initSubItemData(obj: Any): Map<String?, List<EnumItem>> {
|
||||
val dataMap: MutableMap<String?, List<EnumItem>> = HashMap()
|
||||
try {
|
||||
val fields = obj.javaClass.declaredFields
|
||||
for (field in fields) {
|
||||
if (field.name == FILED_CHANGE || field.name == "serialVersionUID") {
|
||||
continue
|
||||
}
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
dataMap[clazz.canonicalName] =
|
||||
buildParamsSubItemListWithEnum(field.type as Class<Enum<*>>)
|
||||
} else if (isEnumList(field)) {
|
||||
val type = field.genericType
|
||||
if (type is ParameterizedType) {
|
||||
val subObject: Class<out Enum<*>> =
|
||||
type.actualTypeArguments[0] as Class<Enum<*>>
|
||||
dataMap[clazz.canonicalName] = buildParamsSubItemListWithEnum(subObject)
|
||||
}
|
||||
} else {
|
||||
dataMap.clear()
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG,e.message)
|
||||
}
|
||||
return dataMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理界面参数选择和设置
|
||||
*
|
||||
* @param anchor
|
||||
* @param dataMap
|
||||
*/
|
||||
fun <P> processSubListLogic(
|
||||
anchor: View,
|
||||
param: P,
|
||||
dataMap: Map<String?, List<EnumItem>>,
|
||||
callBack: KeyItemActionListener<String?>
|
||||
) {
|
||||
try {
|
||||
val nameList = Util.getMapKeyList(dataMap)
|
||||
val subItemList = Util.getMapValueList(dataMap)
|
||||
if (dataMap.size == 1) {
|
||||
//简单列表
|
||||
val list = subItemList[0]
|
||||
val clazz = Class.forName(nameList[0]!!) as Class<Enum<*>>
|
||||
showSimpleSubItemList(anchor.context, list, clazz, object :
|
||||
KeyItemActionListener<List<String>?> {
|
||||
override fun actionChange(t: List<String>?) {
|
||||
updateClassData(param, dataMap)
|
||||
callBack.actionChange(param.toString())
|
||||
}
|
||||
})
|
||||
} else {
|
||||
//复合列表
|
||||
KeyValueDialogUtil.showListConfirmWindow(
|
||||
anchor,
|
||||
getSimpleNameList(nameList),
|
||||
"select item for setting",
|
||||
object :
|
||||
KeyItemActionListener<String?> {
|
||||
override fun actionChange(msg: String?) {
|
||||
if ("confirm" == msg) {
|
||||
updateClassData(param, dataMap)
|
||||
callBack.actionChange(param.toString())
|
||||
} else {
|
||||
val clazz = getClassWithName(msg, nameList) as Class<Enum<*>>?
|
||||
val list = dataMap[clazz!!.canonicalName]!!
|
||||
showSimpleSubItemList(
|
||||
anchor.context,
|
||||
list,
|
||||
clazz
|
||||
) { updateClassData(param, dataMap) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 类名列表获取:通过全名list获取简名list
|
||||
*
|
||||
* @param nameList
|
||||
* @return
|
||||
*/
|
||||
fun getSimpleNameList(nameList: List<String?>): List<String> {
|
||||
val simpleNameList: MutableList<String> = ArrayList()
|
||||
for (str in nameList) {
|
||||
simpleNameList.add(str!!.substring(str!!.lastIndexOf(".") + 1))
|
||||
}
|
||||
return simpleNameList
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过简名获取类字节码
|
||||
*
|
||||
* @param simpleName
|
||||
* @param nameList
|
||||
* @return
|
||||
*/
|
||||
fun getClassWithName(simpleName: String?, nameList: List<String?>): Class<*>? {
|
||||
var clazz: Class<*>? = null
|
||||
try {
|
||||
for (str in nameList) {
|
||||
if (str!!.endsWith(simpleName!!)) {
|
||||
clazz = Class.forName(str!!)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
return clazz
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数条目的选项子列表
|
||||
*
|
||||
* @param <E>
|
||||
* @return
|
||||
</E> */
|
||||
fun <E : Enum<*>?> buildParamsSubItemListWithEnum(clazz: Class<E>): List<EnumItem> {
|
||||
val list: MutableList<EnumItem> = ArrayList()
|
||||
try {
|
||||
var item: EnumItem
|
||||
val objs: Array<out E>? = clazz.getEnumConstants()
|
||||
for (obj in objs!!) {
|
||||
item = EnumItem()
|
||||
item.setName(obj.toString())
|
||||
list.add(item)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG,e.message)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性子列表数据
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
fun getSubItemNameList(data: List<EnumItem>): List<String> {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
for (item in data) {
|
||||
result.add(item.getName().toString())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中属性的顺序值
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedIndex(data: List<EnumItem>): Int {
|
||||
var index = 0
|
||||
for (i in data.indices) {
|
||||
if (data[i].isSelected()) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选中属性的value
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
fun getSelectedValue(data: List<EnumItem>): String {
|
||||
var value = ""
|
||||
for (i in data.indices) {
|
||||
if (data[i].isSelected()) {
|
||||
value = data[i].getName().toString()
|
||||
break
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选择状态
|
||||
*
|
||||
* @param data
|
||||
* @param name
|
||||
*/
|
||||
fun updatedSelectedInfo(data: List<EnumItem>, names: List<String?>) {
|
||||
for (item in data) {
|
||||
item.setSelected(false)
|
||||
for (name in names) {
|
||||
if (item.getName().equals(name)) {
|
||||
item.setSelected(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单参数列表,逻辑处理
|
||||
*/
|
||||
fun <E : Enum<*>?> showSimpleSubItemList(
|
||||
context: Context?,
|
||||
simpleItemList: List<EnumItem>,
|
||||
clazz: Class<E>,
|
||||
callBack: KeyItemActionListener<List<String>?>
|
||||
) {
|
||||
val StrList = getSubItemNameList(simpleItemList)
|
||||
if (StrList.size == 0) {
|
||||
return
|
||||
}
|
||||
val selectedIndex = getSelectedIndex(simpleItemList)
|
||||
if (clazz.isEnum) {
|
||||
KeyValueDialogUtil.showSingleChoiceDialog(
|
||||
context,
|
||||
StrList,
|
||||
selectedIndex,
|
||||
object : KeyItemActionListener<List<String>?> {
|
||||
override fun actionChange(values: List<String>?) {
|
||||
updatedSelectedInfo(simpleItemList, values!!)
|
||||
callBack.actionChange(values)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
KeyValueDialogUtil.showMultiChoiceDialog(
|
||||
context,
|
||||
StrList,
|
||||
) { values ->
|
||||
updatedSelectedInfo(simpleItemList, values!!)
|
||||
callBack.actionChange(values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射给字段赋值
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
fun initClassData(obj: Any?) {
|
||||
if (obj == null || Util.isBlank(obj.toString())) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val pFields = obj.javaClass.declaredFields
|
||||
for (field in pFields) {
|
||||
if (field.name == FILED_CHANGE || field.name == "serialVersionUID") {
|
||||
continue
|
||||
}
|
||||
field.isAccessible = true
|
||||
val clazz = field.type
|
||||
if (setFieldPro(field , obj)){
|
||||
continue
|
||||
}
|
||||
|
||||
val subObj = clazz.newInstance()
|
||||
field[obj] = subObj
|
||||
initClassData(subObj)
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun setFieldPro(field:Field , obj: Any?):Boolean{
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
field[obj] = clazz.enumConstants!![0]
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Util.isWrapClass(clazz)) {
|
||||
if (clazz == Boolean::class.javaObjectType) {
|
||||
field[obj] = false
|
||||
}
|
||||
//如果需要 可在else 中可给Integer Double 等设置初始值
|
||||
return true
|
||||
}
|
||||
if (clazz == MutableList::class.java) {
|
||||
field[obj] = ArrayList<Any>()// todo
|
||||
return true
|
||||
} else if (clazz == String::class.java) {
|
||||
field[obj] = ""
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isEnumList(field: Field): Boolean {
|
||||
if (field.type == MutableList::class.java) {
|
||||
val type = field.genericType
|
||||
if (type is ParameterizedType) {
|
||||
val subType = type.actualTypeArguments[0]
|
||||
val clazz = subType as Class<*>
|
||||
if (clazz.isEnum) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射给字段赋值
|
||||
*
|
||||
* @param obj
|
||||
*/
|
||||
fun updateClassData(obj: Any?, subItemMap: Map<String?, List<EnumItem>>) {
|
||||
try {
|
||||
val pFields = obj?.javaClass?.declaredFields
|
||||
if (pFields != null) {
|
||||
for (field in pFields) {
|
||||
if (field.name == FILED_CHANGE || field.name == "serialVersionUID") {
|
||||
continue
|
||||
}
|
||||
field.isAccessible = true
|
||||
val clazz = field.type
|
||||
if (clazz.isEnum) {
|
||||
val itemList = subItemMap[clazz.canonicalName]!!
|
||||
field[obj] = getEnumData(clazz as Class<Enum<*>>, getSelectedValue(itemList))
|
||||
} else if (isEnumList(field)) {
|
||||
setEnumListProperty(field , obj , subItemMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun setEnumListProperty(field: Field , obj: Any? , subItemMap: Map<String?, List<EnumItem>>) {
|
||||
val type = field.genericType
|
||||
val clazz = field.type
|
||||
if (type is ParameterizedType) {
|
||||
val subObject = type.actualTypeArguments[0] as Class<Enum<*>>
|
||||
val itemList = subItemMap[clazz.canonicalName]
|
||||
val list: MutableList<Any> = ArrayList()
|
||||
val values: List<String> = getSelectedValues(itemList!!)
|
||||
for (value in values) {
|
||||
val test: Any = getEnumData(subObject, value)!!
|
||||
list.add(test)
|
||||
}
|
||||
field[obj] = list
|
||||
}
|
||||
}
|
||||
fun getSelectedValues(data: List<EnumItem>): List<String> {
|
||||
var value: String = ""
|
||||
val values: MutableList<String> = ArrayList()
|
||||
for (i in data.indices) {
|
||||
if (data[i].isSelected()) {
|
||||
value = data[i].getName()!!
|
||||
values.add(value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加携带日期的推送字符串
|
||||
*
|
||||
* @param targetStr
|
||||
* @param appendStr
|
||||
* @return
|
||||
*/
|
||||
fun appendListenRecord(targetStr: String, appendStr: String?): String {
|
||||
if (Util.isBlank(appendStr)) {
|
||||
return targetStr
|
||||
}
|
||||
val sb = StringBuilder(targetStr)
|
||||
sb.append("\n")
|
||||
sb.append(Util.getDateStr(Date()) + ":")
|
||||
sb.append("\n")
|
||||
sb.append(appendStr)
|
||||
//长度限制
|
||||
var result = sb.toString()
|
||||
if (result.length > LISTEN_RECORD_MAX_LENGTH) {
|
||||
result = result.substring(result.length - LISTEN_RECORD_MAX_LENGTH)
|
||||
}
|
||||
val title = "push info:"
|
||||
if (!result.startsWith(title)) {
|
||||
result = """
|
||||
$title
|
||||
$result
|
||||
""".trimIndent()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过字节码和值来获取实例
|
||||
*
|
||||
* @param clazz
|
||||
* @param value
|
||||
* @param <E>
|
||||
* @return
|
||||
</E> */
|
||||
fun <E : Enum<*>?> getEnumData(clazz: Class<E>, value: String): E? {
|
||||
var data: E? = null
|
||||
try {
|
||||
val objs: Array<out E>? = clazz.getEnumConstants()
|
||||
for (obj in objs!!) {
|
||||
if (obj.toString() == value) {
|
||||
data = obj as E
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.e(TAG , e.message)
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
import dji.sdk.keyvalue.key.ComponentType
|
||||
import dji.sdk.keyvalue.key.ProductKey
|
||||
import dji.sdk.keyvalue.utils.MultiComponentManager
|
||||
import dji.sdk.keyvalue.value.common.CameraLensType
|
||||
import dji.sdk.keyvalue.value.product.ProductType
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.DJICommonError
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.manager.capability.CapabilityManager
|
||||
import dji.v5.manager.capability.CapabilityParser
|
||||
import dji.v5.utils.common.DateUtils
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.CompletableEmitter
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 11:18 上午
|
||||
* @description: key的操作基类
|
||||
*/
|
||||
abstract class KeyOperatorCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) {
|
||||
|
||||
private val INTERVAL_TIME = 500L // 连续快速调用key时 可能会导致失败
|
||||
private val TAG_EQUAL = "=="
|
||||
private val TAG = LogUtils.getTag(this)
|
||||
private lateinit var competableEmitter: CompletableEmitter
|
||||
private var unPassedCount = 0;
|
||||
private lateinit var curCheckType: KeyCheckType
|
||||
private var keyCount = 0
|
||||
|
||||
private var whiteList = mutableListOf<String>(
|
||||
"GimbalCalibrationStatus",
|
||||
"IsShootingPhotoPanorama",
|
||||
"PhotoPanoramaMode",
|
||||
"PhotoPanoramaProgress",
|
||||
"ThermalContrast",
|
||||
"ThermalDDE",
|
||||
"ThermalRegionMetersureTemperature",
|
||||
"ThermalBrightness",
|
||||
"ThermalGainModeTemperatureRange",
|
||||
"AircraftLocation3D",
|
||||
"PhotoRatio"
|
||||
)
|
||||
|
||||
/**
|
||||
* 过滤Key类型条件
|
||||
*/
|
||||
abstract fun filter(item: KeyItem<*, *>): Boolean
|
||||
|
||||
/**
|
||||
* 指定指定动作类型
|
||||
*/
|
||||
abstract fun run(item: KeyItem<*, *>)
|
||||
|
||||
/**
|
||||
* 写文件需要
|
||||
*/
|
||||
abstract fun getTAG(): String
|
||||
|
||||
/**
|
||||
* Key执行结果回调TAG,用来改key执行错误或者失败
|
||||
*/
|
||||
abstract fun getErrorTAG(): String
|
||||
|
||||
open fun getIntervalTime(): Long {
|
||||
return INTERVAL_TIME
|
||||
}
|
||||
|
||||
fun execute(): Completable {
|
||||
|
||||
return Completable.create { emitter ->
|
||||
competableEmitter = emitter;
|
||||
unPassedCount = 0
|
||||
val allList: MutableList<KeyItem<*, *>> = ArrayList()
|
||||
KeyItemDataUtil.getAllKeyList(allList)
|
||||
val capabilityKeyCount =
|
||||
CapabilityManager.getInstance().getCapabilityKeyCount(productType)
|
||||
|
||||
LogUtils.i(TAG, "begin check $capabilityKeyCount")
|
||||
saveResult(" ----- begin ${getTAG()} check -----\n\n", false, false)
|
||||
saveResult(" ----- begin ${getTAG()} check -----\n\n", true, false)
|
||||
|
||||
allList.filter { item ->
|
||||
filter(item) && (item.toString() !in whiteList) && CapabilityManager.getInstance().isKeySupported(
|
||||
productType, componentTypeName, ComponentType.find(item.getKeyInfo().componentType), "Key$item"
|
||||
)
|
||||
}.forEach { item ->
|
||||
LogUtils.e(TAG, "${++keyCount} doCheck $item ")
|
||||
LogUtils.e(TAG, "Thread name is 1 " + Thread.currentThread().name)
|
||||
val lock = CountDownLatch(1)
|
||||
item.componetIndex = if (MultiComponentManager.isMultiKey(item.keyInfo.componentType)) {
|
||||
componentIndex
|
||||
} else {
|
||||
0
|
||||
}
|
||||
dependKeySet(item, object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
//从主线程再切回io线程
|
||||
LogUtils.e(TAG, "Thread name is 2 " + Thread.currentThread().name)
|
||||
lock.countDown()
|
||||
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
LogUtils.e(TAG, "Set $item depend key failed!")
|
||||
lock.countDown()
|
||||
}
|
||||
})
|
||||
|
||||
lock.await()
|
||||
Thread.sleep(getIntervalTime()) // 设置完后,立即设置可能会异常如FrequencyBand
|
||||
run(item)
|
||||
}
|
||||
Thread.sleep(getIntervalTime())
|
||||
saveResult(" --------finish ${getTAG()}---------\n", true, true)
|
||||
saveResult(" --------finish ${getTAG()}---------\n", false, true)
|
||||
//遍历完成即完成
|
||||
competableEmitter.onComplete()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行依赖key的 Set方法
|
||||
*/
|
||||
private fun dependKeySet(item: KeyItem<*, *>, callback: CommonCallbacks.CompletionCallback) {
|
||||
// 首先获取具体的依赖的keyitem
|
||||
val keyName = item.toString()
|
||||
val valueBean = CapabilityParser.getInstance().getValueBean(keyName)
|
||||
val dependKeyItem = valueBean?.dependKeyName?.let { CapabilityKeyChecker.getKeyItem(it) }
|
||||
LogUtils.e(TAG, "dependKeyItem key : " + (valueBean?.dependKeyName ?: "null"))
|
||||
|
||||
if (dependKeyItem != null) {
|
||||
dependKeyItem.setKeyOperateCallBack { res ->
|
||||
if (res.toString().contains("SetErrorMsg")) {
|
||||
callback.onFailure(DJICommonError.FACTORY.build(res.toString()))
|
||||
} else {
|
||||
callback.onSuccess()
|
||||
}
|
||||
}
|
||||
dependKeyItem.setComponetIndex(item.getComponetIndex())
|
||||
dependKeyItem.setSubComponetType(item.getSubComponetType())
|
||||
dependKeyItem.setSubComponetIndex(item.getSubComponetIndex())
|
||||
dependKeyItem.doSet(valueBean.dependKeyValue.replace("\\", ""))
|
||||
} else {
|
||||
// 没有找到前置条件,返回成功。
|
||||
callback.onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
fun doKeyParam(item: KeyItem<*, *>, type: KeyCheckType) {
|
||||
curCheckType = type
|
||||
getItemDecoderList(item).forEach {
|
||||
val lock = CountDownLatch(1)
|
||||
item.setKeyOperateCallBack {
|
||||
var result = StringBuilder()
|
||||
val resStr = it.toString()
|
||||
val keyNameIndex = resStr.indexOf(getTAG())
|
||||
if (keyNameIndex <= -1 ) return@setKeyOperateCallBack
|
||||
val keyName = resStr.substring(0, keyNameIndex)
|
||||
val isPassed: Boolean
|
||||
val failedReson = if (resStr.contains(getErrorTAG())) {
|
||||
isPassed = false
|
||||
resStr.substring(resStr.indexOf(TAG_EQUAL))
|
||||
} else {
|
||||
isPassed = true
|
||||
resStr.substring(resStr.indexOf(TAG_EQUAL))
|
||||
}
|
||||
var componentTYpe = ComponentType.find(item.getKeyInfo().componentType)
|
||||
|
||||
result.append("${++unPassedCount} KeyName :${keyName} - ${componentTYpe}\n")
|
||||
.append("SubType:${getLensName(item)}\n")
|
||||
.append("Details:${failedReson}\n")
|
||||
.append("\n ----------------------- \n")
|
||||
|
||||
saveResult(result.toString(), isPassed, true)
|
||||
LogUtils.e(TAG, "SubType:${getLensName(item)} KeyName :${keyName}} ComponentType : $componentTYpe " + resStr)
|
||||
lock.countDown()
|
||||
}
|
||||
item.setComponetIndex(it.componetIndex)
|
||||
item.setSubComponetType(it.subComponetType)
|
||||
item.setSubComponetIndex(it.subComponetIndex)
|
||||
when (type) {
|
||||
KeyCheckType.ACTION -> item.doAction(it.jsonString)
|
||||
KeyCheckType.SET -> item.doSet(it.jsonString)
|
||||
KeyCheckType.GET -> item.doGet()
|
||||
}
|
||||
lock.await()
|
||||
Thread.sleep(getIntervalTime())
|
||||
}
|
||||
|
||||
LogUtils.e(TAG, "check finish!")
|
||||
}
|
||||
|
||||
private fun getLensName(keyItem: KeyItem<*, *>): String {
|
||||
return if (keyItem.keyInfo.componentType == ComponentType.CAMERA.value()) {
|
||||
CameraLensType.find(keyItem.getSubComponetType()).name
|
||||
} else {
|
||||
"DEFAULT"
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveResult(content: String, saveType: Boolean, append: Boolean) {
|
||||
val product = ProductKey.KeyProductType.create().get(ProductType.UNRECOGNIZED)
|
||||
var filePath = LogUtils.getLogPath() + getTAG() + product.name + "【${DateUtils.getSystemTimeOnlyYMD()}】"
|
||||
|
||||
filePath += if (saveType) {
|
||||
"Success.txt"
|
||||
} else {
|
||||
"Failed.txt"
|
||||
}
|
||||
FileUtils.writeFile(filePath, content, append)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过key名称从能力集中获取keyItem需要的参数,包括lenstype ,用例json
|
||||
* 如果在能力集中没有找到用例 需要返回一个默认的(无参的action 返回空)
|
||||
*/
|
||||
private fun getItemDecoderList(keyItem: KeyItem<*, *>): MutableList<CapabilityKeyChecker.ItemDecoder> {
|
||||
val resList: MutableList<CapabilityKeyChecker.ItemDecoder> = ArrayList()
|
||||
//通过item获取 支持的lensType 列表
|
||||
var lensTypeList = if (keyItem.keyInfo.componentType == ComponentType.CAMERA.value()) {
|
||||
CapabilityManager.getInstance().getSupportLens("Key$keyItem", productType, componentTypeName)
|
||||
} else {
|
||||
arrayListOf("DEFAULT")
|
||||
}
|
||||
//获取javaBean 字符列表 用例中没有文件则返回空集合
|
||||
var keyParamList = CapabilityKeyChecker.getKeyParamList(keyItem.toString())
|
||||
|
||||
// 用例文件存在(set类型 都会有) ;action 不在用例文件中的则不自动测试需要人为测试 支持set get
|
||||
if (keyParamList.isNotEmpty() || keyItem.canGet()) {
|
||||
lensTypeList.map {
|
||||
transCameraLensTypeStr(it)
|
||||
}.forEach { subComponentType ->
|
||||
val index = if (MultiComponentManager.isMultiKey(keyItem.keyInfo.componentType)) {
|
||||
componentIndex
|
||||
} else {
|
||||
0
|
||||
}
|
||||
if (curCheckType == KeyCheckType.SET || curCheckType == KeyCheckType.ACTION) {
|
||||
keyParamList
|
||||
.forEach {
|
||||
resList.add(
|
||||
CapabilityKeyChecker.ItemDecoder(
|
||||
componetIndex = index,
|
||||
subComponetType = subComponentType,
|
||||
jsonString = it
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (curCheckType == KeyCheckType.GET) {
|
||||
resList.add(
|
||||
CapabilityKeyChecker.ItemDecoder(
|
||||
componetIndex = index,
|
||||
subComponetType = subComponentType,
|
||||
jsonString = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//添加默认 确保每个key都可以执行到set方法 , action无参数不执行(在用例中未找到)
|
||||
}
|
||||
return resList
|
||||
}
|
||||
|
||||
/**
|
||||
* 将能力集中CameraLensType 字符串转为对应的value
|
||||
*/
|
||||
fun transCameraLensTypeStr(lensName: String): Int {
|
||||
CameraLensType.values().forEach {
|
||||
if (it.name.contains(lensName)) {
|
||||
return it.value()
|
||||
}
|
||||
}
|
||||
return CameraLensType.UNKNOWN.value()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package dji.sampleV5.aircraft.keyvalue
|
||||
|
||||
/**
|
||||
* @author feel.feng
|
||||
* @time 2022/10/26 2:25 下午
|
||||
* @description:
|
||||
*/
|
||||
class KeySetCommand(
|
||||
private val productType: String,
|
||||
private val componentTypeName: String,
|
||||
private val componentIndex: Int
|
||||
) : KeyOperatorCommand(productType, componentTypeName, componentIndex) {
|
||||
|
||||
private val TAG_GET = "【SET】"
|
||||
private val TAG_ERROR = "SetErrorMsg"
|
||||
private val INTERVAL_TIME = 3000L
|
||||
|
||||
//&& ("CameraMode" ==item.toString() || "RegionMeteringArea" == item.toString())
|
||||
override fun filter(item: KeyItem<*, *>): Boolean {
|
||||
return item.canSet()
|
||||
}
|
||||
|
||||
override fun run(item: KeyItem<*, *>) {
|
||||
super.doKeyParam(item, KeyCheckType.SET)
|
||||
}
|
||||
|
||||
override fun getTAG(): String {
|
||||
return TAG_GET
|
||||
}
|
||||
|
||||
override fun getErrorTAG(): String {
|
||||
return TAG_ERROR
|
||||
}
|
||||
|
||||
override fun getIntervalTime(): Long {
|
||||
return INTERVAL_TIME
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
package dji.sampleV5.aircraft.keyvalue;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.PopupWindow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import dji.sampleV5.aircraft.R;
|
||||
import dji.sampleV5.aircraft.util.Util;
|
||||
import dji.v5.utils.common.ContextUtil;
|
||||
import dji.v5.utils.common.DisplayUtil;
|
||||
|
||||
|
||||
public class KeyValueDialogUtil {
|
||||
|
||||
private static final int LIST_Y_OFF_SET = 3;
|
||||
|
||||
private KeyValueDialogUtil(){
|
||||
// init something
|
||||
}
|
||||
/**
|
||||
* 显示单选对话框
|
||||
*/
|
||||
public static void showSingleChoiceDialog(Context context, List<String> data, int selectedIndex, final KeyItemActionListener<List<String>> callBack) {
|
||||
AlertDialog dialog;
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
final String[] items = data.toArray(new String[data.size()]);
|
||||
builder.setSingleChoiceItems(items, selectedIndex, (dialog1, which) -> {
|
||||
if (callBack != null) {
|
||||
callBack.actionChange(Arrays.asList(items[which]));
|
||||
dialog1.dismiss();
|
||||
}
|
||||
});
|
||||
builder.setCancelable(true);
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
public static void showMultiChoiceDialog(Context context, List<String> data, final KeyItemActionListener<List<String>> callBack){
|
||||
AlertDialog dialog;
|
||||
List<String> values = new ArrayList<>();
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
final String[] items = data.toArray(new String[data.size()]);
|
||||
builder.setMultiChoiceItems(items, null, (dialog1, which, isChecked) -> {
|
||||
if (isChecked) {
|
||||
values.add(items[which]);
|
||||
} else {
|
||||
values.remove(items[which]);
|
||||
}
|
||||
|
||||
});
|
||||
builder.setPositiveButton(R.string.confirm, (dialog12, which) -> callBack.actionChange(values));
|
||||
builder.setCancelable(true);
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
|
||||
}
|
||||
/**
|
||||
* 显示简单列表弹窗
|
||||
*
|
||||
* @param anchor
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
public static void showListConfirmWindow(View anchor, final List<String> data, String title, final KeyItemActionListener<String> callback) {
|
||||
if (anchor == null || anchor.getContext() == null) {
|
||||
return;
|
||||
}
|
||||
Context context = anchor.getContext();
|
||||
View rootView = View.inflate(context, R.layout.dialog_list_confirm, null);
|
||||
final PopupWindow window = new PopupWindow(context);
|
||||
window.setWidth(Util.getHeight(ContextUtil.getContext()) / 2);
|
||||
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
window.setOutsideTouchable(false);
|
||||
window.setTouchable(true);
|
||||
window.setFocusable(true);
|
||||
window.setBackgroundDrawable(new ColorDrawable(0xffffff));
|
||||
window.setContentView(rootView);
|
||||
window.showAsDropDown(anchor, 0, LIST_Y_OFF_SET, Gravity.CENTER | Gravity.BOTTOM);
|
||||
|
||||
ListView listView = rootView.findViewById(R.id.list_view);
|
||||
TextView titleView = rootView.findViewById(R.id.title);
|
||||
if (Util.isNotBlank(title)) {
|
||||
titleView.setText(title);
|
||||
titleView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
titleView.setVisibility(View.GONE);
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, R.layout.item_textview, data);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> callback.actionChange(data.get(position)));
|
||||
rootView.findViewById(R.id.button).setOnClickListener(v -> {
|
||||
window.dismiss();
|
||||
callback.actionChange("confirm");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示输入确认弹窗
|
||||
*
|
||||
* @param context
|
||||
* @param item
|
||||
*/
|
||||
public static void showInputDialog(Activity context, KeyItem<? ,?> item, final KeyItemActionListener<String> callback) {
|
||||
showInputDialog(context, context.getResources().getString(R.string.key_value_set) + item.getName() + ":", item.getParamJsonStr(), "", false, callback);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显示输入确认弹窗
|
||||
*
|
||||
* @param context
|
||||
* @param title
|
||||
* @param msg
|
||||
*/
|
||||
public static void showInputDialog(Activity context, String title, final String msg, String hint, boolean singleLine, final KeyItemActionListener<String> callback) {
|
||||
View dialogView = context.getLayoutInflater().inflate(R.layout.dialog_param_input, null);
|
||||
dialogView.setBackgroundColor(context.getResources().getColor(R.color.gray));
|
||||
|
||||
final AlertDialog dialog = new AlertDialog.Builder(context).setView(dialogView).create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.setCancelable(false);
|
||||
|
||||
TextView tvTitle = dialogView.findViewById(R.id.title);
|
||||
tvTitle.setText(title);
|
||||
|
||||
final EditText input = dialogView.findViewById(R.id.input);
|
||||
input.setSingleLine(singleLine);
|
||||
if (Util.isNotBlank(msg)) {
|
||||
input.setText(msg);
|
||||
}
|
||||
if (Util.isNotBlank(hint)) {
|
||||
input.setHint(hint);
|
||||
}
|
||||
input.setMovementMethod(ScrollingMovementMethod.getInstance());
|
||||
dialogView.findViewById(R.id.confirm).setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.actionChange(input.getText().toString().trim());
|
||||
}
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialogView.findViewById(R.id.cancel).setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.actionChange(null);
|
||||
}
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显示通用弹窗
|
||||
*
|
||||
* @param anchor
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
public static void showFilterListWindow(View anchor, final List<KeyItem<?,?>> data, final KeyItemActionListener<KeyItem<?,?>> callback) {
|
||||
if (anchor == null || anchor.getContext() == null) {
|
||||
return;
|
||||
}
|
||||
Context context = anchor.getContext();
|
||||
View rootView = View.inflate(context, R.layout.window_simple_listview, null);
|
||||
final PopupWindow window = new PopupWindow(context);
|
||||
window.setWidth(Util.getHeight(ContextUtil.getContext()) / 2);
|
||||
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
window.setOutsideTouchable(true);
|
||||
window.setTouchable(true);
|
||||
window.setFocusable(true);
|
||||
window.setBackgroundDrawable(new ColorDrawable(0xffffff));
|
||||
|
||||
window.setContentView(rootView);
|
||||
window.showAsDropDown(anchor, 0, - DisplayUtil.dip2px(anchor.getContext(), 47), Gravity.LEFT | Gravity.TOP );
|
||||
|
||||
ListView listView = rootView.findViewById(R.id.list_view);
|
||||
TextView titleView = rootView.findViewById(R.id.tv_title);
|
||||
|
||||
titleView.setText(R.string.commonlyused_key);
|
||||
titleView.setVisibility(View.VISIBLE);
|
||||
|
||||
ArrayAdapter<KeyItem<?,?>> adapter = new ArrayAdapter<KeyItem<?,?>> (context, R.layout.item_textview, data);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (callback != null ) {
|
||||
callback.actionChange(data.get(position));
|
||||
}
|
||||
if (window != null) {
|
||||
window.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示通用弹窗
|
||||
*
|
||||
* @param anchor
|
||||
* @param data
|
||||
* @param callback
|
||||
*/
|
||||
public static void showChannelFilterListWindow(View anchor, final List<ChannelType> data , final KeyItemActionListener<ChannelType> callback) {
|
||||
if (anchor == null || anchor.getContext() == null) {
|
||||
return;
|
||||
}
|
||||
Context context = anchor.getContext();
|
||||
View rootView = View.inflate(context, R.layout.window_simple_listview, null);
|
||||
final PopupWindow window = new PopupWindow(context);
|
||||
window.setWidth(Util.getHeight(ContextUtil.getContext()) / 2);
|
||||
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
window.setOutsideTouchable(true);
|
||||
window.setTouchable(true);
|
||||
window.setFocusable(true);
|
||||
window.setBackgroundDrawable(new ColorDrawable(0xffffff));
|
||||
|
||||
window.setContentView(rootView);
|
||||
window.showAsDropDown(anchor, (int) (anchor.getWidth() * 1.5), - DisplayUtil.dip2px(anchor.getContext(), 37), Gravity.LEFT | Gravity.BOTTOM);
|
||||
ListView listView = rootView.findViewById(R.id.list_view);
|
||||
|
||||
ArrayAdapter<ChannelType> adapter = new ArrayAdapter<ChannelType>(context, R.layout.item_textview, data);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (callback != null ) {
|
||||
callback.actionChange(data.get(position));
|
||||
}
|
||||
if (window != null) {
|
||||
window.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
EditText filter = rootView.findViewById(R.id.et_filter);
|
||||
filter.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
// dosomething
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
// dosomething
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
adapter.getFilter().filter(s.toString());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static void showNormalDialog(Activity context, String title) {
|
||||
View dialogView = context.getLayoutInflater().inflate(R.layout.dialog_tips, null);
|
||||
final AlertDialog dialog = new AlertDialog.Builder(context).setView(dialogView).create();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.setCancelable(false);
|
||||
|
||||
TextView tvTitle = dialogView.findViewById(R.id.title);
|
||||
tvTitle.setText(title);
|
||||
|
||||
dialogView.findViewById(R.id.confirm).setOnClickListener(v -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.FileProvider
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.DeviceInfoUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* ClassName : MSDKLogVM
|
||||
* Description : Log展示
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2022/5/7 12:17 下午
|
||||
* Copyright : ©2022 DJI All Rights Reserved.
|
||||
*/
|
||||
class APPSilentlyUpgradeVM : DJIViewModel() {
|
||||
|
||||
private val testPackageName = "com.dji.test"
|
||||
private val testApkName = "app-debug.apk"
|
||||
|
||||
/**
|
||||
* 只适配了M350和Mavic3行业版本的遥控器。
|
||||
* 设置以后,对应包名的apk,通过代码安装时,遥控不再需要用户操作。
|
||||
* 重启遥控后设置清空。
|
||||
*/
|
||||
fun setAPPSilentlyUpgrade(context: Context) {
|
||||
RemoteControllerKey.KeyAPPSilentlyUpgrade.create().action(testPackageName, {
|
||||
toastResult?.postValue(DJIToastResult.success(testPackageName))
|
||||
}) {
|
||||
toastResult?.postValue(DJIToastResult.failed("$it $testPackageName"))
|
||||
}
|
||||
}
|
||||
|
||||
//本示例,安装包路径: /sdcard/Android/data/你的app的包名/files/你的apk文件名.apk
|
||||
//实际路径请按自身需求设置,通过FileProvider安装APK
|
||||
//测试apk(app-debug.apk)在sample的assets目录下
|
||||
fun installApkWithOutNotice(context: Context) {
|
||||
FileUtils.copyAssetsFileIfNeed(context, "apk/$testApkName", File(context.getExternalFilesDir("/"), testApkName))
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val file = File(context.getExternalFilesDir("/"), testApkName)
|
||||
val uri = FileProvider.getUriForFile(context, DeviceInfoUtil.getPackageName() + ".fileProvider", file)
|
||||
intent.setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.R
|
||||
import dji.sdk.keyvalue.key.RemoteControllerKey
|
||||
import dji.sdk.keyvalue.value.remotecontroller.PairingState
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.manager.SDKManager
|
||||
import dji.v5.utils.common.StringUtils
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/2/14
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class BaseMainActivityVm : DJIViewModel() {
|
||||
|
||||
val sdkNews = MutableLiveData<SDKNews>()
|
||||
|
||||
init {
|
||||
updateNews()
|
||||
}
|
||||
|
||||
private fun updateNews() {
|
||||
sdkNews.postValue(SDKNews(R.string.news_title, R.string.news_description, StringUtils.getResStr(R.string.news_date)))
|
||||
}
|
||||
|
||||
fun doPairing(callback: ((String) -> Unit)? = null) {
|
||||
if (!SDKManager.getInstance().isRegistered) {
|
||||
return
|
||||
}
|
||||
RemoteControllerKey.KeyPairingStatus.create().get({
|
||||
if (it == PairingState.PAIRING) {
|
||||
RemoteControllerKey.KeyStopPairing.create().action()
|
||||
callback?.invoke(StringUtils.getResStr(R.string.stop_pairing))
|
||||
} else {
|
||||
RemoteControllerKey.KeyRequestPairing.create().action()
|
||||
callback?.invoke(StringUtils.getResStr(R.string.start_pairing))
|
||||
}
|
||||
}) {
|
||||
callback?.invoke(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
data class SDKNews(
|
||||
var title: Int,
|
||||
var description: Int,
|
||||
var date: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.common.EmptyMsg
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.action
|
||||
import dji.v5.et.create
|
||||
|
||||
class BasicAircraftControlVM : DJIViewModel() {
|
||||
|
||||
fun startTakeOff(callback: CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>) {
|
||||
FlightControllerKey.KeyStartTakeoff.create().action({
|
||||
callback.onSuccess(it)
|
||||
}, { e: IDJIError ->
|
||||
callback.onFailure(e)
|
||||
})
|
||||
}
|
||||
|
||||
fun startLanding(callback: CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>) {
|
||||
FlightControllerKey.KeyStartAutoLanding.create().action({
|
||||
callback.onSuccess(it)
|
||||
}, { e: IDJIError ->
|
||||
callback.onFailure(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.view.Surface
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.sdk.keyvalue.key.CameraKey
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.camera.CameraMode
|
||||
import dji.sdk.keyvalue.value.airlink.ChannelPriority
|
||||
import dji.sdk.keyvalue.value.camera.CameraType
|
||||
import dji.sdk.keyvalue.value.camera.CameraVideoStreamSourceType
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.sdk.keyvalue.value.flightassistant.VisionAssistDirection
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.listen
|
||||
import dji.v5.et.set
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager.FrameFormat
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager.ScaleType
|
||||
import dji.v5.utils.common.DJIExecutor
|
||||
import dji.v5.utils.common.DateUtils
|
||||
import dji.v5.utils.common.LogPath
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import dji.v5.utils.common.StringUtils
|
||||
import dji.v5.ux.R
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Locale
|
||||
|
||||
const val TAG = "CameraStreamDetailFragmentVM"
|
||||
|
||||
class CameraStreamDetailVM : DJIViewModel() {
|
||||
|
||||
private val _availableLensListData = MutableLiveData<List<CameraVideoStreamSourceType>>(ArrayList())
|
||||
private val _currentLensData = MutableLiveData(CameraVideoStreamSourceType.DEFAULT_CAMERA)
|
||||
private val _cameraName = MutableLiveData("Unknown")
|
||||
private val _isVisionAssistEnabled = MutableLiveData(false)
|
||||
private val _visionAssistViewDirection = MutableLiveData(VisionAssistDirection.UNKNOWN)
|
||||
private val _visionAssistViewDirectionRange = MutableLiveData<List<VisionAssistDirection>>(ArrayList())
|
||||
val cameraStreamEnableMap = MutableLiveData(emptyMap<ComponentIndexType, Boolean>())
|
||||
|
||||
private var cameraIndex = ComponentIndexType.UNKNOWN
|
||||
private var cameraType = ""
|
||||
private var isMotorOn = false
|
||||
private val visionAssistStatusListener = object :
|
||||
ICameraStreamManager.VisionAssistStatusListener {
|
||||
override fun onVisionAssistEnabled(isEnable: Boolean) {
|
||||
_isVisionAssistEnabled.postValue(isEnable)
|
||||
}
|
||||
|
||||
override fun onVisionAssistViewDirectionUpdated(mode: VisionAssistDirection) {
|
||||
_visionAssistViewDirection.postValue(mode)
|
||||
}
|
||||
|
||||
override fun onVisionAssistViewDirectionRangeUpdated(modes: MutableList<VisionAssistDirection>) {
|
||||
_visionAssistViewDirectionRange.postValue(modes)
|
||||
}
|
||||
}
|
||||
|
||||
private val availableCameraUpdatedListener = object :
|
||||
ICameraStreamManager.AvailableCameraUpdatedListener {
|
||||
override fun onAvailableCameraUpdated(availableCameraList: MutableList<ComponentIndexType>) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(map: MutableMap<ComponentIndexType, Boolean>) {
|
||||
cameraStreamEnableMap.postValue(map)
|
||||
}
|
||||
}
|
||||
|
||||
private var streamFile: File? = null
|
||||
private var streamFileOutputStream: FileOutputStream? = null
|
||||
|
||||
private val streamListener = ICameraStreamManager.ReceiveStreamListener { data, offset, length, info ->
|
||||
if (streamFile == null) {
|
||||
val fileName = "[${cameraIndex.name}]${DateUtils.getSystemTime()}.${info.mimeType.name.lowercase(Locale.ROOT)}"
|
||||
ToastUtils.showToast("begin to save,$fileName")
|
||||
streamFile = File(LogUtils.getLogPath(), fileName)
|
||||
streamFileOutputStream = FileOutputStream(streamFile)
|
||||
return@ReceiveStreamListener
|
||||
}
|
||||
DJIExecutor.getExecutor().execute {
|
||||
try {
|
||||
streamFileOutputStream?.write(data, offset, length)
|
||||
} catch (e: Exception) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
setCameraIndex(ComponentIndexType.UNKNOWN)
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeAvailableCameraUpdatedListener(availableCameraUpdatedListener)
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeVisionAssistStatusListener(visionAssistStatusListener)
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
doStopDownloadStreamToLocal()
|
||||
}
|
||||
|
||||
fun setCameraIndex(cameraIndex: ComponentIndexType) {
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
if (this.cameraIndex == cameraIndex) {
|
||||
return
|
||||
}
|
||||
this.cameraIndex = cameraIndex
|
||||
if (this.cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
listenCameraName()
|
||||
listenAvailableLens()
|
||||
listenCurrentLens()
|
||||
listenVisionAssistStatus()
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addAvailableCameraUpdatedListener(availableCameraUpdatedListener)
|
||||
}
|
||||
|
||||
private fun listenCameraName() {
|
||||
CameraKey.KeyCameraType.create(cameraIndex).listen(this) {
|
||||
cameraType = it?.name ?: CameraType.NOT_SUPPORTED.name
|
||||
updateCameraName()
|
||||
}
|
||||
|
||||
FlightControllerKey.KeyAreMotorsOn.create().listen(this) {
|
||||
isMotorOn = it == true
|
||||
updateCameraName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCameraName() {
|
||||
_cameraName.postValue("")
|
||||
if (cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
if (cameraIndex == ComponentIndexType.FPV) {
|
||||
_cameraName.postValue(ComponentIndexType.FPV.name)
|
||||
return
|
||||
}
|
||||
if (cameraIndex == ComponentIndexType.VISION_ASSIST) {
|
||||
var msg = ComponentIndexType.VISION_ASSIST.name
|
||||
if (!isMotorOn) {
|
||||
msg = "$msg(${StringUtils.getResStr(R.string.uxsdk_assistant_video_empty_text)})"
|
||||
}
|
||||
_cameraName.postValue(msg)
|
||||
return
|
||||
}
|
||||
_cameraName.postValue(cameraType)
|
||||
}
|
||||
|
||||
private fun listenAvailableLens() {
|
||||
_availableLensListData.postValue(arrayListOf())
|
||||
if (cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
CameraKey.KeyCameraVideoStreamSourceRange.create(cameraIndex).listen(this) {
|
||||
val list: List<CameraVideoStreamSourceType> = it ?: arrayListOf()
|
||||
_availableLensListData.postValue(list)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenCurrentLens() {
|
||||
_currentLensData.postValue(CameraVideoStreamSourceType.DEFAULT_CAMERA)
|
||||
if (cameraIndex == ComponentIndexType.UNKNOWN) {
|
||||
return
|
||||
}
|
||||
CameraKey.KeyCameraVideoStreamSource.create(cameraIndex).listen(this) {
|
||||
if (it != null) {
|
||||
_currentLensData.postValue(it)
|
||||
} else {
|
||||
_currentLensData.postValue(CameraVideoStreamSourceType.DEFAULT_CAMERA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenVisionAssistStatus() {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addVisionAssistStatusListener(visionAssistStatusListener)
|
||||
}
|
||||
|
||||
fun changeCameraLens(lensType: CameraVideoStreamSourceType) {
|
||||
CameraKey.KeyCameraVideoStreamSource.create(cameraIndex).set(lensType)
|
||||
}
|
||||
|
||||
fun putCameraStreamSurface(
|
||||
surface: Surface, width: Int, height: Int, scaleType: ScaleType
|
||||
) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.putCameraStreamSurface(cameraIndex, surface, width, height, scaleType)
|
||||
}
|
||||
|
||||
fun removeCameraStreamSurface(surface: Surface) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeCameraStreamSurface(surface)
|
||||
}
|
||||
|
||||
fun downloadYUVImageToLocal(format: FrameFormat, formatName: String) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addFrameListener(
|
||||
cameraIndex,
|
||||
format,
|
||||
object : ICameraStreamManager.CameraFrameListener {
|
||||
override fun onFrame(frameData: ByteArray, offset: Int, length: Int, width: Int, height: Int, format: FrameFormat) {
|
||||
try {
|
||||
val dirs = File(LogUtils.getLogPath() + "STREAM_PIC")
|
||||
if (!dirs.exists()) {
|
||||
dirs.mkdirs()
|
||||
}
|
||||
val fileName = "[${cameraIndex.name}][$width x $height]${DateUtils.getSystemTime()}.${formatName}"
|
||||
val file = File(dirs.absolutePath, fileName)
|
||||
FileOutputStream(file).use { stream ->
|
||||
stream.write(frameData, offset, length)
|
||||
stream.flush()
|
||||
stream.close()
|
||||
ToastUtils.showToast("Save to : ${file.path}")
|
||||
}
|
||||
LogUtils.i(TAG, "Save to : ${file.path}")
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showToast("Save fail : $e")
|
||||
}
|
||||
// Because only one frame needs to be saved, you need to call removeOnFrameListener here
|
||||
// If you need to read frame data for a long time, you can choose to actually call remove OnFrameListener according to your needs
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeFrameListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun enableVisionAssist(enable: Boolean) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.enableVisionAssist(enable, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
ToastUtils.showToast("enableVisionAssist onSuccess $enable")
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
ToastUtils.showToast("enableVisionAssist onFailure $enable,error:$error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setVisionAssistViewDirection(direction: VisionAssistDirection) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.setVisionAssistViewDirection(direction, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
ToastUtils.showToast("set Direction onSuccess $direction")
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
ToastUtils.showToast("set Direction onFailure $direction,error:$error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun beginDownloadStreamToLocal() {
|
||||
if (streamFile != null) {
|
||||
ToastUtils.showToast("Pls stop first.")
|
||||
return
|
||||
}
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addReceiveStreamListener(cameraIndex, streamListener)
|
||||
}
|
||||
|
||||
fun stopDownloadStreamToLocal() {
|
||||
if (streamFile == null) {
|
||||
ToastUtils.showToast("Pls begin first.")
|
||||
return
|
||||
}
|
||||
ToastUtils.showToast("stop to save,${streamFile?.name}")
|
||||
doStopDownloadStreamToLocal()
|
||||
}
|
||||
|
||||
fun setStreamEncoderBitrate(bitrate: Int) {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.setStreamEncoderBitrate(cameraIndex, bitrate)
|
||||
}
|
||||
|
||||
fun getStreamEncoderBitrate() = MediaDataCenter.getInstance().cameraStreamManager.getStreamEncoderBitrate(cameraIndex)
|
||||
|
||||
fun changeCameraMode(mode: CameraMode) {
|
||||
CameraKey.KeyCameraMode.create().set(mode)
|
||||
}
|
||||
|
||||
fun setStreamPriority(priority: ChannelPriority) = MediaDataCenter.getInstance().cameraStreamManager.setStreamPriority(cameraIndex, priority)
|
||||
|
||||
fun getStreamPriority() = MediaDataCenter.getInstance().cameraStreamManager.getStreamPriority(cameraIndex)
|
||||
|
||||
fun enableStream(enable: Boolean) = MediaDataCenter.getInstance().cameraStreamManager.enableStream(cameraIndex, enable)
|
||||
|
||||
private fun doStopDownloadStreamToLocal() {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeReceiveStreamListener(streamListener)
|
||||
try {
|
||||
streamFileOutputStream?.flush()
|
||||
streamFileOutputStream?.close()
|
||||
streamFileOutputStream = null
|
||||
streamFile = null
|
||||
} catch (e: Exception) {
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
val availableLensListData: LiveData<List<CameraVideoStreamSourceType>>
|
||||
get() = _availableLensListData
|
||||
|
||||
val currentLensData: LiveData<CameraVideoStreamSourceType>
|
||||
get() = _currentLensData
|
||||
|
||||
val cameraName: LiveData<String>
|
||||
get() = _cameraName
|
||||
|
||||
val isVisionAssistEnabled: LiveData<Boolean>
|
||||
get() = _isVisionAssistEnabled
|
||||
|
||||
val visionAssistViewDirection: LiveData<VisionAssistDirection>
|
||||
get() = _visionAssistViewDirection
|
||||
|
||||
val visionAssistViewDirectionRange: LiveData<List<VisionAssistDirection>>
|
||||
get() = _visionAssistViewDirectionRange
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sdk.keyvalue.value.common.ComponentIndexType
|
||||
import dji.v5.manager.datacenter.MediaDataCenter
|
||||
import dji.v5.manager.interfaces.ICameraStreamManager.AvailableCameraUpdatedListener
|
||||
|
||||
class CameraStreamListVM : DJIViewModel(), AvailableCameraUpdatedListener {
|
||||
|
||||
private val _availableCameraListData = MutableLiveData<List<ComponentIndexType>>(ArrayList())
|
||||
|
||||
init {
|
||||
MediaDataCenter.getInstance().cameraStreamManager.addAvailableCameraUpdatedListener(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
MediaDataCenter.getInstance().cameraStreamManager.removeAvailableCameraUpdatedListener(this)
|
||||
}
|
||||
|
||||
val availableCameraListData: LiveData<List<ComponentIndexType>>
|
||||
get() = _availableCameraListData
|
||||
|
||||
override fun onAvailableCameraUpdated(availableCameraList: List<ComponentIndexType>) {
|
||||
_availableCameraListData.postValue(availableCameraList)
|
||||
}
|
||||
|
||||
override fun onCameraStreamEnableUpdate(cameraStreamEnableMap: MutableMap<ComponentIndexType, Boolean>) {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dji.sampleV5.aircraft.util.DJIToastUtil
|
||||
import dji.v5.utils.common.LogUtils
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/7/5
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
open class DJIViewModel : ViewModel() {
|
||||
val toastResult
|
||||
get() = DJIToastUtil.dJIToastLD
|
||||
|
||||
val logTag = LogUtils.getTag(this)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import dji.v5.ux.core.util.ToastUtils
|
||||
import dji.v5.manager.dataprotect.DataProtectionManager
|
||||
import dji.v5.utils.common.DJIExecutor
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import dji.v5.utils.common.LogUtils.OnExportLogProgressCallback
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/6/30
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class DataProtectionVm : DJIViewModel() {
|
||||
|
||||
fun agreeToProductImprovement(isAgree: Boolean) {
|
||||
DataProtectionManager.getInstance().agreeToProductImprovement(isAgree)
|
||||
}
|
||||
|
||||
fun isAgreeToProductImprovement(): Boolean {
|
||||
return DataProtectionManager.getInstance().isAgreeToProductImprovement
|
||||
}
|
||||
|
||||
fun enableLog(enable: Boolean) {
|
||||
DataProtectionManager.getInstance().enableMSDKLog(enable)
|
||||
}
|
||||
|
||||
fun isLogEnable(): Boolean {
|
||||
return DataProtectionManager.getInstance().isMSDKLogEnabled
|
||||
}
|
||||
|
||||
fun logPath(): String {
|
||||
return DataProtectionManager.getInstance().msdkLogPath
|
||||
}
|
||||
|
||||
fun clearLog(): Boolean {
|
||||
return DataProtectionManager.getInstance().clearMSDKLog()
|
||||
}
|
||||
|
||||
fun zipAndExportLog() {
|
||||
DJIExecutor.getExecutorFor(DJIExecutor.Purpose.IO).execute {
|
||||
LogUtils.zipAndExportLog("logs", object : OnExportLogProgressCallback {
|
||||
override fun onExportBegin() {
|
||||
ToastUtils.showToast("ZipAndExportLog begin")
|
||||
}
|
||||
|
||||
override fun onExportProgress(progress: Int) {
|
||||
ToastUtils.showToast("ZipAndExportLog Progress:$progress")
|
||||
}
|
||||
|
||||
override fun onExportFailed(result: String?) {
|
||||
ToastUtils.showToast("ZipAndExportLog Failed:$result")
|
||||
}
|
||||
|
||||
override fun onExportSuccess(filePath: String?) {
|
||||
ToastUtils.showToast("ZipAndExportLog Success:$filePath")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.v5.manager.diagnostic.*
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2021/6/30
|
||||
*
|
||||
* Copyright (c) 2021, DJI All Rights Reserved.
|
||||
*/
|
||||
class DiagnosticVm : DJIViewModel() {
|
||||
|
||||
val deviceHealthInfos = MutableLiveData(ArrayList<DJIDeviceHealthInfo>())
|
||||
|
||||
val lastDeviceStatus = MutableLiveData(DJIDeviceStatus.NORMAL)
|
||||
val currentDeviceStatus = MutableLiveData(DJIDeviceStatus.NORMAL)
|
||||
|
||||
private val deviceHealthInfoChangeListener = DJIDeviceHealthInfoChangeListener {
|
||||
updateDeviceHealthInfo(it as ArrayList<DJIDeviceHealthInfo>)
|
||||
}
|
||||
|
||||
private val deviceStatusChangeListener = DJIDeviceStatusChangeListener { from, to ->
|
||||
updateDeviceStatus(from, to)
|
||||
}
|
||||
|
||||
private fun updateDeviceHealthInfo(infos: ArrayList<DJIDeviceHealthInfo>) {
|
||||
deviceHealthInfos.value?.clear()
|
||||
deviceHealthInfos.value?.addAll(infos)
|
||||
deviceHealthInfos.postValue(deviceHealthInfos.value)
|
||||
}
|
||||
|
||||
private fun updateDeviceStatus(form: DJIDeviceStatus, to: DJIDeviceStatus) {
|
||||
lastDeviceStatus.value = form
|
||||
currentDeviceStatus.value = to
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
stopListenDeviceHealthInfoChange()
|
||||
stopListenDeviceStatusChange()
|
||||
}
|
||||
|
||||
fun startListenDeviceHealthInfoChange() {
|
||||
DeviceHealthManager.getInstance()
|
||||
.addDJIDeviceHealthInfoChangeListener(deviceHealthInfoChangeListener)
|
||||
}
|
||||
|
||||
fun stopListenDeviceHealthInfoChange() {
|
||||
DeviceHealthManager.getInstance()
|
||||
.removeDJIDeviceHealthInfoChangeListener(deviceHealthInfoChangeListener)
|
||||
}
|
||||
|
||||
fun getCurrentDeviceHealthInfos() {
|
||||
updateDeviceHealthInfo(DeviceHealthManager.getInstance().currentDJIDeviceHealthInfos as ArrayList<DJIDeviceHealthInfo>)
|
||||
}
|
||||
|
||||
fun startListenDeviceStatusChange() {
|
||||
DeviceStatusManager.getInstance()
|
||||
.addDJIDeviceStatusChangeListener(deviceStatusChangeListener)
|
||||
}
|
||||
|
||||
fun stopListenDeviceStatusChange() {
|
||||
DeviceStatusManager.getInstance()
|
||||
.removeDJIDeviceStatusChangeListener(deviceStatusChangeListener)
|
||||
}
|
||||
|
||||
fun getCurrentDeviceStatus() {
|
||||
currentDeviceStatus.value = DeviceStatusManager.getInstance().currentDJIDeviceStatus
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import dji.v5.manager.aircraft.flightrecord.FlightLogManager
|
||||
|
||||
/**
|
||||
* Description : FLightReecordVM
|
||||
* Author : daniel.chen
|
||||
* CreateDate : 2021/7/15 10:51 上午
|
||||
* Copyright : ©2021 DJI All Rights Reserved.
|
||||
*/
|
||||
class FlightRecordVM : DJIViewModel() {
|
||||
fun getFlightLogPath(): String {
|
||||
return FlightLogManager.getInstance().flightRecordPath
|
||||
}
|
||||
|
||||
fun getFlyClogPath(): String {
|
||||
return FlightLogManager.getInstance().flyClogPath
|
||||
}
|
||||
}
|
||||
313
sample/src/main/java/dji/sampleV5/aircraft/models/FlySafeVM.kt
Normal file
313
sample/src/main/java/dji/sampleV5/aircraft/models/FlySafeVM.kt
Normal file
@@ -0,0 +1,313 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sampleV5.aircraft.util.Util
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate3D
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.get
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseComponent
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseListener
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseState
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeDatabaseUpgradeMode
|
||||
import dji.v5.manager.aircraft.flysafe.FlySafeNotificationListener
|
||||
import dji.v5.manager.aircraft.flysafe.FlyZoneManager
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeDatabaseInfo
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeReturnToHomeInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeSeriousWarningInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeTipInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlySafeWarningInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlyZoneInformation
|
||||
import dji.v5.manager.aircraft.flysafe.info.FlyZoneLicenseInfo
|
||||
import dji.v5.utils.common.ContextUtil
|
||||
import dji.v5.utils.common.FileUtils
|
||||
import dji.v5.utils.common.LogUtils
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Class Description
|
||||
*
|
||||
* @author Hoker
|
||||
* @date 2022/8/12
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class FlySafeVM : DJIViewModel() {
|
||||
|
||||
val flySafeWarningInformation = MutableLiveData<FlySafeWarningInformation>()
|
||||
val flySafeSeriousWarningInformation = MutableLiveData<FlySafeSeriousWarningInformation>()
|
||||
val flySafeReturnToHomeInformation = MutableLiveData<FlySafeReturnToHomeInformation>()
|
||||
val flySafeTipInformation = MutableLiveData<FlySafeTipInformation>()
|
||||
val flyZoneInformation = MutableLiveData<MutableList<FlyZoneInformation>>()
|
||||
val serverFlyZoneLicenseInfo = MutableLiveData<MutableList<FlyZoneLicenseInfo>>()
|
||||
val aircraftFlyZoneLicenseInfo = MutableLiveData<MutableList<FlyZoneLicenseInfo>>()
|
||||
|
||||
val importAndSyncState = MutableLiveData<ImportAndSyncState>()
|
||||
val dataBaseInfo = MutableLiveData<DataBaseInfo>()
|
||||
val dataUpgradeState = MutableLiveData<FlySafeDatabaseState>()
|
||||
|
||||
private val availableTestFlySafeDynamicDatabaseName = "de.geojson"
|
||||
private val unAvailableTestFlySafeDynamicDatabaseName = "France.json"
|
||||
|
||||
private val flySafeNotificationListener = object : FlySafeNotificationListener {
|
||||
|
||||
override fun onWarningNotificationUpdate(info: FlySafeWarningInformation) {
|
||||
flySafeWarningInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onSeriousWarningNotificationUpdate(info: FlySafeSeriousWarningInformation) {
|
||||
flySafeSeriousWarningInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onReturnToHomeNotificationUpdate(info: FlySafeReturnToHomeInformation) {
|
||||
flySafeReturnToHomeInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onTipNotificationUpdate(info: FlySafeTipInformation) {
|
||||
flySafeTipInformation.postValue(info)
|
||||
}
|
||||
|
||||
override fun onSurroundingFlyZonesUpdate(infos: MutableList<FlyZoneInformation>) {
|
||||
flyZoneInformation.postValue(infos)
|
||||
}
|
||||
}
|
||||
|
||||
fun initListener() {
|
||||
FlyZoneManager.getInstance().addFlySafeNotificationListener(flySafeNotificationListener)
|
||||
addFlySafeDatabaseListener()
|
||||
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
FlyZoneManager.getInstance().removeFlySafeNotificationListener(flySafeNotificationListener)
|
||||
removeFlySafeDatabaseListener()
|
||||
}
|
||||
|
||||
fun getAircraftLocation(): LocationCoordinate3D = FlightControllerKey.KeyAircraftLocation3D.create().get(LocationCoordinate3D(0.0, 0.0,0.0))
|
||||
|
||||
fun getFlyZonesInSurroundingArea(location: LocationCoordinate2D) {
|
||||
FlyZoneManager.getInstance().getFlyZonesInSurroundingArea(location, object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<MutableList<FlyZoneInformation>> {
|
||||
|
||||
override fun onSuccess(infos: MutableList<FlyZoneInformation>?) {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
flyZoneInformation.postValue(infos ?: arrayListOf())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun downloadFlyZoneLicensesFromServer() {
|
||||
FlyZoneManager.getInstance().downloadFlyZoneLicensesFromServer(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<MutableList<FlyZoneLicenseInfo>> {
|
||||
|
||||
override fun onSuccess(infos: MutableList<FlyZoneLicenseInfo>?) {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
serverFlyZoneLicenseInfo.postValue(infos ?: arrayListOf())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pushFlyZoneLicensesToAircraft() {
|
||||
FlyZoneManager.getInstance().pushFlyZoneLicensesToAircraft(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
pullFlyZoneLicensesFromAircraft()
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pullFlyZoneLicensesFromAircraft() {
|
||||
FlyZoneManager.getInstance().pullFlyZoneLicensesFromAircraft(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<MutableList<FlyZoneLicenseInfo>> {
|
||||
|
||||
override fun onSuccess(infos: MutableList<FlyZoneLicenseInfo>?) {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
|
||||
aircraftFlyZoneLicenseInfo.postValue(infos ?: arrayListOf())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun deleteFlyZoneLicensesFromAircraft() {
|
||||
FlyZoneManager.getInstance().deleteFlyZoneLicensesFromAircraft(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlyZoneLicensesEnabled(info: FlyZoneLicenseInfo, isEnable: Boolean) {
|
||||
FlyZoneManager.getInstance().setFlyZoneLicensesEnabled(info, isEnable, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
pullFlyZoneLicensesFromAircraft()
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun unlockAuthorizationFlyZone(flyZoneID: Int) {
|
||||
FlyZoneManager.getInstance().unlockAuthorizationFlyZone(flyZoneID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun unlockAllEnhancedWarningFlyZone() {
|
||||
FlyZoneManager.getInstance().unlockAllEnhancedWarningFlyZone(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success())
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed(error.toString()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun pushFlySafeDynamicDatabaseToAircraftAndApp(fileName: String) {
|
||||
FlyZoneManager.getInstance().importFlySafeDynamicDatabaseToMSDK(fileName, object :
|
||||
CommonCallbacks.CompletionCallbackWithProgress<Double> {
|
||||
override fun onProgressUpdate(progress: Double?) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(progress!!.toFloat().roundToInt()))
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
importAndSyncState.postValue(ImportAndSyncState(100))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(-1, error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun syncFlySafeMSDKDatabaseToAircraft() {
|
||||
FlyZoneManager.getInstance().pushFlySafeDynamicDatabaseToAircraft(object :
|
||||
CommonCallbacks.CompletionCallbackWithProgress<Double> {
|
||||
override fun onProgressUpdate(progress: Double?) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(Math.round(progress!!.toFloat())))
|
||||
}
|
||||
|
||||
override fun onSuccess() {
|
||||
importAndSyncState.postValue(ImportAndSyncState(100))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(-1, error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlySafeDynamicDatabaseUpgradeMode(flySafeDynamicDatabaseUpgradeMode: FlySafeDatabaseUpgradeMode) {
|
||||
FlyZoneManager.getInstance().setFlySafeDynamicDatabaseUpgradeMode(flySafeDynamicDatabaseUpgradeMode, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
importAndSyncState.postValue(ImportAndSyncState(100))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
importAndSyncState.postValue(ImportAndSyncState(-1, error))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun addFlySafeDatabaseListener() {
|
||||
FlyZoneManager.getInstance().addFlySafeDatabaseListener(object : FlySafeDatabaseListener {
|
||||
override fun onFlySafeDatabaseInfoUpdate(flySafeDatabaseInfo: FlySafeDatabaseInfo) {
|
||||
LogUtils.i("testFly", "dataName :" + flySafeDatabaseInfo.databaseName + " compnent :" + flySafeDatabaseInfo.component)
|
||||
dataBaseInfo.postValue(
|
||||
DataBaseInfo(
|
||||
flySafeDatabaseInfo.databaseName,
|
||||
formatCEDBTime(flySafeDatabaseInfo.databaseTimeStamp * 1000),
|
||||
Util.byte2AdaptiveUnitStrDefault(flySafeDatabaseInfo.databaseSize),
|
||||
flySafeDatabaseInfo.component, flySafeDatabaseInfo.flySafeDatabaseUpgradeMode
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFlySafeDatabaseStateUpdate(flySafeDatabaseState: FlySafeDatabaseState) {
|
||||
dataUpgradeState.postValue(flySafeDatabaseState)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
fun pushAvailableTestFlySafeDynamicDatabaseToApp() {
|
||||
val file = File(ContextUtil.getContext().getExternalFilesDir("/"), availableTestFlySafeDynamicDatabaseName)
|
||||
FileUtils.copyAssetsFileIfNeed(ContextUtil.getContext(), "flysafe/$availableTestFlySafeDynamicDatabaseName", file)
|
||||
pushFlySafeDynamicDatabaseToAircraftAndApp(file.path)
|
||||
}
|
||||
|
||||
fun pushUnAvailableTestFlySafeDynamicDatabaseToApp() {
|
||||
val file = File(ContextUtil.getContext().getExternalFilesDir("/"), unAvailableTestFlySafeDynamicDatabaseName)
|
||||
FileUtils.copyAssetsFileIfNeed(ContextUtil.getContext(), "flysafe/$unAvailableTestFlySafeDynamicDatabaseName", file)
|
||||
pushFlySafeDynamicDatabaseToAircraftAndApp(file.path)
|
||||
}
|
||||
|
||||
private fun formatCEDBTime(timestamp: Long): String {
|
||||
val format = SimpleDateFormat("yyyy/MM/dd")
|
||||
return format.format(timestamp)
|
||||
}
|
||||
|
||||
private fun removeFlySafeDatabaseListener() {
|
||||
FlyZoneManager.getInstance().clearAllFlySafeDatabaseListener()
|
||||
}
|
||||
|
||||
data class DataBaseInfo(
|
||||
var dataBaseName: String,
|
||||
var dataBaseTime: String,
|
||||
var dataBaseSize: String,
|
||||
var component: FlySafeDatabaseComponent,
|
||||
var upgradeMode: FlySafeDatabaseUpgradeMode
|
||||
)
|
||||
|
||||
data class ImportAndSyncState(
|
||||
var importAndSyncProgress: Int,
|
||||
var error: IDJIError? = null,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import android.app.Application
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelLazy
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStore
|
||||
|
||||
// ViewModelStore for Global Use
|
||||
val globalViewModelStore = ViewModelStore()
|
||||
|
||||
/**
|
||||
* Create a global ViewModel in the activity
|
||||
*/
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> ComponentActivity.globalViewModels(
|
||||
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
|
||||
): Lazy<VM> {
|
||||
val factoryPromise = factoryProducer ?: {
|
||||
defaultViewModelProviderFactory
|
||||
}
|
||||
return ViewModelLazy(VM::class, { globalViewModelStore }, factoryPromise)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a global ViewModel in the fragment
|
||||
*/
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Fragment.globalViewModels(
|
||||
noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
|
||||
): Lazy<VM> {
|
||||
val factoryPromise = factoryProducer ?: {
|
||||
defaultViewModelProviderFactory
|
||||
}
|
||||
return ViewModelLazy(VM::class, { globalViewModelStore }, factoryPromise)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a global ViewModel in the fragment
|
||||
*/
|
||||
@MainThread
|
||||
inline fun <reified VM : ViewModel> Application.globalViewModels(): Lazy<VM> {
|
||||
val factory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
|
||||
return ViewModelLazy(VM::class, { globalViewModelStore }, { factory })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.manager.aircraft.aibox.IntelligentBoxAppInfo
|
||||
import dji.v5.manager.aircraft.aibox.IntelligentBoxInfo
|
||||
import dji.v5.manager.aircraft.aibox.IntelligentBoxInfoListener
|
||||
import dji.v5.manager.aircraft.payload.PayloadCenter
|
||||
import dji.v5.manager.aircraft.payload.PayloadIndexType
|
||||
|
||||
|
||||
/**
|
||||
* Description :
|
||||
*
|
||||
* @author: Byte.Cai
|
||||
* date : 2022/12/1
|
||||
*
|
||||
* Copyright (c) 2022, DJI All Rights Reserved.
|
||||
*/
|
||||
class IntelligentBoxVM : DJIViewModel() {
|
||||
private lateinit var payloadIndexType: PayloadIndexType
|
||||
private val intelligentBoxMap = PayloadCenter.getInstance().intelligentBoxManager
|
||||
val intelligentBoxInfo = MutableLiveData<IntelligentBoxInfo>()
|
||||
val intelligentBoxAppInfos = MutableLiveData<List<IntelligentBoxAppInfo>>()
|
||||
|
||||
private val intelligentBoxInfoListener: IntelligentBoxInfoListener = object :
|
||||
IntelligentBoxInfoListener {
|
||||
|
||||
override fun onBoxInfoUpdate(info: IntelligentBoxInfo) {
|
||||
intelligentBoxInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onBoxAppInfoUpdate(infos: List<IntelligentBoxAppInfo>) {
|
||||
intelligentBoxAppInfos.postValue(infos)
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoxSerialNumber() {
|
||||
intelligentBoxMap[payloadIndexType]?.getBoxSerialNumber(object :
|
||||
CommonCallbacks.CompletionCallbackWithParam<String> {
|
||||
override fun onSuccess(t: String) {
|
||||
sendToastMsg(DJIToastResult.success("getBoxSerialNumber: $t"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("getBoxSerialNumber,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun enableApp(appID: String) {
|
||||
intelligentBoxMap[payloadIndexType]?.enableApp(appID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
sendToastMsg(DJIToastResult.success("enableApp,success"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("enableApp,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun disableApp(appID: String) {
|
||||
intelligentBoxMap[payloadIndexType]?.disableApp(appID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
sendToastMsg(DJIToastResult.success("disableApp,success"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("disableApp,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun uninstallApp(appID: String) {
|
||||
intelligentBoxMap[payloadIndexType]?.uninstallApp(appID, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
sendToastMsg(DJIToastResult.success("uninstallApp,success"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
sendToastMsg(DJIToastResult.failed("uninstallApp,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun initListener(payloadIndexType: PayloadIndexType) {
|
||||
this.payloadIndexType = payloadIndexType
|
||||
val iPayloadManager = intelligentBoxMap[payloadIndexType]
|
||||
iPayloadManager?.addBoxInfoListener(intelligentBoxInfoListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
intelligentBoxMap[payloadIndexType]?.removeBoxInfoListener(intelligentBoxInfoListener)
|
||||
}
|
||||
|
||||
private fun sendToastMsg(djiToastResult: DJIToastResult) {
|
||||
toastResult?.postValue(djiToastResult)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
package dji.sampleV5.aircraft.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import dji.sampleV5.aircraft.data.DJIToastResult
|
||||
import dji.sdk.keyvalue.key.FlightControllerKey
|
||||
import dji.sdk.keyvalue.value.common.DoubleRect
|
||||
import dji.sdk.keyvalue.value.common.LocationCoordinate2D
|
||||
import dji.sdk.keyvalue.value.flightcontroller.FlyToMode
|
||||
import dji.v5.common.callback.CommonCallbacks
|
||||
import dji.v5.common.error.IDJIError
|
||||
import dji.v5.et.create
|
||||
import dji.v5.et.listen
|
||||
import dji.v5.manager.KeyManager
|
||||
import dji.v5.manager.intelligent.AutoSensingInfo
|
||||
import dji.v5.manager.intelligent.AutoSensingInfoListener
|
||||
import dji.v5.manager.intelligent.AutoSensingTarget
|
||||
import dji.v5.manager.intelligent.IMissionCapabilityListener
|
||||
import dji.v5.manager.intelligent.IMissionInfoListener
|
||||
import dji.v5.manager.intelligent.IntelligentFlightInfo
|
||||
import dji.v5.manager.intelligent.IntelligentFlightInfoListener
|
||||
import dji.v5.manager.intelligent.IntelligentFlightManager
|
||||
import dji.v5.manager.intelligent.IntelligentModel
|
||||
import dji.v5.manager.intelligent.TargetType
|
||||
import dji.v5.manager.intelligent.flyto.FlyToInfo
|
||||
import dji.v5.manager.intelligent.flyto.FlyToParam
|
||||
import dji.v5.manager.intelligent.flyto.FlyToTarget
|
||||
import dji.v5.manager.intelligent.poi.POICapability
|
||||
import dji.v5.manager.intelligent.poi.POIInfo
|
||||
import dji.v5.manager.intelligent.poi.POIParam
|
||||
import dji.v5.manager.intelligent.poi.POITarget
|
||||
import dji.v5.manager.intelligent.smarttrack.SmartTrackInfo
|
||||
import dji.v5.manager.intelligent.smarttrack.SmartTrackTarget
|
||||
import dji.v5.manager.intelligent.spotlight.SpotLightInfo
|
||||
import dji.v5.manager.intelligent.spotlight.SpotLightTarget
|
||||
|
||||
class IntelligentFlightVM : DJIViewModel() {
|
||||
|
||||
val aircraftHeight = MutableLiveData<Double>()
|
||||
val aircraftLocation = MutableLiveData<LocationCoordinate2D>()
|
||||
|
||||
val intelligentFlightInfo = MutableLiveData<IntelligentFlightInfo>()
|
||||
val autoSensingInfo = MutableLiveData<AutoSensingInfo>()
|
||||
val intelligentModels = MutableLiveData<List<IntelligentModel>>()
|
||||
val runningModelIndex = MutableLiveData<Int>()
|
||||
|
||||
val poiInfo = MutableLiveData<POIInfo>()
|
||||
val poiTarget = MutableLiveData<POITarget>()
|
||||
|
||||
val flyToInfo = MutableLiveData<FlyToInfo>()
|
||||
val flyToTarget = MutableLiveData<FlyToTarget>()
|
||||
|
||||
val spotLightInfo = MutableLiveData<SpotLightInfo>()
|
||||
val spotLightTarget = MutableLiveData<SpotLightTarget>()
|
||||
|
||||
val smartTrackInfo = MutableLiveData<SmartTrackInfo>()
|
||||
val smartTrackTarget = MutableLiveData<SmartTrackTarget>()
|
||||
|
||||
private val intelligentFlightInfoListener: IntelligentFlightInfoListener = object :
|
||||
IntelligentFlightInfoListener {
|
||||
override fun onIntelligentFlightInfoUpdate(info: IntelligentFlightInfo) {
|
||||
intelligentFlightInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onIntelligentFlightErrorUpdate(error: IDJIError) {
|
||||
}
|
||||
}
|
||||
|
||||
private val autoSensingInfoListener: AutoSensingInfoListener = object :
|
||||
AutoSensingInfoListener {
|
||||
override fun onAutoSensingInfoUpdate(info: AutoSensingInfo) {
|
||||
autoSensingInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onTrackingTargetUpdate(target: AutoSensingTarget) {
|
||||
// super.onTrackingTargetUpdate(target)
|
||||
}
|
||||
|
||||
override fun onIntelligentModelUpdate(models: MutableList<IntelligentModel>) {
|
||||
intelligentModels.postValue(models)
|
||||
}
|
||||
|
||||
override fun onRunningIntelligentModelUpdate(modelId: Int) {
|
||||
runningModelIndex.postValue(modelId)
|
||||
}
|
||||
}
|
||||
|
||||
private val poiInfoListener: IMissionInfoListener<POIInfo, POITarget> = object :
|
||||
IMissionInfoListener<POIInfo, POITarget> {
|
||||
override fun onMissionInfoUpdate(info: POIInfo) {
|
||||
poiInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: POITarget) {
|
||||
poiTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
private val poiCapabilityListener: IMissionCapabilityListener<POICapability> = IMissionCapabilityListener<POICapability> {
|
||||
//kk
|
||||
}
|
||||
|
||||
private val flyToInfoListener: IMissionInfoListener<FlyToInfo, FlyToTarget> = object :
|
||||
IMissionInfoListener<FlyToInfo, FlyToTarget> {
|
||||
override fun onMissionInfoUpdate(info: FlyToInfo) {
|
||||
flyToInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: FlyToTarget) {
|
||||
flyToTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
private val spotLightInfoListener: IMissionInfoListener<SpotLightInfo, SpotLightTarget> = object :
|
||||
IMissionInfoListener<SpotLightInfo, SpotLightTarget> {
|
||||
override fun onMissionInfoUpdate(info: SpotLightInfo) {
|
||||
spotLightInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: SpotLightTarget) {
|
||||
spotLightTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
private val smartTrackListener: IMissionInfoListener<SmartTrackInfo, SmartTrackTarget> = object :
|
||||
IMissionInfoListener<SmartTrackInfo, SmartTrackTarget> {
|
||||
override fun onMissionInfoUpdate(info: SmartTrackInfo) {
|
||||
smartTrackInfo.postValue(info)
|
||||
}
|
||||
|
||||
override fun onMissionTargetUpdate(target: SmartTrackTarget) {
|
||||
smartTrackTarget.postValue(target)
|
||||
}
|
||||
}
|
||||
|
||||
fun initListener() {
|
||||
IntelligentFlightManager.getInstance().addIntelligentFlightInfoListener(intelligentFlightInfoListener)
|
||||
IntelligentFlightManager.getInstance().addAutoSensingInfoListener(autoSensingInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.addMissionInfoListener(poiInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.addMissionCapabilityListener(poiCapabilityListener)
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.addMissionInfoListener(flyToInfoListener)
|
||||
IntelligentFlightManager.getInstance().spotLightManager.addMissionInfoListener(spotLightInfoListener)
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.addMissionInfoListener(smartTrackListener)
|
||||
FlightControllerKey.KeyAltitude.create().listen(this) { height ->
|
||||
height?.let {
|
||||
aircraftHeight.postValue(it)
|
||||
}
|
||||
}
|
||||
FlightControllerKey.KeyAircraftLocation.create().listen(this) { location ->
|
||||
location?.let {
|
||||
aircraftLocation.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanListener() {
|
||||
IntelligentFlightManager.getInstance().removeIntelligentFlightInfoListener(intelligentFlightInfoListener)
|
||||
IntelligentFlightManager.getInstance().removeAutoSensingInfoListener(autoSensingInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.removeMissionInfoListener(poiInfoListener)
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.removeMissionCapabilityListener(poiCapabilityListener)
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.removeMissionInfoListener(flyToInfoListener)
|
||||
IntelligentFlightManager.getInstance().spotLightManager.removeMissionInfoListener(spotLightInfoListener)
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.removeMissionInfoListener(smartTrackListener)
|
||||
KeyManager.getInstance().cancelListen(this)
|
||||
}
|
||||
|
||||
fun startAutoSensing() {
|
||||
IntelligentFlightManager.getInstance().startAutoSensing(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startAutoSensing"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startAutoSensing,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopAutoSensing() {
|
||||
IntelligentFlightManager.getInstance().stopAutoSensing(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopAutoSensing"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopAutoSensing,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectIntelligentModel(model: Int) {
|
||||
IntelligentFlightManager.getInstance().selectIntelligentModel(model, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectIntelligentModel,$model"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectIntelligentModel,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startPOIMission(target: POITarget) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.startMission(target, null,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startPOIMission"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startPOIMission,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopPOIMission() {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.stopMission(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopPOIMission"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopPOIMission,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updatePOIMissionTarget(target: POITarget) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.updateMissionTarget(target,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("updatePOIMissionTarget,$target"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("updatePOIMissionTarget,$target,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun updatePOICircleSpeed(speed: Double) {
|
||||
val param = POIParam()
|
||||
param.circleSpeed = speed
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.updateMissionParam(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("updatePOICircleSpeed,$param"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("updatePOICircleSpeed,$param,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun lockCircularVelocity(lock: Boolean) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.lockCircularVelocity(
|
||||
lock,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("lockCircularVelocity:$lock"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("lockCircularVelocity:$lock,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun lockGimbalPitch(lock: Boolean) {
|
||||
IntelligentFlightManager.getInstance().poiMissionManager.lockGimbalPitch(
|
||||
lock,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("lockGimbalPitch:$lock"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("lockGimbalPitch:$lock,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startFlyTo(target: FlyToTarget) {
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.startMission(target, null,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startFlyTo"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startFlyTo,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopFlyTo() {
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.stopMission(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopFlyTo"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopFlyTo,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlyToMode(mode: FlyToMode) {
|
||||
val param = FlyToParam()
|
||||
param.flyToMode = mode
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("setFlyToMode:$mode"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("setFlyToMode:$mode,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun setFlyToHeight(height: Int) {
|
||||
val param = FlyToParam()
|
||||
param.height = height
|
||||
IntelligentFlightManager.getInstance().flyToMissionManager.updateMissionParam(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("setFlyToMode:$height"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("setFlyToMode:$height,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun enterSpotLightMode() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.enterSpotLightMode(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("enterSpotLightMode"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("enterSpotLightMode,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun exitSpotLightMode() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.exitSpotLightMode(
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("exitSpotLightMode"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("exitSpotLightMode,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startSpotlight() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.startMission(null, null,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startSpotlight"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startSpotlight,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectAutoTarget(index: Int) {
|
||||
val param = SpotLightTarget()
|
||||
param.targetIndex = index
|
||||
param.targetType = TargetType.INDEX
|
||||
IntelligentFlightManager.getInstance().spotLightManager.updateMissionTarget(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectAutoTarget:$index"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectAutoTarget:$index,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectManualTarget(bound: DoubleRect) {
|
||||
val param = SpotLightTarget()
|
||||
param.targetRect = bound
|
||||
param.targetType = TargetType.RECT
|
||||
IntelligentFlightManager.getInstance().spotLightManager.updateMissionTarget(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectManualTarget:$bound"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectManualTarget:$bound,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun confirmTarget() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.confirmTarget(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("confirmTarget"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("confirmTarget,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopSpotlight() {
|
||||
IntelligentFlightManager.getInstance().spotLightManager.stopMission(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopSpotlight"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopSpotlight,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun startSmartTrack() {
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.startMission(null, null, object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("startSmartTrack"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("startSmartTrack,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun stopSmartTrack() {
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.stopMission(object :
|
||||
CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("stopSmartTrack"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("stopSmartTrack,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun selectTrackingTarget(index: Int) {
|
||||
val param = SmartTrackTarget()
|
||||
param.index = index
|
||||
IntelligentFlightManager.getInstance().smartTrackMissionManager.updateMissionTarget(param,
|
||||
object : CommonCallbacks.CompletionCallback {
|
||||
override fun onSuccess() {
|
||||
toastResult?.postValue(DJIToastResult.success("selectTrackingTarget:$index"))
|
||||
}
|
||||
|
||||
override fun onFailure(error: IDJIError) {
|
||||
toastResult?.postValue(DJIToastResult.failed("selectTrackingTarget:$index,$error"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user