diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..847e875 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #FF000000 + #FFFFFFFF + + #1D55C6 + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..847e875 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #FF000000 + #FFFFFFFF + + #1D55C6 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..bbeaca5 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,28 @@ + + + 12sp + 14sp + 16sp + 18sp + + + 1dp + 3dp + 5dp + 7dp + 10dp + 20dp + 25dp + 30dp + 40dp + 45dp + 50dp + 60dp + 75dp + + + 1px + + + 55dp + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..847e875 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #FF000000 + #FFFFFFFF + + #1D55C6 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..bbeaca5 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,28 @@ + + + 12sp + 14sp + 16sp + 18sp + + + 1dp + 3dp + 5dp + 7dp + 10dp + 20dp + 25dp + 30dp + 40dp + 45dp + 50dp + 60dp + 75dp + + + 1px + + + 55dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..be044ba --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + STC + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..f7b2149 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,93 @@ +import java.text.SimpleDateFormat + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "com.casic.app.safetreecontroller" + minSdkVersion 23 + targetSdkVersion 33 + versionCode 1000 + versionName "1.0.0.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + kotlin { + experimental { + coroutines 'enable' + } + } + + buildFeatures { + viewBinding true + } + + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "SafeTreeController_" + getBuildDate() + "_" + defaultConfig.versionName + ".apk" + } + } +} + +static def getBuildDate() { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA) + return dateFormat.format(System.currentTimeMillis()) +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + //基础依赖库 + implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.0.11' + implementation 'androidx.core:core-ktx:1.9.0' + def base_version = "1.6.1" + implementation "androidx.appcompat:appcompat:${base_version}" + implementation "com.google.android.material:material:${base_version}" + //Google官方授权框架 + implementation 'pub.devrel:easypermissions:3.0.0' + //沉浸式状态栏。基础依赖包,必须要依赖 + implementation 'com.gyf.immersionbar:immersionbar:3.0.0' + def vm_version = "2.5.1" + //Kotlin协程 + implementation "androidx.lifecycle:lifecycle-runtime-ktx:${vm_version}" + //MVVM+LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${vm_version}" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + //返回值转换器 + implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.3.0' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + //okhttp3日志拦截器 + implementation 'com.squareup.okhttp3:logging-interceptor:4.6.0' + //网络请求和接口封装 + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' + //官方Json解析库 + implementation 'com.google.code.gson:gson:2.10.1' + //视频播放器,RTSP流 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.4.0-release-jitpack' + //是否需要ExoPlayer模式 + implementation 'com.github.CarGuo.GSYVideoPlayer:GSYVideoPlayer-exo2:v8.4.0-release-jitpack' + //更多ijk的编码支持 + implementation 'com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-ex_so:v8.4.0-release-jitpack' + //TCP + implementation 'io.netty:netty-all:4.1.23.Final' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c654d14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt new file mode 100644 index 0000000..83ea1f8 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/base/BaseApplication.kt @@ -0,0 +1,20 @@ +package com.casic.app.safetreecontroller.base + +import android.app.Application +import com.pengxh.kt.lite.utils.SaveKeyValues +import kotlin.properties.Delegates + +class BaseApplication : Application() { + + companion object { + private var application: BaseApplication by Delegates.notNull() + + fun get() = application + } + + override fun onCreate() { + super.onCreate() + application = this + SaveKeyValues.initSharedPreferences(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt new file mode 100644 index 0000000..4beefef --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ByteArray.kt @@ -0,0 +1,21 @@ +package com.casic.app.safetreecontroller.extensions + +fun ByteArray.handleGasConcentration(): Long { + //负值需要计算补码 + val x = if (this[2] < 0) { + this[2].toInt() and 0xFF + } else { + this[2].toInt() + } + val y = if (this[3] < 0) { + this[3].toInt() and 0xFF + } else { + this[3].toInt() + } + val z = if (this[4] < 0) { + this[4].toInt() and 0xFF + } else { + this[4].toInt() + } + return (x * 65536 + y * 256 + z).toLong() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt new file mode 100644 index 0000000..3f1b960 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/extensions/ViewGroup.kt @@ -0,0 +1,19 @@ +package com.casic.app.safetreecontroller.extensions + +import android.app.Activity +import android.view.ViewGroup +import androidx.annotation.ColorRes +import com.gyf.immersionbar.ImmersionBar +import com.pengxh.kt.lite.extensions.convertColor +import com.pengxh.kt.lite.extensions.getStatusBarHeight + +fun ViewGroup.initImmersionBar(activity: Activity, isDarkFont: Boolean, @ColorRes color: Int) { + ImmersionBar.with(activity) + .statusBarDarkFont(isDarkFont) + .statusBarColorInt(color.convertColor(activity)) + .init() + //根据不同设备状态栏高度设置statusBarView高度 + val statusBarHeight = activity.getStatusBarHeight() + this.setPadding(0, statusBarHeight, 0, 0) + this.requestLayout() +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt new file mode 100644 index 0000000..2c76c8c --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ConnectionState.kt @@ -0,0 +1,23 @@ +package com.casic.app.safetreecontroller.tcp + +enum class ConnectionState { + /** + * 正在连接 + * */ + CONNECTING, + + /** + * 已连接 + * */ + CONNECTED, + + /** + * 已断开连接 + * */ + DISCONNECTED, + + /** + * 发生错误 + * */ + ERROR +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt new file mode 100644 index 0000000..e7ebe2f --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/ISocketConnectionListener.kt @@ -0,0 +1,14 @@ +package com.casic.app.safetreecontroller.tcp + +interface ISocketConnectionListener { + + /** + * Socket连接状态 + * */ + fun onServiceStatusConnectChanged(state: ConnectionState) + + /** + * Socket数据 + * */ + fun onMessageResponse(bytes: ByteArray?) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt new file mode 100644 index 0000000..23b5cc0 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/SocketChannelHandler.kt @@ -0,0 +1,29 @@ +package com.casic.app.safetreecontroller.tcp + +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler + +class SocketChannelHandler(private val listener: ISocketConnectionListener) : + SimpleChannelInboundHandler() { + + override fun channelActive(ctx: ChannelHandlerContext) { + super.channelActive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.CONNECTED) + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + super.channelInactive(ctx) + listener.onServiceStatusConnectChanged(ConnectionState.DISCONNECTED) + } + + override fun channelRead0(ctx: ChannelHandlerContext, data: ByteArray?) { + listener.onMessageResponse(data) + } + + override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { + super.exceptionCaught(ctx, cause) + cause.printStackTrace() + ctx.close() + listener.onServiceStatusConnectChanged(ConnectionState.ERROR) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt new file mode 100644 index 0000000..519b1ce --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/tcp/TcpClient.kt @@ -0,0 +1,73 @@ +package com.casic.app.safetreecontroller.tcp + +import android.util.Log +import io.netty.bootstrap.Bootstrap +import io.netty.channel.AdaptiveRecvByteBufAllocator +import io.netty.channel.Channel +import io.netty.channel.ChannelFuture +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelInitializer +import io.netty.channel.ChannelOption +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.SocketChannel +import io.netty.channel.socket.nio.NioSocketChannel +import io.netty.handler.codec.bytes.ByteArrayDecoder +import io.netty.handler.codec.bytes.ByteArrayEncoder +import io.netty.handler.timeout.IdleStateHandler + +class TcpClient(private val socketConnectionListener: ISocketConnectionListener) { + + private val kTag = "TcpClient" + private var nioEventLoopGroup: NioEventLoopGroup? = null + private var channel: Channel? = null + + fun startConnectTcpServer(hostname: String, port: Int) { + nioEventLoopGroup = NioEventLoopGroup() + val bootstrap = Bootstrap() + bootstrap.group(nioEventLoopGroup) //设置的一系列连接参数操作等 + .channel(NioSocketChannel::class.java) + .option(ChannelOption.TCP_NODELAY, true) //无阻塞 + .option(ChannelOption.SO_KEEPALIVE, true) //长连接 + .option( + ChannelOption.RCVBUF_ALLOCATOR, + AdaptiveRecvByteBufAllocator(5000, 5000, 8000) + ) //接收缓冲区 最小值太小时数据接收不全 + .handler(object : ChannelInitializer() { + override fun initChannel(channel: SocketChannel) { + val pipeline = channel.pipeline() + //参数1:代表读套接字超时的时间,没收到数据会触发读超时回调; + //参数2:代表写套接字超时时间,没进行写会触发写超时回调; + //参数3:将在未执行读取或写入时触发超时回调,0代表不处理; + //读超时尽量设置大于写超时,代表多次写超时时写心跳包,多次写了心跳数据仍然读超时代表当前连接错误,即可断开连接重新连接 + pipeline.addLast(IdleStateHandler(60, 10, 0)) + pipeline.addLast(ByteArrayDecoder()) + pipeline.addLast(ByteArrayEncoder()) + pipeline.addLast(SocketChannelHandler(socketConnectionListener)) + } + }) + try { + //连接监听 + val channelFuture = bootstrap.connect(hostname, port) + .addListener(object : ChannelFutureListener { + override fun operationComplete(channelFuture: ChannelFuture) { + if (channelFuture.isSuccess) { + channel = channelFuture.channel() + } + } + }).sync() + // 等待连接关闭 + channelFuture.channel().closeFuture().sync() + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun sendCommand(bytes: ByteArray) { + channel?.writeAndFlush(bytes) + } + + fun disconnect() { + Log.d(kTag, "disconnect ===> 断开连接") + nioEventLoopGroup?.shutdownGracefully() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt new file mode 100644 index 0000000..35879e3 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/CommandCreator.kt @@ -0,0 +1,62 @@ +package com.casic.app.safetreecontroller.utils + +object CommandCreator { + /** + * 查询甲烷浓度指令 + */ + fun createMethaneCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x95.toByte(), 0x00, 0x00, 0x96.toByte()) + } + + /** + * 打开激光 + */ + fun createOpenLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x01, 0x00, 0x93.toByte()) + } + + /** + * 关闭激光 + */ + fun createCloseLightCommand(): ByteArray { + return byteArrayOf(0xAA.toByte(), 0x01, 0x00, 0x91.toByte(), 0x00, 0x00, 0x92.toByte()) + } + + /** + * 设置甲烷阈值 + */ + fun setMethaneThresholdCommandCommand(threshold: Int): ByteArray? { + for (i in 0..255) { + for (j in 0..255) { + for (k in 0..255) { + //浓度值 = 数据码1 * 65536 + 数据码2 * 256 + 数据码3 + if (65536 * i + 256 * j + k == threshold) { + val result = ByteArray(3) + result[0] = i.toByte() + result[1] = j.toByte() + result[2] = k.toByte() + + //计算校验位。校验位先置为0x00 + val bytes = byteArrayOf( + 0xAA.toByte(), + 0x01, + 0x00, + 0x94.toByte(), + result[0], + result[1], + result[2], + 0x00.toByte() + ) + var sum = 0 + for (l in 1 until bytes.size - 1) { + sum += bytes[l] + } + bytes[7] = sum.toByte() + return bytes + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt new file mode 100644 index 0000000..04e4013 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/utils/VideoPlayerManager.kt @@ -0,0 +1,76 @@ +package com.casic.app.safetreecontroller.utils + +import com.shuyu.gsyvideoplayer.GSYVideoManager +import com.shuyu.gsyvideoplayer.model.VideoOptionModel +import com.shuyu.gsyvideoplayer.utils.GSYVideoType +import com.shuyu.gsyvideoplayer.video.StandardGSYVideoPlayer +import tv.danmaku.ijk.media.player.IjkMediaPlayer + +object VideoPlayerManager { + fun setGSYVideoPlayerOptions(videoPlayer: StandardGSYVideoPlayer, url: String) { + val list = ArrayList() + + //开启软解码,硬解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0)) + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mmediacodec-handle-resolution-change", 0 + ) + ) + //软解码:1、打开,0、关闭 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "videotoolbox", 1)) + + // 每处理一个packet之后刷新io上下文 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1)) + //为什么拖动视屏会弹回来,因为ijk的FFMPEG对关键帧问题。 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", 1)) + + // 设置播放前的最大探测时间(单位毫秒),尝试减小这个值 + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_setup_timeout", "100" + ) + ) // 示例值,根据实际情况调整 + + // 视频帧处理不过来的时候丢弃一些帧达到同步的效果(如果视频帧数太高导致卡画面不同步) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30)) + /***************rtsp 配置 */ + list.add( + VideoOptionModel( + IjkMediaPlayer.OPT_CATEGORY_FORMAT, "allowed_media_types", "video" + ) + ) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", 2000)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", -1)) + // 或者尝试设置较小的缓冲大小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "buffer_size", 1316)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp")) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_flags", "prefer_tcp")) + //是否开启缓冲 设置无packet缓存 尝试减小或关闭预缓冲(1开启,0关闭)(谨慎设置,可能会增加卡顿风险)(一般直播项目会开启,达到秒开的效果,不过带来了播放丢帧卡顿的体验) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1)) //是否限制输入缓存数,无限读 + // 设置播放前的探测时间 1,达到首屏秒开效果 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1)) + // 设置播放前的最大探测时间 (100未测试是否是最佳值) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzemaxduration", 100)) + //设置无packet缓存 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer")) + //分析码流时长:默认1024*1000 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzedmaxduration", 100)) + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240)) + //设置是否开启变调 isModifyTone?0:1 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "soundtouch", 1)) + //设置是否开启环路过滤: 0开启,画面质量高,解码开销大,48关闭,画面质量差点,解码开销小 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48)) + //播放重连次数 + list.add(VideoOptionModel(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "reconnect", 5)) + /***************rtsp 配置 */ + // 应用这些配置到GsyVideoPlayer + GSYVideoManager.instance().optionModelList = list + + GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_16_9) + videoPlayer.isReleaseWhenLossAudio = false + videoPlayer.setUp(url, false, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt new file mode 100644 index 0000000..bee5e67 --- /dev/null +++ b/app/src/main/java/com/casic/app/safetreecontroller/view/MainActivity.kt @@ -0,0 +1,173 @@ +package com.casic.app.safetreecontroller.view + +import android.net.ConnectivityManager +import android.net.Network +import android.net.wifi.WifiManager +import android.os.Bundle +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.KeyEvent +import android.view.View +import android.widget.LinearLayout +import androidx.fragment.app.Fragment +import com.casic.app.safetreecontroller.R +import com.casic.app.safetreecontroller.adapter.TabPageViewAdapter +import com.casic.app.safetreecontroller.databinding.ActivityMainBinding +import com.casic.app.safetreecontroller.extensions.handleGasConcentration +import com.casic.app.safetreecontroller.extensions.initImmersionBar +import com.casic.app.safetreecontroller.fragments.DeviceControllerFragment +import com.casic.app.safetreecontroller.fragments.MethaneDataFragment +import com.casic.app.safetreecontroller.fragments.OtherSettingsFragment +import com.casic.app.safetreecontroller.tcp.ConnectionState +import com.casic.app.safetreecontroller.tcp.ISocketConnectionListener +import com.casic.app.safetreecontroller.tcp.TcpClient +import com.casic.app.safetreecontroller.utils.CommandCreator +import com.casic.app.safetreecontroller.utils.LocaleConstant +import com.casic.app.safetreecontroller.utils.VideoPlayerManager +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.getSystemService +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.utils.WeakReferenceHandler + + +class MainActivity : KotlinBaseActivity(), Handler.Callback, + ISocketConnectionListener { + + companion object { + lateinit var weakReferenceHandler: WeakReferenceHandler + } + + private val kTag = "MainActivity" + private val context = this + private val fragmentPages by lazy { ArrayList() } + private val pageTitles = arrayOf("云台控制", "甲烷数据", "其他设置") + private val tcpClient by lazy { TcpClient(this) } + private val connectivityManager by lazy { getSystemService() } + private val wifiManager by lazy { getSystemService()!! } + private var clickTime: Long = 0 + + init { + fragmentPages.add(DeviceControllerFragment()) + fragmentPages.add(MethaneDataFragment()) + fragmentPages.add(OtherSettingsFragment()) + } + + override fun handleMessage(msg: Message): Boolean { + when (msg.what) { + LocaleConstant.PLAY_RTSP_CODE -> binding.rtspPlayerView.startPlayLogic() + } + return true + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + weakReferenceHandler = WeakReferenceHandler(this) + + //动态设置rtspPlayerView宽高 + val rtspViewParams = binding.rtspPlayerView.layoutParams as LinearLayout.LayoutParams + val videoWidth = getScreenWidth() + val videoHeight = videoWidth * (9f / 16) + rtspViewParams.width = videoWidth + rtspViewParams.height = videoHeight.toInt() + binding.rtspPlayerView.layoutParams = rtspViewParams + binding.rtspPlayerView.titleTextView.visibility = View.GONE + binding.rtspPlayerView.backButton.visibility = View.GONE + binding.rtspPlayerView.fullscreenButton.visibility = View.GONE + VideoPlayerManager.setGSYVideoPlayerOptions( + binding.rtspPlayerView, LocaleConstant.SUB_RTSP_URL + ) + + binding.viewPager.adapter = TabPageViewAdapter( + supportFragmentManager, fragmentPages, pageTitles + ) + //绑定 + binding.tabLayout.setupWithViewPager(binding.viewPager) + } + + override fun initEvent() { + + } + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + if (connectivityManager?.bindProcessToNetwork(network) == true) { + "安全树连接成功".show(context) + + /** + * TODO 连接TCP板子,获取甲烷数据 + * */ + tcpClient.startConnectTcpServer("", 0) + } + } + + override fun onUnavailable() { + + } + } + + override fun initViewBinding(): ActivityMainBinding { + return ActivityMainBinding.inflate(layoutInflater) + } + + override fun observeRequestState() { + + } + + override fun setupTopBarLayout() { + binding.rootView.initImmersionBar(this, false, R.color.mainThemeColor) + } + + override fun onServiceStatusConnectChanged(state: ConnectionState) { + when (state) { + ConnectionState.CONNECTING -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接中") + } + + ConnectionState.CONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 已连接") + tcpClient.sendCommand(CommandCreator.createMethaneCommand()) + } + + ConnectionState.DISCONNECTED -> { + Log.d(kTag, "onServiceStatusConnectChanged: 断开连接,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + + ConnectionState.ERROR -> { + Log.d(kTag, "onServiceStatusConnectChanged: 连接出错,开始重连") + tcpClient.startConnectTcpServer("", 0) + } + } + } + + override fun onMessageResponse(bytes: ByteArray?) { + Log.d(kTag, bytes.contentToString()) + if (bytes == null) { + return + } + if (bytes.size == 7) { + val concentration = bytes.handleGasConcentration() + Log.d(kTag, "onMessageResponse: $concentration") + } + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (System.currentTimeMillis() - clickTime > 2000) { + "再试一次退出应用".show(this) + clickTime = System.currentTimeMillis() + return true + } else { + super.onKeyDown(keyCode, event) + } + } + return super.onKeyDown(keyCode, event) + } + + override fun onDestroy() { + super.onDestroy() + tcpClient.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_solid_layout_blue_10.xml b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml new file mode 100644 index 0000000..b7e06e3 --- /dev/null +++ b/app/src/main/res/drawable/bg_solid_layout_blue_10.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..2942b80 --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..db4941d --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/selector_direction_button.xml b/app/src/main/res/drawable/selector_direction_button.xml new file mode 100644 index 0000000..a1bc9ce --- /dev/null +++ b/app/src/main/res/drawable/selector_direction_button.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_scale_button.xml b/app/src/main/res/drawable/selector_scale_button.xml new file mode 100644 index 0000000..4efba9d --- /dev/null +++ b/app/src/main/res/drawable/selector_scale_button.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c92b010 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d --- /dev/null +++ b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d --- /dev/null +++ b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 --- /dev/null +++ b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 --- /dev/null +++ b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp Binary files differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 --- /dev/null +++ b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp Binary files differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..847e875 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #FF000000 + #FFFFFFFF + + #1D55C6 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..bbeaca5 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,28 @@ + + + 12sp + 14sp + 16sp + 18sp + + + 1dp + 3dp + 5dp + 7dp + 10dp + 20dp + 25dp + 30dp + 40dp + 45dp + 50dp + 60dp + 75dp + + + 1px + + + 55dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..be044ba --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + STC + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bd1487a --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + + + + + + + + + + + + + + + +