防止apc线程池爆了

This commit is contained in:
cxf 2026-06-28 13:47:59 +08:00
parent d6a6c505a1
commit 10857ce335
31 changed files with 2135 additions and 893 deletions

View File

@ -16,5 +16,22 @@
"outputFile": "app-release.apk" "outputFile": "app-release.apk"
} }
], ],
"elementType": "File" "elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 24
} }

View File

@ -14,6 +14,8 @@ import android.view.WindowManager
import android.widget.Button import android.widget.Button
import android.widget.Toast import android.widget.Toast
import android.widget.TextView import android.widget.TextView
import com.aros.apron.tools.ToastUtil
import dji.sdk.keyvalue.key.RemoteControllerKey
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
@ -134,7 +136,7 @@ open class MainActivity : BaseActivity() {
var isAppStarted: Boolean = false var isAppStarted: Boolean = false
var streamReceive: Boolean = false var streamReceive: Boolean = false
var isscousse: Boolean=false; var isscousse: Boolean = false;
private var instance: MainActivity? = null private var instance: MainActivity? = null
fun getInstance(): MainActivity? { fun getInstance(): MainActivity? {
@ -653,10 +655,11 @@ open class MainActivity : BaseActivity() {
StickManager.getInstance().enableVirtualStick1() StickManager.getInstance().enableVirtualStick1()
} }
// btn_test3?.setOnClickListener { btn_test3 = findViewById(R.id.btn_test3)
// StreamManager.getInstance().startstream() btn_test3?.setOnClickListener {
// // DJI按钮方式急停测试
// } //FlightManager.getInstance().emergencyHoverByPauseButton(null);
}
initClickListener() initClickListener()
@ -819,7 +822,9 @@ open class MainActivity : BaseActivity() {
GeoidManager.getInstance().init(this) GeoidManager.getInstance().init(this)
MissionV3Manager.getInstance().initMissionManager() MissionV3Manager.getInstance().initMissionManager()
enableStream() enableStream()
initFpvStream() initFpvStream()
startVtxHeartbeat() startVtxHeartbeat()
SpeakerManager.getInstance().addMegaphoneInfoListener() SpeakerManager.getInstance().addMegaphoneInfoListener()
GimbalManager.getInstance().setmode() GimbalManager.getInstance().setmode()
@ -847,15 +852,15 @@ open class MainActivity : BaseActivity() {
// 每个 tag 离降落点的偏移(米):[dx右, dy前] // 每个 tag 离降落点的偏移(米):[dx右, dy前]
val tagOffsetMap = mapOf( val tagOffsetMap = mapOf(
248 to doubleArrayOf(-0.262, 0.052), 248 to doubleArrayOf(-0.262, 0.052),
247 to doubleArrayOf( 0.267, 0.052), 247 to doubleArrayOf(0.267, 0.052),
246 to doubleArrayOf(-0.052, 0.0), 246 to doubleArrayOf(-0.052, 0.0),
244 to doubleArrayOf( 0.056, 0.0), 244 to doubleArrayOf(0.056, 0.0),
242 to doubleArrayOf(-0.262, -0.194), 242 to doubleArrayOf(-0.262, -0.194),
241 to doubleArrayOf( 0.267, -0.192), 241 to doubleArrayOf(0.267, -0.192),
245 to doubleArrayOf( 0.0, -0.404), 245 to doubleArrayOf(0.0, -0.404),
250 to doubleArrayOf(-0.262, -0.483), 250 to doubleArrayOf(-0.262, -0.483),
249 to doubleArrayOf( 0.267, -0.480) 249 to doubleArrayOf(0.267, -0.480)
) )
ApriltagDetector.getInstance().setTagOffsets(tagOffsetMap) ApriltagDetector.getInstance().setTagOffsets(tagOffsetMap)
@ -889,7 +894,7 @@ open class MainActivity : BaseActivity() {
2 -> StreamManager.getInstance().startLiveWithCustom() 2 -> StreamManager.getInstance().startLiveWithCustom()
else -> StreamManager.getInstance().startLiveWithCustom() else -> StreamManager.getInstance().startLiveWithCustom()
} }
// SimplePortScanner.getInstance().startScan() // SimplePortScanner.getInstance().startScan()
}, 5000) }, 5000)
LogUtil.log(TAG, "推流类型:" + PreferenceUtils.getInstance().customStreamType) LogUtil.log(TAG, "推流类型:" + PreferenceUtils.getInstance().customStreamType)
@ -944,7 +949,7 @@ open class MainActivity : BaseActivity() {
2 -> StreamManager.getInstance().startLiveWithCustom() 2 -> StreamManager.getInstance().startLiveWithCustom()
else -> StreamManager.getInstance().startLiveWithCustom() else -> StreamManager.getInstance().startLiveWithCustom()
} }
// SimplePortScanner.getInstance().startScan() // SimplePortScanner.getInstance().startScan()
}, 5000) }, 5000)
LogUtil.log(TAG, "推流类型:" + PreferenceUtils.getInstance().customStreamType) LogUtil.log(TAG, "推流类型:" + PreferenceUtils.getInstance().customStreamType)
@ -1002,7 +1007,7 @@ open class MainActivity : BaseActivity() {
2 -> StreamManager.getInstance().startLiveWithCustom() 2 -> StreamManager.getInstance().startLiveWithCustom()
else -> StreamManager.getInstance().startLiveWithCustom() else -> StreamManager.getInstance().startLiveWithCustom()
} }
// SimplePortScanner.getInstance().startScan() // SimplePortScanner.getInstance().startScan()
}, 5000) }, 5000)
LogUtil.log(TAG, "推流类型:" + PreferenceUtils.getInstance().customStreamType) LogUtil.log(TAG, "推流类型:" + PreferenceUtils.getInstance().customStreamType)
@ -1043,9 +1048,9 @@ open class MainActivity : BaseActivity() {
try { try {
Synchronizedstatus.setIsruningframe(true) Synchronizedstatus.setIsruningframe(true)
if(!isscousse){ if (!isscousse) {
isscousse=true; isscousse = true;
LogUtil.log(TAG,"mix视频帧回调了") LogUtil.log(TAG, "mix视频帧回调了")
} }
if (startArucoType == 1) { if (startArucoType == 1) {
@ -1102,8 +1107,6 @@ open class MainActivity : BaseActivity() {
try { try {
Synchronizedstatus.setIsruningframe(true) Synchronizedstatus.setIsruningframe(true)
if (startArucoType == 1) { if (startArucoType == 1) {
Aprondown.getInstance()?.detectArucoTags( Aprondown.getInstance()?.detectArucoTags(
height, height,
@ -1138,7 +1141,7 @@ open class MainActivity : BaseActivity() {
@SuppressLint("SuspiciousIndentation") @SuppressLint("SuspiciousIndentation")
private fun initFpvStream() { private fun initFpvStream() {
cameraManager.addFrameListener( cameraManager.addFrameListener(
ComponentIndexType.PORT_1, ComponentIndexType.FPV,
ICameraStreamManager.FrameFormat.YUV420_888 ICameraStreamManager.FrameFormat.YUV420_888
) { frameData, _, _, width, height, _ -> ) { frameData, _, _, width, height, _ ->
Movement.getInstance().isVtx = true Movement.getInstance().isVtx = true
@ -1152,24 +1155,34 @@ open class MainActivity : BaseActivity() {
try { try {
Synchronizedstatus.setIsruningframe(true) Synchronizedstatus.setIsruningframe(true)
if(!isscousse){ if (!isscousse) {
isscousse=true; isscousse = true;
LogUtil.log(TAG,"port视频帧回调了") LogUtil.log(TAG, "port视频帧回调了")
} }
if (startArucoType == 1) { if (startArucoType == 1) {
ApronArucoDetect.getInstance()?.detectArucoTags(
height,
width,
frameData,
dictionary
)
// ApronArucodownmany.getInstance()?.detectArucoTags( // ApronArucodownmany.getInstance()?.detectArucoTags(
// height, // height,
// width, // width,
// frameData, // frameData,
// dictionary // dictionary
// ) // )
AprilTagPort.getInstance().processFrame(
frameData, // AprilTagPort.getInstance().processFrame(
width, // frameData,
height // width,
) // height
// )
} else if (startArucoType == 2) { } else if (startArucoType == 2) {
AlternateArucoDetect.getInstance()?.detectArucoTags( AlternateArucoDetect.getInstance()?.detectArucoTags(
height, height,
@ -1178,12 +1191,23 @@ open class MainActivity : BaseActivity() {
dictionary dictionary
) )
} else if (startArucoType == 3) { } else if (startArucoType == 3) {
ApronArucoDetect.getInstance()?.detectForceTriggerTags(
height,
width,
frameData,
dictionary
)
//
// ApronArucodownmany.getInstance()?.detectForceTriggerTags( // ApronArucodownmany.getInstance()?.detectForceTriggerTags(
// height, // height,
// width, // width,
// frameData, // frameData,
// dictionary // dictionary
// ) // )
} }
} finally { } finally {
Synchronizedstatus.setIsruningframe(false) Synchronizedstatus.setIsruningframe(false)
@ -1212,9 +1236,9 @@ open class MainActivity : BaseActivity() {
try { try {
Synchronizedstatus.setIsruningframe(true) Synchronizedstatus.setIsruningframe(true)
if(!isscousse){ if (!isscousse) {
isscousse=true; isscousse = true;
LogUtil.log(TAG,"fpv视频帧回调了") LogUtil.log(TAG, "fpv视频帧回调了")
} }
if (startArucoType == 1) { if (startArucoType == 1) {
@ -1300,7 +1324,7 @@ open class MainActivity : BaseActivity() {
LogUtil.log(TAG, "取消降落,识别机库二维码") LogUtil.log(TAG, "取消降落,识别机库二维码")
if (PreferenceUtils.getInstance().cameraLocationType == 3) { if (PreferenceUtils.getInstance().cameraLocationType == 3) {
Handler().postDelayed(Runnable { Handler().postDelayed(Runnable {
if (!ApronArucodownmany.getInstance().isTriggerSuccess) { if (!AprilTagPort.getInstance().isTriggerSuccess) {
LogUtil.log(TAG, "图传异常:飞往备降点") LogUtil.log(TAG, "图传异常:飞往备降点")
//测试图传丢失 //测试图传丢失
AlternateLandingManager.getInstance().startTaskProcess(null) AlternateLandingManager.getInstance().startTaskProcess(null)
@ -1309,7 +1333,10 @@ open class MainActivity : BaseActivity() {
} else if (PreferenceUtils.getInstance().cameraLocationType == 4 || PreferenceUtils.getInstance().cameraLocationType == 5) { } else if (PreferenceUtils.getInstance().cameraLocationType == 4 || PreferenceUtils.getInstance().cameraLocationType == 5) {
Handler().postDelayed(Runnable { Handler().postDelayed(Runnable {
if (!Aprongim.getInstance().isTriggerSuccess) { if (!Aprongim.getInstance().isTriggerSuccess) {
LogUtil.log(TAG, "图传异常:飞往备降点"+ Movement.getInstance().isVtx) LogUtil.log(
TAG,
"图传异常:飞往备降点" + Movement.getInstance().isVtx
)
//测试图传丢失 //测试图传丢失
AlternateLandingManager.getInstance().startTaskProcess(null) AlternateLandingManager.getInstance().startTaskProcess(null)
} }
@ -1371,7 +1398,7 @@ open class MainActivity : BaseActivity() {
LogUtil.log(TAG, "取消降落,识别备降点二维码") LogUtil.log(TAG, "取消降落,识别备降点二维码")
if (PreferenceUtils.getInstance().cameraLocationType == 3) { if (PreferenceUtils.getInstance().cameraLocationType == 3) {
Handler().postDelayed(Runnable { Handler().postDelayed(Runnable {
if (!ApronArucoDetect.getInstance().isTriggerSuccess) { if (!AprilTagPort.getInstance().isTriggerSuccess) {
LogUtil.log(TAG, "图传异常:飞往备降点") LogUtil.log(TAG, "图传异常:飞往备降点")
//测试图传丢失 //测试图传丢失
AlternateLandingManager.getInstance().startTaskProcess(null) AlternateLandingManager.getInstance().startTaskProcess(null)

View File

@ -62,7 +62,7 @@ public abstract class BaseManager {
mqttMessage.setQos(1); mqttMessage.setQos(1);
org.eclipse.paho.client.mqttv3.IMqttDeliveryToken token = org.eclipse.paho.client.mqttv3.IMqttDeliveryToken token =
MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_SERVICES_REPLY, mqttMessage); MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_SERVICES_REPLY, mqttMessage);
LogUtil.log(TAG, "回复已提交, tid=" + entity.getTid() + ", msgId=" + token.getMessageId() + ", method=" + entity.getMethod()); //LogUtil.log(TAG, "回复已提交, tid=" + entity.getTid() + ", msgId=" + token.getMessageId() + ", method=" + entity.getMethod());
} else { } else {
LogUtil.log(TAG, "回复失败mqtt 未连接, tid=" + entity.getTid()); LogUtil.log(TAG, "回复失败mqtt 未连接, tid=" + entity.getTid());
} }
@ -237,7 +237,7 @@ public abstract class BaseManager {
if( Movement.getInstance().getTask_media_count()!=0){ if( Movement.getInstance().getTask_media_count()!=0){
LogUtil.log(TAG ,"getTask_media_count"+Movement.getInstance().getTask_media_count()); LogUtil.log(TAG ,"getTask_media_count"+Movement.getInstance().getTask_media_count());
} }
LogUtil.log(TAG ,"QWQsendFlightTaskProgress2Server"); // LogUtil.log(TAG ,"QWQsendFlightTaskProgress2Server");
try { try {
if (MqttManager.getInstance().mqttAndroidClient.isConnected()) { if (MqttManager.getInstance().mqttAndroidClient.isConnected()) {
// 必须加 final否则内部类回调里访问不了 // 必须加 final否则内部类回调里访问不了
@ -613,7 +613,9 @@ public abstract class BaseManager {
public boolean getGimbalAndCameraEnabled() { public boolean getGimbalAndCameraEnabled() {
if (!PreferenceUtils.getInstance().getNeedTriggerApronArucoLand() && !PreferenceUtils.getInstance().getNeedTriggerAlterArucoLand() && Movement.getInstance().getGoHomeState() != 2) { if (!PreferenceUtils.getInstance().getNeedTriggerApronArucoLand()
&& !PreferenceUtils.getInstance().getNeedTriggerAlterArucoLand()
&& Movement.getInstance().getGoHomeState() != 2) {
return true; return true;
} else { } else {
LogUtil.log(TAG, "降落时不允许操作云台/相机/虚拟摇杆"); LogUtil.log(TAG, "降落时不允许操作云台/相机/虚拟摇杆");

View File

@ -5,6 +5,7 @@ import android.os.Looper;
import com.aros.apron.constant.AMSConfig; import com.aros.apron.constant.AMSConfig;
import com.aros.apron.tools.LogUtil; import com.aros.apron.tools.LogUtil;
import com.aros.apron.tools.MqttManager;
import com.aros.apron.tools.ToastUtil; import com.aros.apron.tools.ToastUtil;
import org.eclipse.paho.android.service.MqttAndroidClient; import org.eclipse.paho.android.service.MqttAndroidClient;
@ -68,7 +69,11 @@ public class MqttActionCallBack implements IMqttActionListener {
@Override @Override
public void run() { public void run() {
try { try {
mqttAndroidClient.connect(options, null, MqttActionCallBack.this); // 使用MqttManager当前持有的client避免使用已失效的旧引用导致"Invalid ClientHandle"
MqttAndroidClient currentClient = MqttManager.getInstance().mqttAndroidClient;
if (currentClient != null && !currentClient.isConnected()) {
currentClient.connect(MqttManager.getInstance().mMqttConnectOptions, null, MqttActionCallBack.this);
}
} catch (MqttException e) { } catch (MqttException e) {
LogUtil.log(TAG, "mqtt重连异常:" + e.toString()); LogUtil.log(TAG, "mqtt重连异常:" + e.toString());
} }

View File

@ -24,6 +24,7 @@ import com.aros.apron.manager.FlightManager;
import com.aros.apron.manager.FlyToPointManager; import com.aros.apron.manager.FlyToPointManager;
import com.aros.apron.manager.GimbalManager; import com.aros.apron.manager.GimbalManager;
import com.aros.apron.manager.MLTEManager; import com.aros.apron.manager.MLTEManager;
import com.aros.apron.manager.MediaManager;
import com.aros.apron.manager.MissionV3Manager; import com.aros.apron.manager.MissionV3Manager;
import com.aros.apron.manager.OSDManager; import com.aros.apron.manager.OSDManager;
import com.aros.apron.manager.PayloadlightManager; import com.aros.apron.manager.PayloadlightManager;
@ -249,6 +250,8 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended {
LogUtil.log(TAG, "收到服务端响应TaskFail" + jsonString); LogUtil.log(TAG, "收到服务端响应TaskFail" + jsonString);
ApronExecutionStatus.getInstance().setServerReplyTaskFail(true); ApronExecutionStatus.getInstance().setServerReplyTaskFail(true);
break; break;
case Constant.TAKEOFF_TO_POINT: case Constant.TAKEOFF_TO_POINT:
// //1.检查图传是否连接 // //1.检查图传是否连接
// MissionDataBean data = new Gson().fromJson(new Gson().toJson(message.getData()), MissionDataBean.class); // MissionDataBean data = new Gson().fromJson(new Gson().toJson(message.getData()), MissionDataBean.class);
@ -680,6 +683,23 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended {
case Constant.HANGXIANTEST: case Constant.HANGXIANTEST:
MLTEManager.getInstance().test(); MLTEManager.getInstance().test();
break; break;
case Constant.RESTART_RTSP:
LogUtil.log(TAG, "收到:重启视频推流" + jsonString);
//重启视频推流
StreamManager.getInstance().restart(message);
break;
case Constant.RTMP_PUSH:
LogUtil.log(TAG, "收到RTMP推流" + jsonString);
StreamManager.getInstance().startLiveWithRTMP(message);
break;
case Constant.STOP_RTMP:
LogUtil.log(TAG, "收到停止RTMP推流" + jsonString);
StreamManager.getInstance().stopRTMP(message);
break;
case Constant.FLY_TO_LAND_POINT:
LogUtil.log(TAG,"收到一键备降点任务"+jsonString);
break;
@ -1036,9 +1056,9 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended {
@Override @Override
public void deliveryComplete(IMqttDeliveryToken token) { public void deliveryComplete(IMqttDeliveryToken token) {
try { try {
LogUtil.log(TAG, "消息送达确认, msgId=" + token.getMessageId() // LogUtil.log(TAG, "消息送达确认, msgId=" + token.getMessageId()
+ ", complete=" + token.isComplete() // + ", complete=" + token.isComplete()
+ ", topics=" + java.util.Arrays.toString(token.getTopics())); // + ", topics=" + java.util.Arrays.toString(token.getTopics()));
} catch (Exception e) { } catch (Exception e) {
LogUtil.log(TAG, "deliveryComplete 异常: " + e); LogUtil.log(TAG, "deliveryComplete 异常: " + e);
} }

View File

@ -366,6 +366,23 @@ public class Constant {
* 航线测试 * 航线测试
*/ */
public static final String HANGXIANTEST="hangxiantest"; public static final String HANGXIANTEST="hangxiantest";
public static final String RESTART_RTSP="restart_rtsp";
public static final String FLY_TO_LAND_POINT="fly_to_land_point";
/**
* RTMP推流
*/
public static final String RTMP_PUSH="push_rtmp";
public static final String STOP_RTMP="stop_rtmp";
} }

View File

@ -2,7 +2,9 @@ package com.aros.apron.entity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Queue;
import dji.sdk.wpmz.value.mission.WaylineExecuteWaypoint; import dji.sdk.wpmz.value.mission.WaylineExecuteWaypoint;
import dji.sdk.wpmz.value.mission.WaylineWaypoint; import dji.sdk.wpmz.value.mission.WaylineWaypoint;
@ -41,4 +43,46 @@ public class CurrentWayline {
public void setRouteWaypoints(List<WaylineExecuteWaypoint> waypoints) { public void setRouteWaypoints(List<WaylineExecuteWaypoint> waypoints) {
this.routeWaypoints = waypoints; this.routeWaypoints = waypoints;
} }
/** 解析后的航点列表(含经纬度、高度、悬停时间、到达/离开状态) */
private List<ParsedWaypoint> parsedWaypoints = new ArrayList<>();
/** 航点队列用于FIFO处理peek=当前目标poll=处理完成出队 */
private final Queue<ParsedWaypoint> waypointQueue = new LinkedList<>();
public List<ParsedWaypoint> getParsedWaypoints() {
return parsedWaypoints;
}
public void setParsedWaypoints(List<ParsedWaypoint> parsedWaypoints) {
this.parsedWaypoints = parsedWaypoints;
// 同步初始化队列清空旧数据新航点全部入队
waypointQueue.clear();
if (parsedWaypoints != null) {
waypointQueue.addAll(parsedWaypoints);
}
}
/** 获取队列直接操作用于peek/poll */
public Queue<ParsedWaypoint> getWaypointQueue() {
return waypointQueue;
}
/** 查看当前待处理的航点不出队队列空返回null */
public ParsedWaypoint peekNextWaypoint() {
return waypointQueue.peek();
}
/** 当前航点处理完成,出队 */
public void pollNextWaypoint() {
waypointQueue.poll();
}
/** 根据航点序号查找 */
public ParsedWaypoint getParsedWaypoint(int index) {
if (index >= 0 && index < parsedWaypoints.size()) {
return parsedWaypoints.get(index);
}
return null;
}
} }

View File

@ -56,6 +56,7 @@ public class MessageDown {
public static class Data { public static class Data {
private int result; private int result;
private String url; private String url;
private String rtmp_url;
private int video_quality; private int video_quality;
private int ideo_quality; private int ideo_quality;
private AlternateLandPoint alternate_land_point; private AlternateLandPoint alternate_land_point;
@ -632,6 +633,14 @@ public class MessageDown {
this.url = url; this.url = url;
} }
public String getRtmp_url() {
return rtmp_url;
}
public void setRtmp_url(String rtmp_url) {
this.rtmp_url = rtmp_url;
}
public int getVideo_quality() { public int getVideo_quality() {
return video_quality; return video_quality;
} }

View File

@ -56,7 +56,11 @@ public class Movement {
private int landingPower;//降落电量所需百分比 private int landingPower;//降落电量所需百分比
private int lowBatteryRTHState;//智能低电量返航状态 0未触发智能低电量返航 1触发智能低电量返航飞行器正在倒计时 2执行智能低电量返航 3智能低电量返航被取消 private int lowBatteryRTHState;//智能低电量返航状态 0未触发智能低电量返航 1触发智能低电量返航飞行器正在倒计时 2执行智能低电量返航 3智能低电量返航被取消
private String missionName;//当前正在执行的航线名 private String missionName;//当前正在执行的航线名
private int currentWaypointIndex = 0;//当前航点下标 private int currentWaypointIndex = 0;//当前航点下标DJI回调
private int targetWaypointIndex = 0;//GPS检测目标航点下标自研
private boolean planeWing;//飞机是否在飞 private boolean planeWing;//飞机是否在飞
private boolean isMotorsOn;//电机是否起转 private boolean isMotorsOn;//电机是否起转
@ -2525,6 +2529,14 @@ public class Movement {
this.currentWaypointIndex = currentWaypointIndex; this.currentWaypointIndex = currentWaypointIndex;
} }
public int getTargetWaypointIndex() {
return targetWaypointIndex;
}
public void setTargetWaypointIndex(int targetWaypointIndex) {
this.targetWaypointIndex = targetWaypointIndex;
}
public String getMissionName() { public String getMissionName() {
return missionName; return missionName;
} }

View File

@ -747,7 +747,6 @@ public class CameraManager extends BaseManager {
KeyConnection, ComponentIndexType.PORT_1)); KeyConnection, ComponentIndexType.PORT_1));
if (isConnect != null && isConnect && getGimbalAndCameraEnabled()) { if (isConnect != null && isConnect && getGimbalAndCameraEnabled()) {
if (message != null) { if (message != null) {
int cameraMode = message.getData().getCamera_mode(); int cameraMode = message.getData().getCamera_mode();
// 新增如果当前已经是该模式直接返回成功 // 新增如果当前已经是该模式直接返回成功
if (cameraMode == Movement.getInstance().getCamera_mode()) { if (cameraMode == Movement.getInstance().getCamera_mode()) {
@ -1466,24 +1465,24 @@ public class CameraManager extends BaseManager {
@Override @Override
public void onSuccess() { public void onSuccess() {
// 切换到红外视频源时自动设置调色盘为铁红色6 // 切换到红外视频源时自动设置调色盘为铁红色6
if (type == 3) { // if (type == 3) {
KeyManager.getInstance().setValue( // KeyManager.getInstance().setValue(
KeyTools.createCameraKey(CameraKey.KeyThermalPalette, // KeyTools.createCameraKey(CameraKey.KeyThermalPalette,
ComponentIndexType.PORT_1, // ComponentIndexType.PORT_1,
CameraLensType.CAMERA_LENS_THERMAL), // CameraLensType.CAMERA_LENS_THERMAL),
CameraThermalPalette.find(6), // CameraThermalPalette.find(6),
new CommonCallbacks.CompletionCallback() { // new CommonCallbacks.CompletionCallback() {
@Override // @Override
public void onSuccess() { // public void onSuccess() {
LogUtil.log(TAG, "红外切换成功,已设置调色盘为铁红"); // LogUtil.log(TAG, "红外切换成功,已设置调色盘为铁红");
} // }
//
@Override // @Override
public void onFailure(@NonNull IDJIError error) { // public void onFailure(@NonNull IDJIError error) {
LogUtil.log(TAG, "设置红外调色盘失败:" + getIDJIErrorMsg(error)); // LogUtil.log(TAG, "设置红外调色盘失败:" + getIDJIErrorMsg(error));
} // }
}); // });
} // }
sendMsg2Server(message); sendMsg2Server(message);
} }

View File

@ -14,8 +14,10 @@ import androidx.annotation.Nullable;
import com.aros.apron.base.BaseManager; import com.aros.apron.base.BaseManager;
import com.aros.apron.constant.AMSConfig; import com.aros.apron.constant.AMSConfig;
import com.aros.apron.entity.ApronExecutionStatus; import com.aros.apron.entity.ApronExecutionStatus;
import com.aros.apron.entity.CurrentWayline;
import com.aros.apron.entity.MessageDown; import com.aros.apron.entity.MessageDown;
import com.aros.apron.entity.Movement; import com.aros.apron.entity.Movement;
import com.aros.apron.entity.ParsedWaypoint;
import com.aros.apron.activity.MainActivity; import com.aros.apron.activity.MainActivity;
import com.aros.apron.mix.Aprondown; import com.aros.apron.mix.Aprondown;
import com.aros.apron.mix.Aprongim; import com.aros.apron.mix.Aprongim;
@ -47,6 +49,7 @@ import dji.sdk.keyvalue.key.FlightControllerKey;
import dji.sdk.keyvalue.key.GimbalKey; import dji.sdk.keyvalue.key.GimbalKey;
import dji.sdk.keyvalue.key.KeyTools; import dji.sdk.keyvalue.key.KeyTools;
import dji.sdk.keyvalue.key.ProductKey; import dji.sdk.keyvalue.key.ProductKey;
import dji.sdk.keyvalue.key.RemoteControllerKey;
import dji.sdk.keyvalue.key.RtkMobileStationKey; import dji.sdk.keyvalue.key.RtkMobileStationKey;
import dji.sdk.keyvalue.value.camera.CameraExposureCompensation; import dji.sdk.keyvalue.value.camera.CameraExposureCompensation;
import dji.sdk.keyvalue.value.camera.CameraExposureMode; import dji.sdk.keyvalue.value.camera.CameraExposureMode;
@ -351,9 +354,10 @@ public class FlightManager extends BaseManager {
if (newValue.getAltitude() != null) { if (newValue.getAltitude() != null) {
Movement.getInstance().setElevation(newValue.getAltitude()); Movement.getInstance().setElevation(newValue.getAltitude());
Movement.getInstance().setTask_height(newValue.getAltitude()); Movement.getInstance().setTask_height(newValue.getAltitude());
} }
double distance = LocationUtils.getDistance(String.valueOf(Movement.getInstance().getHomepoint_longitude()), double distance = LocationUtils.getDistance(String.valueOf(Movement.getInstance().getHomepoint_longitude()),
String.valueOf(Movement.getInstance().getHomepoint_latitude()), String.valueOf(Movement.getInstance().getHomepoint_latitude()),
String.valueOf(newValue.getLongitude()), String.valueOf(newValue.getLongitude()),
@ -370,6 +374,67 @@ public class FlightManager extends BaseManager {
Movement.getInstance().setLatitude(newValue.getLatitude()); Movement.getInstance().setLatitude(newValue.getLatitude());
Movement.getInstance().setLongitude(newValue.getLongitude()); Movement.getInstance().setLongitude(newValue.getLongitude());
pushFlightAttitude(); pushFlightAttitude();
//航点到达/离开检测队列FIFOpeek队头处理完poll出队
// 进入距离1m 速度0离开主动(速度>0.2且距离>1m) 兜底(hoverTime+1s)
ParsedWaypoint pw = CurrentWayline.getInstance().peekNextWaypoint();
if (pw != null) {
final ParsedWaypoint finalPw = pw;
final int targetIdx = finalPw.getIndex();
double dist = Gpsdistance.calculateDistance(
newValue.getLatitude(), newValue.getLongitude(),
finalPw.getLatitude(), finalPw.getLongitude());
double speed = Movement.getInstance().getHorizontal_speed();
// 进入距离1m 速度为0 未到达 "进入"并启动兜底定时器
if (dist <= 1.0 && Math.abs(speed) < 0.1 && !finalPw.isReached()) {
finalPw.markReached();
sendWaypointReachOrLeave("0", String.valueOf(targetIdx));
LogUtil.log(TAG, "到达航点[" + targetIdx + "] 距离=" + dist + "m 速度=" + speed + "m/s 悬停=" + finalPw.getHoverTime() + "s");
// 兜底hoverTime+1s 后若仍未离开则强制离开并出队
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!finalPw.isLeft()) {
finalPw.markLeft();
sendWaypointReachOrLeave("1", String.valueOf(targetIdx));
LogUtil.log(TAG, "离开航点[" + targetIdx + "] 兜底超时(hoverTime+1s)");
CurrentWayline.getInstance().pollNextWaypoint();
Movement.getInstance().setTargetWaypointIndex(targetIdx + 1);
}
}, (finalPw.getHoverTime() + 1) * 1000L);
}
// 主动离开已到达未离开 速度>0.2 距离>1m "离开"并出队
if (finalPw.isReached() && !finalPw.isLeft() && speed > 0.2 && dist > 1.0) {
finalPw.markLeft();
sendWaypointReachOrLeave("1", String.valueOf(targetIdx));
LogUtil.log(TAG, "离开航点[" + targetIdx + "] 距离=" + dist + "m 速度=" + speed + "m/s");
CurrentWayline.getInstance().pollNextWaypoint();
Movement.getInstance().setTargetWaypointIndex(targetIdx + 1);
}
}
// ========== 判断是否到达FlyTo目标点 ========== // ========== 判断是否到达FlyTo目标点 ==========
double targetLat = Movement.getInstance().getFlyto_target_latitude(); double targetLat = Movement.getInstance().getFlyto_target_latitude();
@ -1288,11 +1353,15 @@ public class FlightManager extends BaseManager {
heightLogCounter = 0; heightLogCounter = 0;
} }
if (isFlying && (Movement.getInstance().getElevation() < 15 && Movement.getInstance().getUltrasonicHeight() < 50 || forceTriggerDetection) && !isSendDetect) { // 超声波卡住(50dm)飞控高度 <5m 也允许触发
double flyingHeight = Movement.getInstance().getElevation(); double currElevation = Movement.getInstance().getElevation();
int currUlt = Movement.getInstance().getUltrasonicHeight();
boolean altOk = (currElevation < 15 && currUlt < 50) // 正常两个传感器一致
|| (currElevation < 5); // 兜底飞控高度够低跳过超声波
if (isFlying && (altOk || forceTriggerDetection) && !isSendDetect) {
double thresholdMin = triggerToAlternatePoint ? FLYING_HEIGHT_THRESHOLD_MIN_ALTERNATE : FLYING_HEIGHT_THRESHOLD_MIN; double thresholdMin = triggerToAlternatePoint ? FLYING_HEIGHT_THRESHOLD_MIN_ALTERNATE : FLYING_HEIGHT_THRESHOLD_MIN;
if (flyingHeight > thresholdMin || forceTriggerDetection) { if (currElevation > thresholdMin || forceTriggerDetection) {
boolean shouldTriggerDetection; boolean shouldTriggerDetection;
@ -1339,7 +1408,7 @@ public class FlightManager extends BaseManager {
PreferenceUtils.getInstance().setNeedTriggerAlterArucoLand(false); PreferenceUtils.getInstance().setNeedTriggerAlterArucoLand(false);
PreferenceUtils.getInstance().setNeedTriggerApronArucoLand(true); PreferenceUtils.getInstance().setNeedTriggerApronArucoLand(true);
LogUtil.log(TAG, "开始识别机库二维码,椭球高度:" + Movement.getInstance().getElevation() + "" + "--超声波高度:" + Movement.getInstance().getUltrasonicHeight() + "分米"); LogUtil.log(TAG, "开始识别机库二维码,椭球高度:" + Movement.getInstance().getElevation() + "" + "--超声波高度:" + Movement.getInstance().getUltrasonicHeight() + "分米");
sendEvent2Server("开始视觉降落", 1); sendEvent2Server("开始视觉降落"+Movement.getInstance().getCapacity_percent(), 1);
} }
//PerceptionManager.getInstance().setPerceptionEnable(false); //PerceptionManager.getInstance().setPerceptionEnable(false);
@ -1670,17 +1739,23 @@ public class FlightManager extends BaseManager {
if (flightMode != null) { if (flightMode != null) {
switch (flightMode) { switch (flightMode) {
case GO_HOME: case GO_HOME:
KeyManager.getInstance().performAction(createKey(FlightControllerKey.KeyStopGoHome), new CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>() { KeyManager.getInstance().performAction(createKey(FlightControllerKey.KeyStopGoHome), new CommonCallbacks.CompletionCallbackWithParam<EmptyMsg>() {
@Override @Override
public void onSuccess(EmptyMsg emptyMsg) { public void onSuccess(EmptyMsg emptyMsg) {
LogUtil.log(TAG, "紧急悬停,取消返航成功"); //只有在取消返航时才能显示取消返航失败
Movement.getInstance().setMode_code(3);
isGimbalReset = false;
sendMsg2Server(message); sendMsg2Server(message);
resetAircrftLandingStatus(); resetAircrftLandingStatus();
} }
@Override @Override
public void onFailure(@NonNull IDJIError error) { public void onFailure(@NonNull IDJIError error) {
LogUtil.log(TAG, "取消返航执行失败" + error);
//isGimbalReset = false;
sendFailMsg2Server(message, "紧急悬停,取消返航失败:" + getIDJIErrorMsg(error)); sendFailMsg2Server(message, "紧急悬停,取消返航失败:" + getIDJIErrorMsg(error));
resetAircrftLandingStatus();
} }
}); });
break; break;
@ -1770,5 +1845,66 @@ public class FlightManager extends BaseManager {
} }
}); });
// 急停清理视觉降落定时器解除 isStartAruco 入口 guard确保下次可重新触发
ApronArucoDetect.getInstance().stopAndReset();
ApronArucoDetectPort.getInstance().stopAndReset();
Aprongim.getInstance().stopAndReset();
Aprondown.getInstance().stopAndReset();
ApronArucodownmany.getInstance().stopAndReset();
}
// public void emergencyHoverByPauseButton(MessageDown message) {
// LogUtil.log(TAG, "紧急悬停(DJI按钮方式)setValue KeyPauseButtonDown=true");
// KeyManager.getInstance().setValue(createKey(RemoteControllerKey.KeyPauseButtonDown), true, new CommonCallbacks.CompletionCallback() {
// @Override
// public void onSuccess() {
// LogUtil.log(TAG, "紧急悬停(DJI按钮方式) 成功");
// resetAircrftLandingStatus();
// }
//
// @Override
// public void onFailure(@NonNull IDJIError error) {
// LogUtil.log(TAG, "紧急悬停(DJI按钮方式) 失败: " + getIDJIErrorMsg(error));
// }
// });
// }
/**
* 发送航点进入/离开到服务端格式与350demo WaypointEventSender一致
* @param state "0"=进入 "1"=离开
* @param index 航点序号
*/
private void sendWaypointReachOrLeave(String state, String index) {
try {
if (MqttManager.getInstance().mqttAndroidClient != null
&& MqttManager.getInstance().mqttAndroidClient.isConnected()) {
com.google.gson.JsonObject msg = new com.google.gson.JsonObject();
msg.addProperty("msg_type", 60203);
msg.addProperty("result", 1);
msg.addProperty("waypointActionState", state);
msg.addProperty("waypointIndex", index);
org.eclipse.paho.client.mqttv3.MqttMessage mqttMessage =
new org.eclipse.paho.client.mqttv3.MqttMessage(
new Gson().toJson(msg).getBytes("UTF-8"));
mqttMessage.setQos(1);
MqttManager.getInstance().mqttAndroidClient.publish(
AMSConfig.UP_UAV_SERVICES_REPLY,
mqttMessage);
LogUtil.log(TAG, "航点事件发送: " + ("0".equals(state) ? "进入" : "离开") + " index=" + index);
} else {
LogUtil.log(TAG, "航点事件发送失败MQTT未连接");
}
} catch (Exception e) {
LogUtil.log(TAG, "航点事件发送异常: " + e);
}
} }
} }

View File

@ -72,7 +72,7 @@ public class LTEManager extends BaseManager {
@Override @Override
public void onLTELinkInfoUpdate(LTELinkInfo t1) { public void onLTELinkInfoUpdate(LTELinkInfo t1) {
if (t1 != null) { if (t1 != null) {
LogUtil.log(TAG, "信号质量" + t1.getLinkQualityLevel()); // LogUtil.log(TAG, "信号质量" + t1.getLinkQualityLevel());
if(t1.getLinkQualityLevel()!=null){ if(t1.getLinkQualityLevel()!=null){
Movement.getInstance().setQuality_4g(t1.getLinkQualityLevel().getLteGndSingalBar().value()+ ""); Movement.getInstance().setQuality_4g(t1.getLinkQualityLevel().getLteGndSingalBar().value()+ "");
Movement.getInstance().setGnd_quality_4g(t1.getLinkQualityLevel().getOcuSyncSignalQualityLevel().value()+ ""); Movement.getInstance().setGnd_quality_4g(t1.getLinkQualityLevel().getOcuSyncSignalQualityLevel().value()+ "");

View File

@ -3,6 +3,7 @@ package com.aros.apron.manager;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -19,6 +20,7 @@ import com.amazonaws.services.s3.model.ProgressListener;
import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest;
import com.aros.apron.base.BaseManager; import com.aros.apron.base.BaseManager;
import com.aros.apron.entity.ApronExecutionStatus; import com.aros.apron.entity.ApronExecutionStatus;
import com.aros.apron.entity.MessageDown;
import com.aros.apron.entity.Movement; import com.aros.apron.entity.Movement;
import com.aros.apron.tools.LogUtil; import com.aros.apron.tools.LogUtil;
import com.aros.apron.tools.PreferenceUtils; import com.aros.apron.tools.PreferenceUtils;
@ -36,6 +38,7 @@ import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import dji.sdk.keyvalue.key.FlightControllerKey; import dji.sdk.keyvalue.key.FlightControllerKey;
import dji.sdk.keyvalue.key.KeyTools; import dji.sdk.keyvalue.key.KeyTools;
@ -62,8 +65,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
public class MediaManager extends BaseManager { public class MediaManager extends BaseManager {
private final String TAG = "MediaManager"; private final String TAG = "MediaManager";
private final String mediaFileDir = "/apronPic"; private final String mediaFileDir = "/apronPic";
private MediaFileListState mState = null; private MediaFileListState mState = null;
private List<MediaFile> mediaFiles = new ArrayList<>(); private List<MediaFile> mediaFiles = new ArrayList<>();
@ -74,6 +77,10 @@ public class MediaManager extends BaseManager {
/* ===== 下载失败重试计数 ===== */ /* ===== 下载失败重试计数 ===== */
private int downloadFailTimes = 0; private int downloadFailTimes = 0;
private static final int MAX_DOWNLOAD_RETRY = 3; private static final int MAX_DOWNLOAD_RETRY = 3;
/* ===== 统一主线程Handler ===== */
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private MediaManager() { private MediaManager() {
} }
@ -85,289 +92,354 @@ public class MediaManager extends BaseManager {
return MediaManagerHolder.INSTANCE; return MediaManagerHolder.INSTANCE;
} }
public void init() { public void init() {
Boolean isConnect = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey.KeyConnection)); Boolean isConnect = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey.KeyConnection));
if (isConnect != null && isConnect) { if (isConnect != null && isConnect) {
MediaFileListDataSource source = new MediaFileListDataSource.Builder().setIndexType(ComponentIndexType.PORT_1).build(); MediaFileListDataSource source = new MediaFileListDataSource.Builder().setIndexType(ComponentIndexType.PORT_1).build();
MediaDataCenter.getInstance().getMediaManager().setMediaFileDataSource(source); MediaDataCenter.getInstance().getMediaManager().setMediaFileDataSource(source);
MediaDataCenter.getInstance().getMediaManager().addMediaFileListStateListener(new MediaFileListStateListener() { MediaDataCenter.getInstance().getMediaManager().addMediaFileListStateListener(new MediaFileListStateListener() {
@Override @Override
public void onUpdate(MediaFileListState mediaFileListState) { public void onUpdate(MediaFileListState state) {
mState = mediaFileListState; mState = state;
LogUtil.log(TAG, "当前媒体文件状态:" + mediaFileListState.name()); LogUtil.log(TAG, "【状态监听】state=" + state.name() + " | isPulling=" + isPulling + " | retryCycle=" + retryCycle);
sendEvent2Server("媒体文件状态"+mState,1);
if (state == MediaFileListState.UP_TO_DATE && isPulling) {
LogUtil.log(TAG, "【状态监听】UP_TO_DATE 到达,开始处理文件列表");
processFileList();
}
} }
}); });
} }
} }
/* =================================================================
* enablePlayback / 拉列表 两层重试DJI方式(3次) 自有方式(20次)
* DJI方式enable IDLE调pull/UPDATING只等 等20s UP_TO_DATE
* 自有方式enable 2s后第一次pull 20s后第二次pull 检查数据
* 每步都挂超时兜底SDK不给回调也不卡死
* ================================================================= */
private int enterPlayBackFailTimes; private int enterPlayBackFailTimes;
private boolean isEnablePlayback; private volatile boolean isPlaybackEnabling;
private volatile boolean isPlaybackEnabling; // 防止并发调用 private static final int PULL_TIMEOUT_S = 20;
private Handler playbackTimeoutHandler = new Handler(android.os.Looper.getMainLooper()); private int retryCycle = 0;
private Runnable playbackTimeoutRunnable = null; private boolean isPulling;
private static final int PLAYBACK_ENABLE_TIMEOUT_MS = 15000; // 进入媒体模式超时 15 private Runnable pullTimeoutRunnable;
// 两层重试计数器
private static final int DJI_MAX_RETRIES = 3;
private static final int OWN_MAX_RETRIES = 20;
private int djiRetries = 0;
private int ownRetries = 0;
private boolean useOwnMethod = false;
public void enablePlayback() { public void enablePlayback() {
LogUtil.log(TAG, "【enablePlayback】调用 | isPlaybackEnabling=" + isPlaybackEnabling
+ " | useOwnMethod=" + useOwnMethod + " | dji=" + djiRetries + "/" + DJI_MAX_RETRIES
+ " | own=" + ownRetries + "/" + OWN_MAX_RETRIES);
if (isPlaybackEnabling) { if (isPlaybackEnabling) {
LogUtil.log(TAG, "媒体模式已在启用中,跳过重复调用"); LogUtil.log(TAG, "【enablePlayback】已在启用中跳过");
return; return;
} }
boolean isFirstEntry = !isPlaybackEnabling;
isPlaybackEnabling = true;
// 停止端口扫描和 RTSP 刷新关闭 RTSP 推流确保媒体上传稳定 // DJI方式3次耗尽 降级自有方式
// StreamManager.getInstance().stopStreamRefreshTimer(); if (!useOwnMethod && djiRetries >= DJI_MAX_RETRIES) {
//com.aros.apron.tools.SimplePortScanner.getInstance().stopScan(); LogUtil.log(TAG, "【enablePlayback】DJI方式" + DJI_MAX_RETRIES + "次均失败 → 降级自有方式");
StreamManager.getInstance().stopstream(); useOwnMethod = true;
ownRetries = 0;
// 仅首次调用时清理状态重试时保留计数器
if (isFirstEntry) {
uploadedFileNames.clear();
bucketChecked = false;
downloadFailTimes = 0;
enterPlayBackFailTimes = 0;
isEnablePlayback = false;
pullMediaFileListFromCameraFailTimes = 0;
updatingWaitCount = 0;
pullqwq = false;
pullStartTime = System.currentTimeMillis();
LogUtil.log(TAG, "已清空上传文件集合");
} }
MediaDataCenter.getInstance().getMediaManager().enable(new CommonCallbacks.CompletionCallback() { // 自有方式次数耗尽 放弃发关机兜底
@Override if (useOwnMethod && ownRetries >= OWN_MAX_RETRIES) {
public void onSuccess() { LogUtil.log(TAG, "【enablePlayback】自有方式" + OWN_MAX_RETRIES + "次也失败,彻底放弃 → 发关机");
LogUtil.log(TAG, "进入媒体模式成功"); sendEvent2Server("媒体列表拉取彻底失败(DJI3+自有20)", 2);
isPlaybackEnabling = false; // 成功后释放锁
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MediaFileListDataSource source = new
MediaFileListDataSource.Builder().setIndexType(ComponentIndexType.PORT_1).build();
MediaDataCenter.getInstance().getMediaManager().setMediaFileDataSource(source);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
pullMediaFileListFromCamera();
}
}, 3000);
}
}, 3000);
}
@Override
public void onFailure(@NonNull IDJIError idjiError) {
LogUtil.log(TAG, "" + enterPlayBackFailTimes + "次进入媒体模式失败:" + new Gson().toJson(idjiError));
if (!isEnablePlayback) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (enterPlayBackFailTimes < 10) {
enterPlayBackFailTimes++;
isPlaybackEnabling = false; // 释放锁
LogUtil.log(TAG, "" + enterPlayBackFailTimes + "次重试进入媒体模式");
retryEnablePlayback();
} else {
isPlaybackEnabling = false; // 失败放弃后释放锁
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("媒体模式进入失败:关机",1);
}
}
}, 1500);
}
}
});
}
/** 重试进入媒体模式(不清理状态,保留计数器) */
private void retryEnablePlayback() {
isPlaybackEnabling = true;
MediaDataCenter.getInstance().getMediaManager().enable(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "进入媒体模式成功(重试)");
isPlaybackEnabling = false;
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
MediaFileListDataSource source = new
MediaFileListDataSource.Builder().setIndexType(ComponentIndexType.PORT_1).build();
MediaDataCenter.getInstance().getMediaManager().setMediaFileDataSource(source);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
pullMediaFileListFromCamera();
}
}, 3000);
}
}, 3000);
}
@Override
public void onFailure(@NonNull IDJIError idjiError) {
LogUtil.log(TAG, "" + enterPlayBackFailTimes + "次进入媒体模式失败:" + new Gson().toJson(idjiError));
if (!isEnablePlayback) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (enterPlayBackFailTimes < 10) {
enterPlayBackFailTimes++;
isPlaybackEnabling = false;
LogUtil.log(TAG, "" + enterPlayBackFailTimes + "次重试进入媒体模式");
retryEnablePlayback();
} else {
isPlaybackEnabling = false;
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("媒体模式进入失败:关机",1);
}
}
}, 1500);
}
}
});
}
private int pullMediaFileListFromCameraFailTimes;
private int updatingWaitCount = 0;
private static final int MAX_UPDATING_WAIT = 15; // 最多等待15秒无文件时快速跳过
private boolean pullqwq = false;
private boolean isPullMediaFileListFromCameraSuccess;
private long pullStartTime = 0; // 记录整个拉取流程开始时间
private static final int MAX_PULL_DURATION = 25; // 整个拉取流程最多25秒超时强制关机
private void pullMediaFileListFromCamera() {
// 全局超时检查防止状态机异常导致无限循环
long elapsed = (System.currentTimeMillis() - pullStartTime) / 1000;
if (elapsed >= MAX_PULL_DURATION) {
LogUtil.log(TAG, "拉取流程总耗时 " + elapsed + "s超时强制关机");
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("媒体文件拉取超时", 2);
disablePlayback(); disablePlayback();
return; return;
} }
MediaFileListState currentState = MediaDataCenter.getInstance().getMediaManager().getMediaFileListState(); isPlaybackEnabling = true;
LogUtil.log(TAG, "当前状态:" + currentState + ",准备拉取文件列表(已耗时" + elapsed + "s");
if (currentState == MediaFileListState.IDLE) { if (!useOwnMethod) {
LogUtil.log(TAG, "状态为IDLE开始拉取文件列表"); djiRetries++;
MediaDataCenter.getInstance().getMediaManager().pullMediaFileListFromCamera(new PullMediaFileListParam.Builder().count(-1).build(), new CommonCallbacks.CompletionCallback() { LogUtil.log(TAG, "【DJI方式】第" + djiRetries + "/" + DJI_MAX_RETRIES + "");
@Override
public void onSuccess() {
LogUtil.log(TAG, "拉取请求已接受等待状态变为UP_TO_DATE");
pullMediaFileListFromCameraFailTimes = 0;
}
@Override
public void onFailure(@NonNull IDJIError idjiError) {
LogUtil.log(TAG, "拉取请求失败: " + new Gson().toJson(idjiError));
if (pullMediaFileListFromCameraFailTimes < 5) {
pullMediaFileListFromCameraFailTimes++;
LogUtil.log(TAG, "" + pullMediaFileListFromCameraFailTimes + "次重试...");
} else {
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("拉取媒体文件失败",2);
disablePlayback();
LogUtil.log(TAG, "发送关闭无人机");
}
}
});
// 不依赖回调续命无条件调度下一轮轮询
new Handler().postDelayed(MediaManager.this::pullMediaFileListFromCamera, 1000);
} else if (currentState == MediaFileListState.UP_TO_DATE) {
// 状态为UP_TO_DATE获取文件列表数据
LogUtil.log(TAG, "状态为UP_TO_DATE获取文件列表数据");
try {
// 确保获取文件列表数据
List<MediaFile> rawList = MediaDataCenter.getInstance().getMediaManager().getMediaFileListData().getData();
// 检查文件列表是否为空
if (rawList == null || rawList.isEmpty()) {
LogUtil.log(TAG, "文件列表为空,重试拉取");
// 状态已经是UP_TO_DATE时空列表可能确实无文件快速重试2次后放弃
if (pullMediaFileListFromCameraFailTimes < 2) {
pullMediaFileListFromCameraFailTimes++;
LogUtil.log(TAG, "" + pullMediaFileListFromCameraFailTimes + "次重试...");
new Handler().postDelayed(MediaManager.this::pullMediaFileListFromCamera, 3000);
} else {
LogUtil.log(TAG, "UP_TO_DATE状态文件列表持续为空确认无文件");
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("拉取媒体文件失败",2);
disablePlayback();
LogUtil.log(TAG, "发送关闭无人机");
}
return;
}
LogUtil.log(TAG, "原始文件列表数量: " + rawList.size());
// 过滤已上传文件
mediaFiles = new ArrayList<>();
for (MediaFile mf : rawList) {
if (!uploadedFileNames.contains(mf.getFileName())) {
mediaFiles.add(mf);
} else {
LogUtil.log(TAG, "跳过已上传文件: " + mf.getFileName());
}
}
// 修复在过滤后设置任务媒体计数
Movement.getInstance().setTask_media_count(mediaFiles.size());
LogUtil.log(TAG, "过滤后文件数量: " + mediaFiles.size());
if(PreferenceUtils.getInstance().getMissionType()==0){
sendFlightTaskProgress2Server();
}
if (mediaFiles.isEmpty()) {
LogUtil.log(TAG, "所有文件均已上传,直接清理");
downLoadMediaFileIndex = 0;
// 提前设置关机标志 aircraftStoredReply 能立即回复成功
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
removeAllFiles();
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
pullOriginalMediaFileFromCamera();
}
} catch (Exception e) {
LogUtil.log(TAG, "获取文件列表数据失败: " + e.getMessage());
// 发生异常时快速重试2次
if (pullMediaFileListFromCameraFailTimes < 2) {
pullMediaFileListFromCameraFailTimes++;
LogUtil.log(TAG, "" + pullMediaFileListFromCameraFailTimes + "次重试...");
new Handler().postDelayed(MediaManager.this::pullMediaFileListFromCamera, 2000);
} else {
LogUtil.log(TAG, "重试次数达到上限,拉取失败");
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("拉取媒体文件失败",2);
disablePlayback();
LogUtil.log(TAG, "发送关闭无人机");
}
}
} else { } else {
// 其他状态如UPDATING等待状态变化不要重复调用pullMediaFileListFromCamera ownRetries++;
LogUtil.log(TAG, "状态为" + currentState + ",等待状态变化... (count=" + updatingWaitCount + ")"); LogUtil.log(TAG, "【自有方式】第" + ownRetries + "/" + OWN_MAX_RETRIES + "");
updatingWaitCount++; }
// 增加超时处理避免无限等待 // 首次进入清理状态
if (updatingWaitCount >= MAX_UPDATING_WAIT) { if (!useOwnMethod && djiRetries == 1) {
LogUtil.log(TAG, "等待状态变化超时,强制关机"); uploadedFileNames.clear();
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); bucketChecked = false;
sendEvent2Server("媒体文件状态更新超时",2); downloadFailTimes = 0;
disablePlayback(); enterPlayBackFailTimes = 0;
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "【enablePlayback】首次进入清理状态");
return; }
// 只在推流活跃时才停避免无流时产生"直播未启动"的无效日志
if (MediaDataCenter.getInstance().getLiveStreamManager().isStreaming()) {
StreamManager.getInstance().stopstream();
}
// SDK enable 可能不给回调 15s 超时兜底防止 isPlaybackEnabling 锁死
final java.util.concurrent.atomic.AtomicBoolean enableDone = new java.util.concurrent.atomic.AtomicBoolean(false);
Runnable enableTimeout = () -> {
if (enableDone.compareAndSet(false, true)) {
LogUtil.log(TAG, "【enablePlayback】⏰ SDK enable 15s 无回调!强制重置");
isPlaybackEnabling = false;
retryAfterDisable("SDK enable无回调");
}
};
mainHandler.postDelayed(enableTimeout, 15000);
MediaDataCenter.getInstance().getMediaManager().enable(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
if (!enableDone.compareAndSet(false, true)) return; // 超时已触发忽略
mainHandler.removeCallbacks(enableTimeout);
LogUtil.log(TAG, "【enablePlayback】SDK enable 成功 | method=" + (useOwnMethod ? "自有" : "DJI"));
isPlaybackEnabling = false;
mainHandler.postDelayed(() -> {
MediaFileListDataSource source = new MediaFileListDataSource.Builder()
.setIndexType(ComponentIndexType.PORT_1).build();
MediaDataCenter.getInstance().getMediaManager().setMediaFileDataSource(source);
if (!useOwnMethod) {
// DJI方式走状态驱动流程
LogUtil.log(TAG, "【DJI方式】6s延迟到开始 pullMediaFileListFromCamera");
mainHandler.postDelayed(() -> pullMediaFileListFromCamera(), 3000);
} else {
// 自有方式enable成功后2s就拉不等状态
LogUtil.log(TAG, "【自有方式】enable成功2s后第一次拉取");
mainHandler.postDelayed(() -> forcePullInOwnMode(), 2000);
}
}, 3000);
} }
new Handler().postDelayed(MediaManager.this::pullMediaFileListFromCamera, 1000); @Override
public void onFailure(@NonNull IDJIError e) {
if (!enableDone.compareAndSet(false, true)) return; // 超时已触发忽略
mainHandler.removeCallbacks(enableTimeout);
LogUtil.log(TAG, "【enablePlayback】SDK enable 失败: " + e.description()
+ " | method=" + (useOwnMethod ? "自有" : "DJI"));
isPlaybackEnabling = false;
retryAfterDisable("enable失败");
}
});
}
/**
* 自有方式enable 成功 2s后第一次拉 等20s 第二次拉 检查数据
* SDK 可能不给任何回调每步都挂超时兜底
*/
private void forcePullInOwnMode() {
LogUtil.log(TAG, "【自有方式】2s后第一次拉取 | ownRetries=" + ownRetries);
isPulling = true;
MediaFileListState state = MediaDataCenter.getInstance().getMediaManager().getMediaFileListState();
LogUtil.log(TAG, "【自有方式】当前SDK状态=" + state);
// 第一次拉不管状态直接调 SDK pull
MediaDataCenter.getInstance().getMediaManager().pullMediaFileListFromCamera(
new PullMediaFileListParam.Builder().count(-1).build(),
new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "【自有方式】第一次pull onSuccess → 20s后第二次拉");
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "【自有方式】第一次pull onFailure: " + e.description() + " → 继续等20s");
}
});
// SDK 可能不给回调 20s后不管结果直接第二次拉
mainHandler.postDelayed(() -> {
LogUtil.log(TAG, "【自有方式】20s到开始第二次拉取");
MediaDataCenter.getInstance().getMediaManager().pullMediaFileListFromCamera(
new PullMediaFileListParam.Builder().count(-1).build(),
new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "【自有方式】第二次pull onSuccess → 检查数据");
if (isPulling) checkDataInOwnMode();
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "【自有方式】第二次pull onFailure: " + e.description());
if (isPulling) checkDataInOwnMode();
}
});
// 第二次拉也可能不给回调再挂 20s 兜底
mainHandler.postDelayed(() -> {
if (isPulling) {
LogUtil.log(TAG, "【自有方式】第二次pull 20s兜底超时强制检查数据");
checkDataInOwnMode();
}
}, PULL_TIMEOUT_S * 1000L);
}, PULL_TIMEOUT_S * 1000L);
}
/**
* 自有方式不管 SDK 状态直接拿数据尝试处理
*/
private void checkDataInOwnMode() {
if (!isPulling) return;
isPulling = false;
try {
List<MediaFile> data = MediaDataCenter.getInstance().getMediaManager()
.getMediaFileListData().getData();
LogUtil.log(TAG, "【自有方式】SDK返回文件数=" + (data != null ? data.size() : 0)
+ " | state=" + MediaDataCenter.getInstance().getMediaManager().getMediaFileListState());
if (data != null && !data.isEmpty()) {
// 拉到了重置所有计数器走正常处理流程
LogUtil.log(TAG, "【自有方式】✅ 成功拉到" + data.size() + "个文件,切回正常流程");
djiRetries = 0;
ownRetries = 0;
retryCycle = 0;
useOwnMethod = false;
processFileList();
} else {
LogUtil.log(TAG, "【自有方式】无数据,退出重进");
retryAfterDisable("自有方式无数据");
}
} catch (Exception e) {
LogUtil.log(TAG, "【自有方式】获取数据异常: " + e.getMessage());
retryAfterDisable("自有方式获取数据异常");
} }
} }
private void retryAfterDisable(String reason) {
retryCycle++;
LogUtil.log(TAG, "【retry】" + reason + " → 先disable再enable | method=" + (useOwnMethod ? "自有" : "DJI")
+ " | retryCycle=" + retryCycle);
disablePlaybackAndThen(() -> {
LogUtil.log(TAG, "【retry】disable完成3s后enablePlayback");
mainHandler.postDelayed(() -> enablePlayback(), 3000);
});
}
private void pullMediaFileListFromCamera() {
// 防重复post先取消旧的超时
if (pullTimeoutRunnable != null) {
mainHandler.removeCallbacks(pullTimeoutRunnable);
LogUtil.log(TAG, "【pullList】取消旧超时");
}
isPulling = true;
// 定义一个统一的超时runnableSDK不给回调的兜底
pullTimeoutRunnable = () -> {
LogUtil.log(TAG, "【pullList】⏰ 20s超时触发当前state="
+ MediaDataCenter.getInstance().getMediaManager().getMediaFileListState()
+ " | djiRetries=" + djiRetries + " → 退出重进");
isPulling = false;
retryAfterDisable("媒体文件拉取超时(SDK无回调)");
};
// 启动20s看门狗
mainHandler.postDelayed(pullTimeoutRunnable, PULL_TIMEOUT_S * 1000L);
LogUtil.log(TAG, "【pullList】20s超时看门狗已启动");
MediaFileListState state = MediaDataCenter.getInstance().getMediaManager().getMediaFileListState();
LogUtil.log(TAG, "【pullList】当前SDK状态=" + state + " | isPulling=" + isPulling + " | djiRetries=" + djiRetries);
// 状态已是 UP_TO_DATE数据已就绪直接处理
if (state == MediaFileListState.UP_TO_DATE) {
LogUtil.log(TAG, "【pullList】状态已是UP_TO_DATE数据已就绪直接处理");
processFileList();
return;
}
// DJI方式IDLE 才调 pullUPDATING 只等状态严格按 DJI 建议
if (state == MediaFileListState.IDLE) {
LogUtil.log(TAG, "【pullList】状态=IDLE → 调用SDK pullMediaFileList");
MediaDataCenter.getInstance().getMediaManager().pullMediaFileListFromCamera(
new PullMediaFileListParam.Builder().count(-1).build(),
new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
if (pullTimeoutRunnable == null) {
LogUtil.log(TAG, "【pullList】SDK pull onSuccess 但数据已处理完毕,忽略");
return;
}
LogUtil.log(TAG, "【pullList】SDK pull onSuccess → 续时20s等待UP_TO_DATE");
mainHandler.removeCallbacks(pullTimeoutRunnable);
mainHandler.postDelayed(pullTimeoutRunnable, PULL_TIMEOUT_S * 1000L);
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "【pullList】SDK pull onFailure: " + e.description()
+ " → 继续等状态监听器或超时");
}
});
return;
}
// DJI方式UPDATING 不主动拉只等状态监听器通知 UP_TO_DATE 或超时
LogUtil.log(TAG, "【pullList】状态=" + state + ",等状态监听器通知 UP_TO_DATE 或超时");
}
/** 状态监听器回调——SDK 通知 UP_TO_DATE 时处理 */
private void processFileList() {
LogUtil.log(TAG, "【processFileList】进入 | isPulling=" + isPulling + " | cycle=" + retryCycle);
if (pullTimeoutRunnable != null) {
mainHandler.removeCallbacks(pullTimeoutRunnable);
pullTimeoutRunnable = null;
LogUtil.log(TAG, "【processFileList】超时看门狗已取消");
}
isPulling = false;
retryCycle = 0;
djiRetries = 0;
ownRetries = 0;
useOwnMethod = false;
try {
List<MediaFile> rawList = MediaDataCenter.getInstance().getMediaManager()
.getMediaFileListData().getData();
LogUtil.log(TAG, "【processFileList】SDK返回文件数=" + (rawList != null ? rawList.size() : 0));
if (rawList == null || rawList.isEmpty()) {
LogUtil.log(TAG, "【processFileList】文件列表为空关机");
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("无媒体文件", 1);
disablePlayback();
return;
}
LogUtil.log(TAG, "文件列表数量: " + rawList.size());
mediaFiles = new ArrayList<>();
for (MediaFile mf : rawList) {
if (!uploadedFileNames.contains(mf.getFileName())) {
mediaFiles.add(mf);
}
}
Movement.getInstance().setTask_media_count(mediaFiles.size());
if (PreferenceUtils.getInstance().getMissionType() == 0) sendFlightTaskProgress2Server();
if (mediaFiles.isEmpty()) {
LogUtil.log(TAG, "全部已上传,直接清理");
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
removeAllFiles();
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
pullOriginalMediaFileFromCamera();
}
} catch (Exception e) {
LogUtil.log(TAG, "处理文件列表异常: " + e.getMessage());
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("处理文件列表失败", 2);
disablePlayback();
}
}
/* =================================================================
* 文件下载/上传 保持原始逻辑不变
* ================================================================= */
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
public void pullOriginalMediaFileFromCamera() { public void pullOriginalMediaFileFromCamera() {
@ -379,7 +451,6 @@ public class MediaManager extends BaseManager {
downLoadMediaFileIndex++; downLoadMediaFileIndex++;
if (downLoadMediaFileIndex == mediaFiles.size()) { if (downLoadMediaFileIndex == mediaFiles.size()) {
// This refers to when all files have been downloaded or failed. Clear SD card, cache, exit media mode, and shut down the drone
downLoadMediaFileIndex = 0; downLoadMediaFileIndex = 0;
removeAllFiles(); removeAllFiles();
} else { } else {
@ -445,7 +516,7 @@ public class MediaManager extends BaseManager {
} catch (IOException error) { } catch (IOException error) {
LogUtil.log(TAG, "File " + downLoadMediaFileIndex + " error: " + error.getMessage()); LogUtil.log(TAG, "File " + downLoadMediaFileIndex + " error: " + error.getMessage());
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("文件流关闭失败",2); sendEvent2Server("文件流关闭失败", 2);
disablePlayback(); disablePlayback();
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "发送关闭无人机");
} }
@ -474,7 +545,7 @@ public class MediaManager extends BaseManager {
}, 2000); }, 2000);
} else { } else {
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("" + downLoadMediaFileIndex + "个文件下载失败(已重试" + MAX_DOWNLOAD_RETRY + "次)",2); sendEvent2Server("" + downLoadMediaFileIndex + "个文件下载失败(已重试" + MAX_DOWNLOAD_RETRY + "次)", 2);
disablePlayback(); disablePlayback();
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "发送关闭无人机");
} }
@ -485,7 +556,7 @@ public class MediaManager extends BaseManager {
Log.e(TAG, "Error opening file: " + e.getMessage()); Log.e(TAG, "Error opening file: " + e.getMessage());
// 发生异常时也要确保关机 // 发生异常时也要确保关机
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("文件打开失败",2); sendEvent2Server("文件打开失败", 2);
disablePlayback(); disablePlayback();
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "发送关闭无人机");
// 关闭文件流 // 关闭文件流
@ -498,15 +569,19 @@ public class MediaManager extends BaseManager {
} }
} }
/* =================================================================
* MinIO上传 原始逻辑不变
* ================================================================= */
private AmazonS3 s3 = new AmazonS3Client(new AWSCredentials() { private AmazonS3 s3 = new AmazonS3Client(new AWSCredentials() {
@Override @Override
public String getAWSAccessKeyId() { public String getAWSAccessKeyId() {
return PreferenceUtils.getInstance().getAccessKey(); // minio的key return PreferenceUtils.getInstance().getAccessKey();
} }
@Override @Override
public String getAWSSecretKey() { public String getAWSSecretKey() {
return PreferenceUtils.getInstance().getSecretKey(); // minio的密钥 return PreferenceUtils.getInstance().getSecretKey();
} }
}, Region.getRegion(Regions.US_EAST_1), new ClientConfiguration() }, Region.getRegion(Regions.US_EAST_1), new ClientConfiguration()
.withConnectionTimeout(30000) .withConnectionTimeout(30000)
@ -520,7 +595,7 @@ public class MediaManager extends BaseManager {
@Override @Override
public void subscribe(ObservableEmitter<String> emitter) throws Exception { public void subscribe(ObservableEmitter<String> emitter) throws Exception {
// 服务器地址 // 服务器地址
s3.setEndpoint(PreferenceUtils.getInstance().getUploadUrl()); // http://ip:端口号 s3.setEndpoint(PreferenceUtils.getInstance().getUploadUrl());
// Bucket只在首次上传时检查创建后续上传不再重复请求 // Bucket只在首次上传时检查创建后续上传不再重复请求
if (!bucketChecked) { if (!bucketChecked) {
synchronized (this) { synchronized (this) {
@ -534,17 +609,11 @@ public class MediaManager extends BaseManager {
} }
} }
// 上传文件到网关MINIO存储服务
s3.putObject( s3.putObject(
new PutObjectRequest( new PutObjectRequest(
PreferenceUtils.getInstance().getBucketName(), PreferenceUtils.getInstance().getBucketName(),
"/" + PreferenceUtils.getInstance().getObjectKey() + "/" + mediaFile.getFileName(), "/" + PreferenceUtils.getInstance().getObjectKey() + "/" + mediaFile.getFileName(),
file file
// new PutObjectRequest(
// PreferenceUtils.getInstance().getBucketName(),
// "/" + PreferenceUtils.getInstance().getObjectKey() + "/" +
// PreferenceUtils.getInstance().getFlightId() + "/" + mediaFile.getFileName(),
// file
).withProgressListener(new ProgressListener() { ).withProgressListener(new ProgressListener() {
@Override @Override
public void progressChanged(ProgressEvent progressEvent) { public void progressChanged(ProgressEvent progressEvent) {
@ -572,7 +641,6 @@ public class MediaManager extends BaseManager {
}) })
); );
// 获取文件上传后访问地址url
GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest( GeneratePresignedUrlRequest urlRequest = new GeneratePresignedUrlRequest(
PreferenceUtils.getInstance().getBucketName(), PreferenceUtils.getInstance().getBucketName(),
"/" + PreferenceUtils.getInstance().getObjectKey() + "/" "/" + PreferenceUtils.getInstance().getObjectKey() + "/"
@ -580,7 +648,6 @@ public class MediaManager extends BaseManager {
); );
String url = s3.generatePresignedUrl(urlRequest).toString(); String url = s3.generatePresignedUrl(urlRequest).toString();
// 文件上传后访问地址url
emitter.onNext(url); emitter.onNext(url);
emitter.onComplete(); emitter.onComplete();
} }
@ -590,28 +657,23 @@ public class MediaManager extends BaseManager {
.subscribe(new Observer<String>() { .subscribe(new Observer<String>() {
@Override @Override
public void onSubscribe(Disposable d) { public void onSubscribe(Disposable d) {
// Handle on subscribe (optional)
} }
@Override @Override
public void onNext(String url) { public void onNext(String url) {
// 上传成功重置下载重试计数
downloadFailTimes = 0; downloadFailTimes = 0;
//上传完成发送事件
sendMediaUpload2Server(mediaFile.getFileName(), downLoadMediaFileIndex + 1, mediaFiles.size()); sendMediaUpload2Server(mediaFile.getFileName(), downLoadMediaFileIndex + 1, mediaFiles.size());
} }
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Override @Override
public void onError(Throwable e) { public void onError(Throwable e) {
// 每上传失败一张就清除缓存
uploadedFileNames.add(mediaFile.getFileName()); uploadedFileNames.add(mediaFile.getFileName());
FileUtil.deleteFile(file); FileUtil.deleteFile(file);
LogUtil.log(TAG, "Error uploading file " + downLoadMediaFileIndex + ": " + e.getMessage()); LogUtil.log(TAG, "Error uploading file " + downLoadMediaFileIndex + ": " + e.getMessage());
downLoadMediaFileIndex++; downLoadMediaFileIndex++;
if (downLoadMediaFileIndex == mediaFiles.size()) { if (downLoadMediaFileIndex == mediaFiles.size()) {
// 所有文件已经下载完成或失败清空SD卡缓存退出媒体模式发送无人机关机
downLoadMediaFileIndex = 0; downLoadMediaFileIndex = 0;
removeAllFiles(); removeAllFiles();
} else { } else {
@ -622,16 +684,14 @@ public class MediaManager extends BaseManager {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@Override @Override
public void onComplete() { public void onComplete() {
// 每上传一张就清除缓存并记录已上传文件名
uploadedFileNames.add(mediaFile.getFileName()); uploadedFileNames.add(mediaFile.getFileName());
FileUtil.deleteFile(file); FileUtil.deleteFile(file);
LogUtil.log(TAG, "File " + downLoadMediaFileIndex + " uploaded successfully."); LogUtil.log(TAG, "File " + downLoadMediaFileIndex + " uploaded successfully.");
sendEvent2Server( "" + downLoadMediaFileIndex + "个文件已上传",1); sendEvent2Server("" + downLoadMediaFileIndex + "个文件已上传", 1);
downLoadMediaFileIndex++; downLoadMediaFileIndex++;
if (downLoadMediaFileIndex == mediaFiles.size()) { if (downLoadMediaFileIndex == mediaFiles.size()) {
// 所有文件已上传完成清空SD卡缓存退出媒体模式发送无人机关机 sendEvent2Server("媒体文件已上传完毕", 1);
sendEvent2Server( "媒体文件已上传完毕",1);
removeAllFiles(); removeAllFiles();
downLoadMediaFileIndex = 0; downLoadMediaFileIndex = 0;
} else { } else {
@ -641,12 +701,15 @@ public class MediaManager extends BaseManager {
}); });
} }
/* =================================================================
* 删除文件 / 退出媒体模式
* ================================================================= */
public void removeAllFiles() { public void removeAllFiles() {
// 确保即使没有文件也能正常关机
if (mediaFiles == null || mediaFiles.isEmpty()) { if (mediaFiles == null || mediaFiles.isEmpty()) {
LogUtil.log(TAG, "没有文件需要清除,直接关机"); LogUtil.log(TAG, "没有文件需要清除,直接关机");
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
sendEvent2Server("没有媒体文件需要清除",1); sendEvent2Server("没有媒体文件需要清除", 1);
disablePlayback(); disablePlayback();
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "发送关闭无人机");
return; return;
@ -657,7 +720,7 @@ public class MediaManager extends BaseManager {
public void onSuccess() { public void onSuccess() {
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
LogUtil.log(TAG, "清除文件成功 "); LogUtil.log(TAG, "清除文件成功 ");
sendEvent2Server("媒体文件已清除",1); sendEvent2Server("媒体文件已清除", 1);
disablePlayback(); disablePlayback();
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "发送关闭无人机");
} }
@ -666,16 +729,13 @@ public class MediaManager extends BaseManager {
public void onFailure(@NonNull IDJIError idjiError) { public void onFailure(@NonNull IDJIError idjiError) {
ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true); ApronExecutionStatus.getInstance().setAircraftWaitShutDown(true);
LogUtil.log(TAG, "清除文件失败: " + new Gson().toJson(idjiError)); LogUtil.log(TAG, "清除文件失败: " + new Gson().toJson(idjiError));
sendEvent2Server("媒体文件清除失败",2); sendEvent2Server("媒体文件清除失败", 2);
LogUtil.log(TAG, "发送关闭无人机"); LogUtil.log(TAG, "发送关闭无人机");
} }
}); });
} }
//退出媒体模式 public void disablePlayback() {
public void disablePlayback() {
// 任务结束停止视频流刷新定时器
// StreamManager.getInstance().stopStreamRefreshTimer();
MediaDataCenter.getInstance().getMediaManager().disable(new CommonCallbacks.CompletionCallback() { MediaDataCenter.getInstance().getMediaManager().disable(new CommonCallbacks.CompletionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
@ -684,27 +744,65 @@ public class MediaManager extends BaseManager {
@Override @Override
public void onFailure(@NonNull IDJIError idjiError) { public void onFailure(@NonNull IDJIError idjiError) {
LogUtil.log(TAG, "退出媒体模式失败:"+new Gson().toJson(idjiError)); LogUtil.log(TAG, "退出媒体模式失败:" + new Gson().toJson(idjiError));
} }
}); });
} }
private int downLoadMediaFileIndex = 0; private static final long DISABLE_TIMEOUT_MS = 15_000;
private String getSDCardPath(){ /**
if (checkSDCard()) { * 退出媒体模式 + 超时保护 + 完成后执行后续
return Environment.getExternalStorageDirectory() * disable 正常回调回来就执行如果SDK不回调用15s后强制执行防卡死
.getPath(); */
} else { private void disablePlaybackAndThen(Runnable then) {
return Environment.getExternalStorageDirectory() LogUtil.log(TAG, "【disablePlayback】开始退出媒体模式15s超时兜底");
.getParentFile().getPath(); final AtomicBoolean done = new AtomicBoolean(false);
}
Runnable disableTimeout = () -> {
if (done.compareAndSet(false, true)) {
LogUtil.log(TAG, "【disablePlayback】⏰ 15s超时SDK disable无响应强制执行后续");
then.run();
}
};
mainHandler.postDelayed(disableTimeout, DISABLE_TIMEOUT_MS);
MediaDataCenter.getInstance().getMediaManager().disable(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
if (done.compareAndSet(false, true)) {
mainHandler.removeCallbacks(disableTimeout);
LogUtil.log(TAG, "【disablePlayback】SDK disable成功 → 执行后续");
then.run();
}
}
@Override
public void onFailure(@NonNull IDJIError idjiError) {
if (done.compareAndSet(false, true)) {
mainHandler.removeCallbacks(disableTimeout);
LogUtil.log(TAG, "【disablePlayback】SDK disable失败: " + idjiError.description() + " → 仍执行后续");
then.run();
}
}
});
} }
/* =================================================================
* 工具方法
* ================================================================= */
private int downLoadMediaFileIndex = 0;
private String getSDCardPath() {
if (checkSDCard()) {
return Environment.getExternalStorageDirectory().getPath();
} else {
return Environment.getExternalStorageDirectory().getParentFile().getPath();
}
}
private boolean checkSDCard() { private boolean checkSDCard() {
return TextUtils.equals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState()); return TextUtils.equals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
} }
} }

View File

@ -18,11 +18,13 @@ import com.aros.apron.constant.ErrorCode;
import com.aros.apron.entity.CurrentWayline; import com.aros.apron.entity.CurrentWayline;
import com.aros.apron.entity.MessageDown; import com.aros.apron.entity.MessageDown;
import com.aros.apron.entity.Movement; import com.aros.apron.entity.Movement;
import com.aros.apron.entity.ParsedWaypoint;
import com.aros.apron.entity.Synchronizedstatus; import com.aros.apron.entity.Synchronizedstatus;
import com.aros.apron.tools.LogUtil; import com.aros.apron.tools.LogUtil;
import com.aros.apron.tools.PreferenceUtils; import com.aros.apron.tools.PreferenceUtils;
import com.aros.apron.tools.RestartAPPTool; import com.aros.apron.tools.RestartAPPTool;
import com.aros.apron.tools.TakeoffProgressScheduler; import com.aros.apron.tools.TakeoffProgressScheduler;
import com.aros.apron.tools.Utils;
import com.dji.wpmzsdk.common.data.KMZInfo; import com.dji.wpmzsdk.common.data.KMZInfo;
import com.dji.wpmzsdk.manager.WPMZManager; import com.dji.wpmzsdk.manager.WPMZManager;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -33,6 +35,7 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import dji.sdk.keyvalue.key.CameraKey; import dji.sdk.keyvalue.key.CameraKey;
@ -821,6 +824,13 @@ public class MissionV3Manager extends BaseManager {
if (kmzInfo != null) { if (kmzInfo != null) {
// Utils.printJson(TAG,"航点详情:"+new Gson().toJson(kmzInfo)); // Utils.printJson(TAG,"航点详情:"+new Gson().toJson(kmzInfo));
WaylineWaylinesParseInfo waylineWaylinesParseInfo = kmzInfo.getWaylineWaylinesParseInfo(); WaylineWaylinesParseInfo waylineWaylinesParseInfo = kmzInfo.getWaylineWaylinesParseInfo();
//LogUtil.log(TAG,"航点详情:"+new Gson().toJson(kmzInfo));
//LogUtil.log(TAG,"航点详情:"+waylineWaylinesParseInfo.getWaylines());
//解析航点
if (waylineWaylinesParseInfo != null) { if (waylineWaylinesParseInfo != null) {
List<Wayline> waylines = waylineWaylinesParseInfo.getWaylines(); List<Wayline> waylines = waylineWaylinesParseInfo.getWaylines();
if (waylines != null && waylines.size() > 0) { if (waylines != null && waylines.size() > 0) {
@ -828,6 +838,95 @@ public class MissionV3Manager extends BaseManager {
if (waypoints != null && waypoints.size() > 0) { if (waypoints != null && waypoints.size() > 0) {
CurrentWayline.getInstance().setWaypoints(waypoints); CurrentWayline.getInstance().setWaypoints(waypoints);
LogUtil.log(TAG, "该航线有" + waypoints.size() + "个航点"); LogUtil.log(TAG, "该航线有" + waypoints.size() + "个航点");
// 打印每个航点的详细信息
// 构建解析后的航点列表
// 先从航线级 actionGroups 提取每个航点的 hoverTimestartIndexendIndex
java.util.Map<Integer, Integer> hoverTimeMap = new java.util.HashMap<>();
try {
String waylineJson = new Gson().toJson(waylines.get(0));
com.google.gson.JsonObject waylineObj = new Gson().fromJson(waylineJson, com.google.gson.JsonObject.class);
com.google.gson.JsonArray actionGroups = waylineObj.getAsJsonArray("actionGroups");
if (actionGroups != null) {
for (int g = 0; g < actionGroups.size(); g++) {
com.google.gson.JsonObject group = actionGroups.get(g).getAsJsonObject();
int startIdx = group.has("startIndex") ? group.get("startIndex").getAsInt() : 0;
int endIdx = group.has("endIndex") ? group.get("endIndex").getAsInt() : startIdx;
com.google.gson.JsonArray actions = group.getAsJsonArray("actions");
if (actions != null) {
int totalHover = 0;
for (int a = 0; a < actions.size(); a++) {
com.google.gson.JsonObject action = actions.get(a).getAsJsonObject();
if (action.has("aircraftHoverParam")) {
com.google.gson.JsonObject hoverParam = action.getAsJsonObject("aircraftHoverParam");
if (hoverParam != null && hoverParam.has("hoverTime")) {
totalHover += (int) Math.round(hoverParam.get("hoverTime").getAsDouble());
}
}
}
for (int idx = startIdx; idx <= endIdx && idx < waypoints.size(); idx++) {
hoverTimeMap.put(idx, totalHover);
}
}
}
}
} catch (Exception e) {
LogUtil.log(TAG, "航线级actionGroups解析跳过: " + e.getMessage());
}
List<ParsedWaypoint> parsedList = new ArrayList<>();
for (int i = 0; i < waypoints.size(); i++) {
WaylineExecuteWaypoint wp = waypoints.get(i);
// 先打印完整 JSON方便查看所有字段
String wpJson = new Gson().toJson(wp);
// LogUtil.log(TAG, "航点[" + i + "]完整JSON: " + wpJson);
// JSON 提取各字段WaylineExecuteWaypoint 无直接 getter
double lat = 0, lng = 0, alt = 0, speed = 0;
int hoverTime = hoverTimeMap.getOrDefault(i, 0);
try {
com.google.gson.JsonObject jsonObj = new Gson().fromJson(wpJson, com.google.gson.JsonObject.class);
// 经纬度优先顶层latitude waylineWaypoint.latitude location.latitude
if (jsonObj.has("latitude") && !jsonObj.get("latitude").isJsonNull()) {
lat = jsonObj.get("latitude").getAsDouble();
} else if (jsonObj.has("waylineWaypoint") && jsonObj.getAsJsonObject("waylineWaypoint").has("latitude")) {
lat = jsonObj.getAsJsonObject("waylineWaypoint").get("latitude").getAsDouble();
} else if (jsonObj.has("location") && jsonObj.getAsJsonObject("location").has("latitude")) {
lat = jsonObj.getAsJsonObject("location").get("latitude").getAsDouble();
}
if (jsonObj.has("longitude") && !jsonObj.get("longitude").isJsonNull()) {
lng = jsonObj.get("longitude").getAsDouble();
} else if (jsonObj.has("waylineWaypoint") && jsonObj.getAsJsonObject("waylineWaypoint").has("longitude")) {
lng = jsonObj.getAsJsonObject("waylineWaypoint").get("longitude").getAsDouble();
} else if (jsonObj.has("location") && jsonObj.getAsJsonObject("location").has("longitude")) {
lng = jsonObj.getAsJsonObject("location").get("longitude").getAsDouble();
}
// 高度优先altitude waylineWaypoint.altitude executeHeight
if (jsonObj.has("altitude") && !jsonObj.get("altitude").isJsonNull()) {
alt = jsonObj.get("altitude").getAsDouble();
} else if (jsonObj.has("waylineWaypoint") && jsonObj.getAsJsonObject("waylineWaypoint").has("altitude")) {
alt = jsonObj.getAsJsonObject("waylineWaypoint").get("altitude").getAsDouble();
} else if (jsonObj.has("executeHeight") && !jsonObj.get("executeHeight").isJsonNull()) {
alt = jsonObj.get("executeHeight").getAsDouble();
}
if (jsonObj.has("speed") && !jsonObj.get("speed").isJsonNull()) {
speed = jsonObj.get("speed").getAsDouble();
}
} catch (Exception e) {
LogUtil.log(TAG, "航点[" + i + "]部分字段解析跳过: " + e.getMessage());
}
ParsedWaypoint pw = new ParsedWaypoint(i, lat, lng, alt, hoverTime, speed);
parsedList.add(pw);
// LogUtil.log(TAG, "航点[" + i + "] " + pw.toString());
}
// 保存解析后的航点列表
CurrentWayline.getInstance().setParsedWaypoints(parsedList);
} else { } else {
LogUtil.log(TAG, "WPMZManager getWaypointInfo有误"); LogUtil.log(TAG, "WPMZManager getWaypointInfo有误");
} }

View File

@ -1,9 +1,7 @@
package com.aros.apron.manager; package com.aros.apron.manager;
import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -16,8 +14,7 @@ import com.aros.apron.tools.PreferenceUtils;
import com.aros.apron.tools.SimplePortScanner; import com.aros.apron.tools.SimplePortScanner;
import com.google.gson.Gson; import com.google.gson.Gson;
import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.Executors;
import dji.sdk.keyvalue.key.CameraKey; import dji.sdk.keyvalue.key.CameraKey;
import dji.sdk.keyvalue.key.DJIKey; import dji.sdk.keyvalue.key.DJIKey;
@ -44,8 +41,6 @@ import dji.v5.manager.interfaces.ILiveStreamManager;
public class StreamManager extends BaseManager { public class StreamManager extends BaseManager {
private static final String TAG = "StreamManager"; private static final String TAG = "StreamManager";
// ========== 新增线程池和主线程 Handler防止 ANR ==========
private final ExecutorService streamExecutor = Executors.newSingleThreadExecutor();
private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final Handler mainHandler = new Handler(Looper.getMainLooper());
// ========== 5秒定时刷新视频流防止起飞卡死 ========== // ========== 5秒定时刷新视频流防止起飞卡死 ==========
@ -110,48 +105,66 @@ public class StreamManager extends BaseManager {
return StreamHolder.INSTANCE; return StreamHolder.INSTANCE;
} }
public void restart(MessageDown message) {
sendMsg2Server(message);
LogUtil.log(TAG, "重启推流:先停后启");
ILiveStreamManager mgr = MediaDataCenter.getInstance().getLiveStreamManager();
if (mgr == null) return;
mgr.stopStream(new CommonCallbacks.CompletionCallback() {
@Override public void onSuccess() {
LogUtil.log(TAG, "旧流已停,重新启动");
resetStreamState();
startLiveWithRTSP();
}
@Override public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "停流失败,直接重启");
resetStreamState();
startLiveWithRTSP();
}
});
}
// ========== 新增重置推流状态用于端口关闭后重启 ========== // ========== 新增重置推流状态用于端口关闭后重启 ==========
public void resetStreamState() { public void resetStreamState() {
//stopStreamRefreshTimer(); //stopStreamRefreshTimer();
// SimplePortScanner.getInstance().stopScan(); // 同时停止端口扫描 // SimplePortScanner.getInstance().stopScan(); // 同时停止端口扫描
mainHandler.removeCallbacksAndMessages(null); // 清理所有待执行的回调 mainHandler.removeCallbacksAndMessages(null); // 清理所有待执行的回调
startLiveFailTimes = 0; startLiveFailTimes = 0;
isLiveStreamAlreadyStart = false; isStartingRTSP.set(false);
isStartingRTSP = false;
LogUtil.log(TAG, "推流状态已重置"); LogUtil.log(TAG, "推流状态已重置");
} }
public void stopstream() { public void stopstream() {
streamExecutor.execute(() -> { ILiveStreamManager mgr = MediaDataCenter.getInstance().getLiveStreamManager();
ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); mgr.stopStream(new CommonCallbacks.CompletionCallback() {
liveStreamManager.stopStream(new CommonCallbacks.CompletionCallback() { @Override
@Override public void onSuccess() {
public void onSuccess() { LogUtil.log(TAG, "直播关闭成功,重置状态");
LogUtil.log(TAG, "直播关闭成功"); isStartingRTSP.set(false);
} }
@Override
@Override public void onFailure(@NonNull IDJIError e) {
public void onFailure(@NonNull IDJIError idjiError) { LogUtil.log(TAG, "直播关闭失败:" + e.description());
LogUtil.log(TAG, "直播关闭失败"); isStartingRTSP.set(false);
} }
});
}); });
} }
public void startstream() { public void startstream() {
streamExecutor.execute(() -> { ILiveStreamManager mgr = MediaDataCenter.getInstance().getLiveStreamManager();
ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); mgr.startStream(new CommonCallbacks.CompletionCallback() {
liveStreamManager.startStream(new CommonCallbacks.CompletionCallback() { @Override
@Override public void onSuccess() { LogUtil.log(TAG, "直播开启成功"); }
public void onSuccess() { @Override
LogUtil.log(TAG, "直播开启成功"); public void onFailure(@NonNull IDJIError e) { LogUtil.log(TAG, "直播开启失败"); }
}
@Override
public void onFailure(@NonNull IDJIError idjiError) {
LogUtil.log(TAG, "直播开启成功失败");
}
});
}); });
} }
@ -164,8 +177,8 @@ public class StreamManager extends BaseManager {
public void onLiveStreamStatusUpdate(LiveStreamStatus status) { public void onLiveStreamStatusUpdate(LiveStreamStatus status) {
if (status != null) { if (status != null) {
Movement.getInstance().setLiveStatus(status.isStreaming() ? 1 : 0); Movement.getInstance().setLiveStatus(status.isStreaming() ? 1 : 0);
Log.d(TAG, "推流状态" + status.isStreaming() + "帧率:" + status.getFps() + "--" + "码率:" + status.getVbps() + "---" + "延迟:" + status.getRtt()); LogUtil.log(TAG, "推流状态" + status.isStreaming() + "帧率:" + status.getFps() + "--" + "码率:" + status.getVbps() + "---" + "延迟:" + status.getRtt());
sendEvent2Server("推流状态" + status.isStreaming() + "帧率:" + status.getFps() + "--" + "码率:" + status.getVbps() + "---" + "延迟:" + status.getRtt(),1);
} }
} }
@ -204,8 +217,9 @@ public class StreamManager extends BaseManager {
private volatile int startLiveFailTimes; private volatile int startLiveFailTimes;
private volatile boolean isLiveStreamAlreadyStart; // AtomicBoolean 替代 volatile booleanCAS 防并发不会卡死
private volatile boolean isStartingRTSP = false; // 防止并发调用 private final AtomicBoolean isStartingRTSP = new AtomicBoolean(false);
private final AtomicBoolean isStartingRTMP = new AtomicBoolean(false);
// 无限重试指数退避控制 // 无限重试指数退避控制
private static final long RETRY_BASE_MS = 3000; // 初始 3 private static final long RETRY_BASE_MS = 3000; // 初始 3
@ -214,13 +228,12 @@ public class StreamManager extends BaseManager {
// 知眸测试 // 知眸测试
public void startLiveWithCustom() { public void startLiveWithCustom() {
streamExecutor.execute(() -> { Boolean isAircraftConnected = KeyManager.getInstance().getValue(DJIKey.create(ProductKey.KeyConnection));
Boolean isAircraftConnected = KeyManager.getInstance().getValue(DJIKey.create(ProductKey.KeyConnection)); if (isAircraftConnected == null || !isAircraftConnected) {
if (isAircraftConnected == null || !isAircraftConnected) { LogUtil.log(TAG, "飞行器未连接");
LogUtil.log(TAG, "飞行器未连接"); return;
return; }
} ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager();
ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager();
mainHandler.post(() -> { mainHandler.post(() -> {
LogUtil.log(TAG, "自定义推流地址:" + PreferenceUtils.getInstance().getCustomStreamUrl()); LogUtil.log(TAG, "自定义推流地址:" + PreferenceUtils.getInstance().getCustomStreamUrl());
LiveStreamSettings.Builder streamSettingBuilder = new LiveStreamSettings.Builder(); LiveStreamSettings.Builder streamSettingBuilder = new LiveStreamSettings.Builder();
@ -236,9 +249,8 @@ public class StreamManager extends BaseManager {
}); });
if (!liveStreamManager.isStreaming()) { if (!liveStreamManager.isStreaming()) {
doStartLiveCustom(liveStreamManager); mainHandler.postDelayed(() -> doStartLiveCustom(liveStreamManager), 3000);
} }
});
} }
private void doStartLiveCustom(ILiveStreamManager liveStreamManager) { private void doStartLiveCustom(ILiveStreamManager liveStreamManager) {
@ -247,7 +259,6 @@ public class StreamManager extends BaseManager {
public void onSuccess() { public void onSuccess() {
mainHandler.post(() -> { mainHandler.post(() -> {
LogUtil.log(TAG, "自定义推流启动成功"); LogUtil.log(TAG, "自定义推流启动成功");
isLiveStreamAlreadyStart = true;
}); });
} }
@ -255,16 +266,16 @@ public class StreamManager extends BaseManager {
public void onFailure(@NonNull IDJIError error) { public void onFailure(@NonNull IDJIError error) {
mainHandler.post(() -> { mainHandler.post(() -> {
LogUtil.log(TAG, "" + startLiveFailTimes + "次自定义推流失败:" + new Gson().toJson(error)); LogUtil.log(TAG, "" + startLiveFailTimes + "次自定义推流失败:" + new Gson().toJson(error));
if (!isLiveStreamAlreadyStart) { // SDK 真实状态判断不用手动标志位
if (!liveStreamManager.isStreaming()) {
long retryDelay = Math.min(RETRY_BASE_MS * (1L << Math.min(startLiveFailTimes, 4)), RETRY_MAX_MS); long retryDelay = Math.min(RETRY_BASE_MS * (1L << Math.min(startLiveFailTimes, 4)), RETRY_MAX_MS);
startLiveFailTimes++; startLiveFailTimes++;
if (startLiveFailTimes <= 3 || startLiveFailTimes % LOG_INTERVAL == 0) { if (startLiveFailTimes <= 3 || startLiveFailTimes % LOG_INTERVAL == 0) {
LogUtil.log(TAG, "自定义推流准备第" + startLiveFailTimes + "次重试,间隔 " + retryDelay + "ms"); LogUtil.log(TAG, "自定义推流准备第" + startLiveFailTimes + "次重试,间隔 " + retryDelay + "ms");
} }
isStartingRTSP = false; isStartingRTSP.set(false);
isLiveStreamAlreadyStart = false;
mainHandler.postDelayed(() -> { mainHandler.postDelayed(() -> {
streamExecutor.execute(() -> startLiveWithCustom()); startLiveWithCustom();
}, retryDelay); }, retryDelay);
} }
}); });
@ -273,240 +284,370 @@ public class StreamManager extends BaseManager {
} }
private int isliveindex = 1; //1 代表 port 2 代表 fpv // ========== RTMP 推流MQTT rtmp_push 协议 ==========
public void switchptspfpv(ComponentIndexType ComponentIndex, MessageDown message) { /**
if(isliveindex==2){ * MQTT 收到 rtmp_push 协议后调用先停止当前推流再启动 RTMP 推流
sendMsg2Server(message); * @param message MQTT 下发的消息data.rtmp_url RTMP 推流地址
*/
public void startLiveWithRTMP(MessageDown message) {
// CAS 防并发
if (!isStartingRTMP.compareAndSet(false, true)) {
LogUtil.log(TAG, "startLiveWithRTMP 正在执行中,忽略本次调用");
return; return;
} }
isliveindex = 2;
String rtmpUrl = message.getData() != null ? message.getData().getRtmp_url() : null;
if (rtmpUrl == null || rtmpUrl.isEmpty()) {
LogUtil.log(TAG, "RTMP URL 为空");
isStartingRTMP.set(false);
sendFailMsg2Server(message, "RTMP URL 为空");
return;
}
LogUtil.log(TAG, "========== 开始 RTMP 推流流程 ==========");
LogUtil.log(TAG, "RTMP URL: " + rtmpUrl);
Boolean isAircraftConnected = KeyManager.getInstance().getValue(DJIKey.create(FlightControllerKey.KeyConnection));
if (isAircraftConnected == null || !isAircraftConnected) {
LogUtil.log(TAG, "飞行器未连接");
isStartingRTMP.set(false);
sendFailMsg2Server(message, "飞行器未连接");
return;
}
ILiveStreamManager mgr = MediaDataCenter.getInstance().getLiveStreamManager();
if (mgr == null) {
LogUtil.log(TAG, "LiveStreamManager 为 null");
sendFailMsg2Server(message, "LiveStreamManager 为 null");
isStartingRTMP.set(false);
return;
}
// 杀掉所有 RTSP 排队中的回调重置 RTSP 状态防止 RTSP 延迟任务覆盖 RTMP 配置
resetStreamState();
// 先发送回执表示收到指令
sendMsg2Server(message); sendMsg2Server(message);
ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager();
LogUtil.log(TAG, "切换 RTSP 推流 fpv:" + PreferenceUtils.getInstance().getRtspUserName()
+ "--" + PreferenceUtils.getInstance().getRtspPort() + "--" + PreferenceUtils.getInstance().getRtspPassWord());
LiveStreamSettings.Builder streamSettingBuilder = new LiveStreamSettings.Builder(); // 先停止当前推流成功或失败都继续启动 RTMP
mgr.stopStream(new CommonCallbacks.CompletionCallback() {
LiveStreamSettings streamSettings = streamSettingBuilder.setLiveStreamType(LiveStreamType.RTSP) @Override
.setRtspSettings(new RtspSettings.Builder().setPassWord(PreferenceUtils.getInstance().getRtspPassWord()). public void onSuccess() {
setPort(Integer.parseInt(PreferenceUtils.getInstance().getRtspPort())). LogUtil.log(TAG, "旧流已停止,开始配置 RTMP");
setUserName(PreferenceUtils.getInstance().getRtspUserName()).build()).build(); configureAndStartRTMP(mgr, rtmpUrl, message);
liveStreamManager.setLiveStreamSettings(streamSettings);
liveStreamManager.setCameraIndex(ComponentIndex);
liveStreamManager.setLiveStreamQuality(StreamQuality.FULL_HD);
liveStreamManager.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO);
}
public void switchptspport(ComponentIndexType ComponentIndex, MessageDown message) {
if(isliveindex==1){
sendMsg2Server(message);
return;
}
isliveindex = 1;
sendMsg2Server(message);
ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager();
LogUtil.log(TAG, "切换 RTSP 推流 port:" + PreferenceUtils.getInstance().getRtspUserName()
+ "--" + PreferenceUtils.getInstance().getRtspPort() + "--" + PreferenceUtils.getInstance().getRtspPassWord());
LiveStreamSettings.Builder streamSettingBuilder = new LiveStreamSettings.Builder();
LiveStreamSettings streamSettings = streamSettingBuilder.setLiveStreamType(LiveStreamType.RTSP)
.setRtspSettings(new RtspSettings.Builder().setPassWord(PreferenceUtils.getInstance().getRtspPassWord()).
setPort(Integer.parseInt(PreferenceUtils.getInstance().getRtspPort())).
setUserName(PreferenceUtils.getInstance().getRtspUserName()).build()).build();
liveStreamManager.setLiveStreamSettings(streamSettings);
liveStreamManager.setCameraIndex(ComponentIndex);
liveStreamManager.setLiveStreamQuality(StreamQuality.FULL_HD);
liveStreamManager.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO);
}
// ========== 核心修复RTSP 推流入口全部在子线程执行 ==========
public void startLiveWithRTSP() {
// 防止并发调用
if (isStartingRTSP) {
LogUtil.log(TAG, "startLiveWithRTSP 正在执行中,忽略本次调用");
return;
}
streamExecutor.execute(() -> {
isStartingRTSP = true;
if (startLiveFailTimes == 0) {
isLiveStreamAlreadyStart = false;
LogUtil.log(TAG, "========== 开始 RTSP 推流流程 ==========");
} }
Boolean isAircraftConnected = KeyManager.getInstance().getValue(DJIKey.create(FlightControllerKey.KeyConnection)); @Override
if (isAircraftConnected == null || !isAircraftConnected) { public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "飞行器未连接"); LogUtil.log(TAG, "停流失败,仍尝试启动 RTMP: " + e.description());
isStartingRTSP = false; configureAndStartRTMP(mgr, rtmpUrl, message);
return;
} }
ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager();
if (liveStreamManager == null) {
LogUtil.log(TAG, "LiveStreamManager 为 null");
isStartingRTSP = false;
return;
}
if (liveStreamManager.isStreaming()) {
LogUtil.log(TAG, "RTSP 推流已在运行,无需重复启动");
isLiveStreamAlreadyStart = true;
isStartingRTSP = false;
return;
}
if (!MainActivity.Companion.getStreamReceive()) {
LogUtil.log(TAG, "相机流未准备好,尝试模拟点击 FPV Widget 恢复");
mainHandler.post(() -> {
MainActivity mainActivity = MainActivity.Companion.getInstance();
if (mainActivity != null) {
mainActivity.smartRefreshVideoStream();
}
});
mainHandler.postDelayed(() -> {
streamExecutor.execute(() -> {
startLiveFailTimes++;
if (startLiveFailTimes <= 3 || startLiveFailTimes % LOG_INTERVAL == 0) {
LogUtil.log(TAG, "相机流未准备好,第" + startLiveFailTimes + "次重试");
}
startLiveWithRTSP();
});
}, 2000);
isStartingRTSP = false; // 释放锁让重试能正常进入
return;
}
String rtspUser = PreferenceUtils.getInstance().getRtspUserName();
String rtspPort = PreferenceUtils.getInstance().getRtspPort();
String rtspPass = PreferenceUtils.getInstance().getRtspPassWord();
if (rtspUser == null || rtspPort == null || rtspPass == null ||
rtspUser.isEmpty() || rtspPort.isEmpty() || rtspPass.isEmpty()) {
LogUtil.log(TAG, "RTSP 配置参数有误user=" + rtspUser + ", port=" + rtspPort);
isStartingRTSP = false;
return;
}
LogUtil.log(TAG, "RTSP 配置检查通过user=" + rtspUser + ", port=" + rtspPort + ", camera=" + (isliveindex == 1 ? "PORT_1" : "FPV"));
//等待相机模式稳定避免刚切换模式就推流
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
//设置 RTSP 参数
final ILiveStreamManager finalLiveStreamManager = liveStreamManager;
mainHandler.post(() -> {
LiveStreamSettings.Builder streamSettingBuilder = new LiveStreamSettings.Builder();
LiveStreamSettings streamSettings = streamSettingBuilder.setLiveStreamType(LiveStreamType.RTSP)
.setRtspSettings(new RtspSettings.Builder()
.setPassWord(rtspPass)
.setPort(Integer.parseInt(rtspPort))
.setUserName(rtspUser)
.build())
.build();
finalLiveStreamManager.setLiveStreamSettings(streamSettings);
// 设置相机源
ComponentIndexType cameraIndex = (isliveindex == 1) ? ComponentIndexType.PORT_1 : ComponentIndexType.FPV;
finalLiveStreamManager.setCameraIndex(cameraIndex);
finalLiveStreamManager.setLiveStreamQuality(StreamQuality.FULL_HD);
finalLiveStreamManager.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO);
LogUtil.log(TAG, "RTSP 参数设置完成,等待 500ms 后启动推流");
mainHandler.postDelayed(() -> {
streamExecutor.execute(() -> {
// 9. 启动推流前再次检查
if (finalLiveStreamManager.isStreaming()) {
LogUtil.log(TAG, "推流已在运行,跳过启动");
isLiveStreamAlreadyStart = true;
isStartingRTSP = false;
return;
}
LogUtil.log(TAG, "开始调用 startStream...");
doStartLiveWithRTSP(finalLiveStreamManager, false);
});
}, 500);
});
}); });
} }
private void configureAndStartRTMP(ILiveStreamManager mgr, String rtmpUrl, MessageDown message) {
mainHandler.post(() -> {
LiveStreamSettings streamSettings = new LiveStreamSettings.Builder()
.setLiveStreamType(LiveStreamType.RTMP)
.setRtmpSettings(new RtmpSettings.Builder()
.setUrl(rtmpUrl)
.build())
.build();
mgr.setLiveStreamSettings(streamSettings);
mgr.setCameraIndex((isliveindex == 1) ? ComponentIndexType.PORT_1 : ComponentIndexType.FPV);
mgr.setLiveStreamQuality(StreamQuality.FULL_HD);
mgr.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO);
// ========== 新增实际启动流的私有方法确保在子线程执行 ========== LogUtil.log(TAG, "RTMP 配置完成3s 后启动推流");
private void doStartLiveWithRTSP(ILiveStreamManager liveStreamManager, boolean isRestart) {
if (liveStreamManager.isStreaming()) { mainHandler.postDelayed(() -> {
LogUtil.log(TAG, "推流已在运行,跳过启动 (isRestart=" + isRestart + ")"); if (mgr.isStreaming()) {
isLiveStreamAlreadyStart = true; // 旧流未停干净主动再停一次然后重试启动
isStartingRTSP = false; // 重置并发标志 LogUtil.log(TAG, "SDK显示仍在推流再次停止后重试");
mgr.stopStream(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "二次停流成功,重试 RTMP 启动");
mainHandler.postDelayed(() -> doStartRTMPStream(mgr, message), 2000);
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "二次停流失败,仍尝试 RTMP 启动: " + e.description());
mainHandler.postDelayed(() -> doStartRTMPStream(mgr, message), 2000);
}
});
return;
}
doStartRTMPStream(mgr, message);
}, 3000);
});
}
private void doStartRTMPStream(ILiveStreamManager mgr, MessageDown message) {
if (mgr.isStreaming()) {
// 二次停流后仍显示推流中再做最后一次 stop + retry不轻易放弃
LogUtil.log(TAG, "RTMP 启动前检查SDK仍在推流最后一次强制停流");
mgr.stopStream(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "三次停流成功,最终尝试 RTMP 启动");
mainHandler.postDelayed(() -> {
mgr.startStream(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "RTMP 推流启动成功(终极重试)");
isStartingRTMP.set(false);
LogUtil.log(TAG, "========== RTMP 推流启动成功 ==========");
}
@Override
public void onFailure(@NonNull IDJIError error) {
LogUtil.log(TAG, "RTMP 推流最终失败: " + error.description());
isStartingRTMP.set(false);
sendFailMsg2Server(message, "RTMP 推流最终失败: " + error.description());
}
});
}, 2000);
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "三次停流失败,彻底放弃");
isStartingRTMP.set(false);
sendFailMsg2Server(message, "RTMP 推流失败:多次停流无效");
}
});
return; return;
} }
LogUtil.log(TAG, "开始调用 RTMP startStream...");
LogUtil.log(TAG, "开始调用 startStream... (isRestart=" + isRestart + ")"); mgr.startStream(new CommonCallbacks.CompletionCallback() {
liveStreamManager.startStream(new CommonCallbacks.CompletionCallback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
mainHandler.post(() -> { LogUtil.log(TAG, "RTMP 推流启动成功");
LogUtil.log(TAG, "自定义 RTSP 推流启动成功" + (isRestart ? "(重启)" : "")); isStartingRTMP.set(false);
isliveindex = 1; LogUtil.log(TAG, "========== RTMP 推流启动成功 ==========");
isLiveStreamAlreadyStart = true;
startLiveFailTimes = 0; // 重置失败计数
isStartingRTSP = false; // 重置并发标志
LogUtil.log(TAG, "========== RTSP 推流启动成功 ==========");
// 启动5秒定时刷新视频流防止起飞卡死
// startStreamRefreshTimer();
// 开始端口扫描
// SimplePortScanner.getInstance().startScan();
});
} }
@Override @Override
public void onFailure(@NonNull IDJIError error) { public void onFailure(@NonNull IDJIError error) {
mainHandler.post(() -> { LogUtil.log(TAG, "RTMP 推流启动失败: " + error.description());
String detailedError = "" + startLiveFailTimes + "次开始 RTSP 推流失败type=" + error.errorType()+ ", code=" + error.errorCode() + ", hint=" + error.hint(); isStartingRTMP.set(false);
LogUtil.log(TAG, detailedError); sendFailMsg2Server(message, "RTMP 推流失败: " + error.description());
LogUtil.log(TAG, "完整错误:" + new Gson().toJson(error));
if (!isLiveStreamAlreadyStart) {
// 计算指数退避间隔3s 6s 12s 24s 30s(封顶)
long retryDelay = Math.min(RETRY_BASE_MS * (1L << Math.min(startLiveFailTimes, 4)), RETRY_MAX_MS);
startLiveFailTimes++;
if (startLiveFailTimes <= 3 || startLiveFailTimes % LOG_INTERVAL == 0) {
LogUtil.log(TAG, "准备第" + startLiveFailTimes + "次重试,间隔 " + retryDelay + "ms");
}
// 重置所有状态让下次重试是干净的
isStartingRTSP = false;
isLiveStreamAlreadyStart = false;
stopstream(); // 先关再开避免端口占用
// 指数退避重试
mainHandler.postDelayed(() -> {
startLiveWithRTSP();
}, retryDelay);
} else {
isStartingRTSP = false;
}
});
} }
}); });
} }
// ========== 停止 RTMP 推流MQTT stop_rtmp 协议 ==========
/**
* MQTT 收到 stop_rtmp 协议后调用停止 RTMP 推流切回默认 RTSP
* @param message MQTT 下发的消息
*/
public void stopRTMP(MessageDown message) {
sendMsg2Server(message);
LogUtil.log(TAG, "========== 停止 RTMP 推流,切回 RTSP ==========");
// 立即释放 RTMP + 清场不阻塞快速连发的 push_rtmp
isStartingRTMP.set(false);
resetStreamState();
ILiveStreamManager mgr = MediaDataCenter.getInstance().getLiveStreamManager();
if (mgr == null) {
LogUtil.log(TAG, "LiveStreamManager 为 null");
return;
}
mgr.stopStream(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "RTMP 已停,启动 RTSP");
startLiveWithRTSP();
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "停流失败,仍启动 RTSP: " + e.description());
startLiveWithRTSP();
}
});
}
private volatile int isliveindex = 1; //1 代表 port 2 代表 fpv
/**
* 切换 RTSP 推流相机源 DJI 示例一样直接设 cameraIndexSDK 内部切换
*/
public void switchptspfpv(ComponentIndexType ComponentIndex, MessageDown message) {
if (isliveindex == 2) { sendMsg2Server(message); return; }
isliveindex = 2;
LogUtil.log(TAG, "切换推流到 FPV");
MediaDataCenter.getInstance().getLiveStreamManager().setCameraIndex(ComponentIndexType.FPV);
sendMsg2Server(message);
}
public void switchptspport(ComponentIndexType ComponentIndex, MessageDown message) {
if (isliveindex == 1) { sendMsg2Server(message); return; }
isliveindex = 1;
LogUtil.log(TAG, "切换推流到 PORT_1");
MediaDataCenter.getInstance().getLiveStreamManager().setCameraIndex(ComponentIndexType.PORT_1);
sendMsg2Server(message);
}
public void startLiveWithRTSP() {
// CAS 防并发只有一个线程能拿到 true其他被拦截
if (!isStartingRTSP.compareAndSet(false, true)) {
LogUtil.log(TAG, "startLiveWithRTSP 正在执行中,忽略本次调用");
return;
}
// RTMP 推流正在进行中不再启动 RTSP防止覆盖
if (isStartingRTMP.get()) {
LogUtil.log(TAG, "RTMP 推流正在进行中,取消 RTSP 启动");
isStartingRTSP.set(false);
return;
}
if (startLiveFailTimes == 0) {
LogUtil.log(TAG, "========== 开始 RTSP 推流流程 ==========");
}
Boolean isAircraftConnected = KeyManager.getInstance().getValue(DJIKey.create(FlightControllerKey.KeyConnection));
if (isAircraftConnected == null || !isAircraftConnected) {
LogUtil.log(TAG, "飞行器未连接");
isStartingRTSP.set(false);
return;
}
ILiveStreamManager mgr = MediaDataCenter.getInstance().getLiveStreamManager();
if (mgr == null) {
LogUtil.log(TAG, "LiveStreamManager 为 null");
isStartingRTSP.set(false);
return;
}
// SDK 真实状态判断是否已在推流
if (mgr.isStreaming()) {
LogUtil.log(TAG, "RTSP 推流已在运行SDK状态无需重复启动");
isStartingRTSP.set(false);
return;
}
if (!MainActivity.Companion.getStreamReceive()) {
LogUtil.log(TAG, "相机流未准备好3s 后重试");
startLiveFailTimes++;
isStartingRTSP.set(false);
// 重试前检查 RTMP 是否已接管
mainHandler.postDelayed(() -> {
if (isStartingRTMP.get()) {
LogUtil.log(TAG, "RTMP 已接管,取消 RTSP 重试(相机流就绪等待)");
return;
}
startLiveWithRTSP();
}, 3000);
return;
}
String rtspUser = PreferenceUtils.getInstance().getRtspUserName();
String rtspPort = PreferenceUtils.getInstance().getRtspPort();
String rtspPass = PreferenceUtils.getInstance().getRtspPassWord();
if (rtspUser == null || rtspPort == null || rtspPass == null ||
rtspUser.isEmpty() || rtspPort.isEmpty() || rtspPass.isEmpty()) {
LogUtil.log(TAG, "RTSP 配置参数有误");
isStartingRTSP.set(false);
return;
}
LogUtil.log(TAG, "RTSP 配置检查通过user=" + rtspUser + ", port=" + rtspPort);
LiveStreamSettings streamSettings = new LiveStreamSettings.Builder()
.setLiveStreamType(LiveStreamType.RTSP)
.setRtspSettings(new RtspSettings.Builder()
.setPassWord(rtspPass)
.setPort(Integer.parseInt(rtspPort))
.setUserName(rtspUser)
.build())
.build();
mgr.setLiveStreamSettings(streamSettings);
mgr.setCameraIndex((isliveindex == 1) ? ComponentIndexType.PORT_1 : ComponentIndexType.FPV);
mgr.setLiveStreamQuality(StreamQuality.FULL_HD);
mgr.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO);
LogUtil.log(TAG, "参数设置完成3s 后启动推流");
mainHandler.postDelayed(() -> {
// RTMP 已接管取消 RTSP 启动
if (isStartingRTMP.get()) {
LogUtil.log(TAG, "RTMP 已接管,取消 RTSP 启动3s 等待期间 RTMP 介入)");
isStartingRTSP.set(false);
return;
}
// 启动前再次用 SDK 状态确认
if (mgr.isStreaming()) {
LogUtil.log(TAG, "SDK显示已在推流跳过");
isStartingRTSP.set(false);
return;
}
LogUtil.log(TAG, "开始调用 startStream...");
mgr.startStream(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "RTSP 推流启动成功");
startLiveFailTimes = 0;
isStartingRTSP.set(false);
LogUtil.log(TAG, "========== RTSP 推流启动成功 ==========");
}
@Override
public void onFailure(@NonNull IDJIError error) {
LogUtil.log(TAG, "" + startLiveFailTimes + "次 RTSP 推流失败:" + error.description());
// SDK 真实状态判断是否要重试
if (!mgr.isStreaming()) {
// RTMP 已接管不再重试 RTSP
if (isStartingRTMP.get()) {
LogUtil.log(TAG, "RTMP 已接管,取消 RTSP 失败重试");
isStartingRTSP.set(false);
return;
}
long retryDelay = Math.min(RETRY_BASE_MS * (1L << Math.min(startLiveFailTimes, 4)), RETRY_MAX_MS);
startLiveFailTimes++;
isStartingRTSP.set(false);
// 停流 等回调 重试
mgr.stopStream(new CommonCallbacks.CompletionCallback() {
@Override
public void onSuccess() {
LogUtil.log(TAG, "重试前停流成功," + retryDelay + "ms 后重试");
// 重试前再次检查 RTMP 状态
mainHandler.postDelayed(() -> {
if (isStartingRTMP.get()) {
LogUtil.log(TAG, "RTMP 已接管,取消 RTSP 重试");
return;
}
startLiveWithRTSP();
}, retryDelay);
}
@Override
public void onFailure(@NonNull IDJIError e) {
LogUtil.log(TAG, "重试前停流失败,仍重试");
// 重试前再次检查 RTMP 状态
mainHandler.postDelayed(() -> {
if (isStartingRTMP.get()) {
LogUtil.log(TAG, "RTMP 已接管,取消 RTSP 重试");
return;
}
startLiveWithRTSP();
}, retryDelay);
}
});
} else {
isStartingRTSP.set(false);
}
}
});
}, 3000);
}
} }

View File

@ -414,7 +414,7 @@ public class Aprondown {
} }
} else { } else {
LogUtil.log(TAG_LOG, "执行位移"); LogUtil.log(TAG_LOG, "执行位移");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.5f);
} }
} else if (lostDuration > 8000) { } else if (lostDuration > 8000) {
LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点"); LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点");
@ -570,6 +570,30 @@ public class Aprondown {
Apronmixvalue.getInstance().setIsaglinetrue(false); // 下次降落重新对准 Apronmixvalue.getInstance().setIsaglinetrue(false); // 下次降落重新对准
} }
/** ★ 急停清理 */
public void stopAndReset() {
isStartAruco = false;
startFastStick = false;
frameCounter = 0;
if (lastFuture != null && !lastFuture.isDone()) {
lastFuture.cancel(true);
lastFuture = null;
}
handler.removeCallbacks(runnable);
handlerCallbackCount = 0;
isHeightStableMonitoring = false;
lastHeightCheckTime = 0;
lastUltrasonicHeight = 0;
arucoNotFoundTag = false;
startTime = 0;
endTime = 0;
dropTimesTag = false;
dropTimes = 0;
if (pidControlX != null) pidControlX.reset();
if (pidControlY != null) pidControlY.reset();
LogUtil.log(TAG_LOG, "【急停清理】已重置");
}
private double calculateYawErrorFromCorners(Point[] pts) { private double calculateYawErrorFromCorners(Point[] pts) {
double dxTop = pts[1].x - pts[0].x; double dxTop = pts[1].x - pts[0].x;
@ -702,7 +726,7 @@ public class Aprondown {
@Override @Override
public void run() { public void run() {
performOperation(); performOperation();
if (handlerCallbackCount < 15) { if (handlerCallbackCount < 18) {
handler.postDelayed(this, 50); handler.postDelayed(this, 50);
} else { } else {
performNextStep(); performNextStep();

View File

@ -1035,7 +1035,7 @@ public class Aprongim {
} else { } else {
// 高空丢失下降寻找 // 高空丢失下降寻找
LogUtil.log(TAG_LOG, "【执行移动】丢失下降 vz=-0.3"); LogUtil.log(TAG_LOG, "【执行移动】丢失下降 vz=-0.3");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.5f);
} }
} else if (lostDuration > 8000) { } else if (lostDuration > 8000) {
LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点"); LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点");
@ -1079,13 +1079,43 @@ public class Aprongim {
} }
} }
/** ★ 急停清理 */
public void stopAndReset() {
isStartAruco = false;
startFastStick = false;
frameCounter = 0;
if (lastFuture != null && !lastFuture.isDone()) {
lastFuture.cancel(true);
lastFuture = null;
}
handler.removeCallbacks(runnable);
handlerCallbackCount = 0;
isYawAligned = false;
currentLandingMode = 0;
counterRound = 0;
for (int i = 0; i < landingCounters.length; i++) {
landingCounters[i] = 0;
}
isHeightStableMonitoring = false;
lastHeightCheckTime = 0;
lastUltrasonicHeight = 0;
arucoNotFoundTag = false;
startTime = 0;
endTime = 0;
dropTimesTag = false;
dropTimes = 0;
if (pidControlX != null) pidControlX.reset();
if (pidControlY != null) pidControlY.reset();
LogUtil.log(TAG_LOG, "【急停清理】已重置");
}
private int handlerCallbackCount = 0; private int handlerCallbackCount = 0;
private Handler handler = new Handler(Looper.getMainLooper()); private Handler handler = new Handler(Looper.getMainLooper());
private Runnable runnable = new Runnable() { private Runnable runnable = new Runnable() {
@Override @Override
public void run() { public void run() {
performOperation(); performOperation();
if (handlerCallbackCount < 15) { if (handlerCallbackCount < 18) {
handler.postDelayed(this, 50); handler.postDelayed(this, 50);
} else { } else {
performNextStep(); performNextStep();

View File

@ -61,7 +61,7 @@ public class Apronmixvalue {
Aprondown.getInstance().setDropTimes(0); Aprondown.getInstance().setDropTimes(0);
// 如果高度已经大于 40 分米直接切换不用上升 // 如果高度已经大于 40 分米直接切换不用上升
if (Movement.getInstance().getUltrasonicHeight() >= 40) { if (Movement.getInstance().getUltrasonicHeight() >= 40 || Movement.getInstance().getElevation()>4) {
Synchronizedstatus.setSwitchtime(true); Synchronizedstatus.setSwitchtime(true);
return; return;
} }
@ -69,7 +69,7 @@ public class Apronmixvalue {
Runnable runnable = new Runnable() { Runnable runnable = new Runnable() {
@Override @Override
public void run() { public void run() {
if (Movement.getInstance().getUltrasonicHeight() < 50) { if (Movement.getInstance().getUltrasonicHeight() < 50 && Movement.getInstance().getElevation() <5) {
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f);
handler.postDelayed(this, 200); handler.postDelayed(this, 200);
} else { } else {

File diff suppressed because it is too large Load Diff

View File

@ -311,7 +311,7 @@ public class ApronArucoDetect {
} }
} else { } else {
LogUtil.log(TAG_LOG, "执行位移"); LogUtil.log(TAG_LOG, "执行位移");
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.5f);
} }
isHeightStableMonitoring = false; isHeightStableMonitoring = false;
@ -390,23 +390,21 @@ public class ApronArucoDetect {
dropTimesTag = true; dropTimesTag = true;
} else { } else {
// 原有丢失处理 // 丢失处理
LogUtil.log(TAG_LOG, "找不到了二维码"); int lostUltHeight = Movement.getInstance().getUltrasonicHeight();
// 新增识别失败截图保存
if (saveFailScreenshot) {
saveFailScreenshot(grayImgMat, width, height);
}
if (!arucoNotFoundTag) { if (!arucoNotFoundTag) {
startTime = System.currentTimeMillis(); startTime = System.currentTimeMillis();
arucoNotFoundTag = true; arucoNotFoundTag = true;
LogUtil.log(TAG_LOG, String.format("【丢失开始】ult=%d dropTimes=%d", lostUltHeight, dropTimes));
} }
endTime = System.currentTimeMillis(); endTime = System.currentTimeMillis();
long lostDuration = endTime - startTime; long lostDuration = endTime - startTime;
if (lostDuration > 1000 && lostDuration <= 12000) { if (lostDuration > 1000 && lostDuration <= 12000) {
if (Movement.getInstance().getUltrasonicHeight() <= 20) { if (lostUltHeight <= 20) {
LogUtil.log(TAG_LOG, String.format("【丢失拉高】vz=3.0 ult=%d 丢失=%dms dropTimes=%d",
lostUltHeight, lostDuration, dropTimes));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f);
if (dropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) { if (dropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) {
LogUtil.log(TAG_LOG, "超过复降限制,去备降点"); LogUtil.log(TAG_LOG, "超过复降限制,去备降点");
@ -420,11 +418,11 @@ public class ApronArucoDetect {
LogUtil.log(TAG_LOG, "复降第:" + dropTimes + ""); LogUtil.log(TAG_LOG, "复降第:" + dropTimes + "");
} }
} else { } else {
LogUtil.log(TAG_LOG, "执行位移"); LogUtil.log(TAG_LOG, String.format("【丢失下降】vz=-0.3 ult=%d 丢失=%dms", lostUltHeight, lostDuration));
DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f);
} }
} else if (lostDuration > 8000) { } else if (lostDuration > 8000) {
LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点"); LogUtil.log(TAG_LOG, String.format("【丢失超时】触发备降 丢失=%dms", lostDuration));
AlternateLandingManager.getInstance().startTaskProcess(null); AlternateLandingManager.getInstance().startTaskProcess(null);
Movement.getInstance().setAlternate(true); Movement.getInstance().setAlternate(true);
} }
@ -697,6 +695,30 @@ public class ApronArucoDetect {
failScreenshotIndex = 0; failScreenshotIndex = 0;
} }
/** ★ 急停清理 */
public void stopAndReset() {
isStartAruco = false;
startFastStick = false;
frameCounter = 0;
if (lastFuture != null && !lastFuture.isDone()) {
lastFuture.cancel(true);
lastFuture = null;
}
handler.removeCallbacks(runnable);
handlerCallbackCount = 0;
isHeightStableMonitoring = false;
lastHeightCheckTime = 0;
lastUltrasonicHeight = 0;
arucoNotFoundTag = false;
startTime = 0;
endTime = 0;
dropTimesTag = false;
dropTimes = 0;
if (pidControlX != null) pidControlX.reset();
if (pidControlY != null) pidControlY.reset();
LogUtil.log(TAG_LOG, "【急停清理】已重置");
}
private double calculateYawErrorFromCorners(Point[] pts) { private double calculateYawErrorFromCorners(Point[] pts) {
double dxTop = pts[1].x - pts[0].x; double dxTop = pts[1].x - pts[0].x;
@ -750,6 +772,13 @@ public class ApronArucoDetect {
float vx = (float) Math.max(-0.2, Math.min(0.2, rawVx)); float vx = (float) Math.max(-0.2, Math.min(0.2, rawVx));
float vy = (float) Math.max(-0.2, Math.min(0.2, rawVy)); float vy = (float) Math.max(-0.2, Math.min(0.2, rawVy));
LogUtil.log(TAG_LOG, String.format(
"【PID链路】errX=%.0f errY=%.0f scale=%.0f | 输入: pidX_in=%.4f pidY_in=%.4f | PID输出: rawVx=%.4f rawVy=%.4f | 钳位后: vx=%.4f vy=%.4f",
errX, errY, scaleFactor,
errX / scaleFactor, -errY / scaleFactor,
rawVx, rawVy,
vx, vy));
double pixelWidth = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) + double pixelWidth = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) +
Math.pow(pts[1].y - pts[0].y, 2)); Math.pow(pts[1].y - pts[0].y, 2));
@ -780,31 +809,12 @@ public class ApronArucoDetect {
float vz; float vz;
if (currentHeight <= 4) { if (currentHeight <= 4) {
vz = 0.0f; vz = 0.0f;
if (Math.abs(errX) > 120) { // 修复低空改用 PID 连续输出 + 低速度上限去掉阶梯式开关控制
vx = rawVx > 0 ? 0.135f : -0.135f; // 之前 bang-bang 固定档位导致来回震荡0.050.090.135-0.09...
} else if (Math.abs(errX) > 80) { // 现在用 PID 平滑输出上限压到 0.06m/s避免低空猛冲晃出二维码范围
vx = rawVx > 0 ? 0.09f : -0.09f; float lowMaxSpeed = 0.06f;
} else if (Math.abs(errX) > 60) { vx = (float) Math.max(-lowMaxSpeed, Math.min(lowMaxSpeed, rawVx));
vx = rawVx > 0 ? 0.07f : -0.07f; vy = (float) Math.max(-lowMaxSpeed, Math.min(lowMaxSpeed, rawVy));
} else if (Math.abs(errX) > 30) {
vx = rawVx > 0 ? 0.05f : -0.05f;
} else {
vx = 0f;
}
if (Math.abs(errY) > 120) {
vy = rawVy > 0 ? 0.135f : -0.135f;
} else if (Math.abs(errY) > 80) {
vy = rawVy > 0 ? 0.09f : -0.09f;
} else if (Math.abs(errY) > 60) {
vy = rawVy > 0 ? 0.07f : -0.07f;
} else if (Math.abs(errY) > 30) {
vy = rawVy > 0 ? 0.05f : -0.05f;
} else {
vy = 0f; // 修正
}
} else if (currentHeight <= 8) { } else if (currentHeight <= 8) {
vz = SLOW_SUPER_SPEED; vz = SLOW_SUPER_SPEED;
} else { } else {
@ -813,8 +823,11 @@ public class ApronArucoDetect {
DroneHelper.getInstance().moveVxVyYawrateHeight(vx, vy, yawRate, vz); DroneHelper.getInstance().moveVxVyYawrateHeight(vx, vy, yawRate, vz);
LogUtil.log(TAG_LOG, "vx" + vx + "vy" + vy + " errX=" + (int) errX + " errY=" + (int) errY + LogUtil.log(TAG_LOG, String.format(
" pixelW=" + (int) pixelWidth + " vz=" + vz + " ult=" + currentHeight + " yaw=" + yawRate); "【摇杆下发】vx=%.3f vy=%.3f vz=%.2f yaw=%.1f | errX=%d errY=%d pixelW=%d ult=%d | LENS=(%d,%d)",
vx, vy, vz, yawRate,
(int)errX, (int)errY, (int)pixelWidth, currentHeight,
(int)LENS_OFFSET_X, (int)LENS_OFFSET_Y));
} }
private int handlerCallbackCount = 0; private int handlerCallbackCount = 0;
@ -823,7 +836,7 @@ public class ApronArucoDetect {
@Override @Override
public void run() { public void run() {
performOperation(); performOperation();
if (handlerCallbackCount < 15) { if (handlerCallbackCount < 18) {
handler.postDelayed(this, 50); handler.postDelayed(this, 50);
} else { } else {
performNextStep(); performNextStep();

View File

@ -1013,13 +1013,61 @@ public class ApronArucoDetectPort {
} }
} }
/** ★ 急停清理:重置所有状态,取消所有定时器,确保下次降落可以重新触发 */
public void stopAndReset() {
// 1. 取消入口 guard否则下次 detectArucoTags 直接 return
isStartAruco = false;
startFastStick = false;
frameCounter = 0;
// 2. 取消 ScheduledExecutorService 里的 pending 检测任务
if (lastFuture != null && !lastFuture.isDone()) {
lastFuture.cancel(true);
lastFuture = null;
}
// 3. 停止快速下降 handler
handler.removeCallbacks(runnable);
handlerCallbackCount = 0;
// 4. 重置旋转阶段
isYawAligned = false;
currentLandingMode = 0;
// 5. 重置分支计数器
counterRound = 0;
for (int i = 0; i < landingCounters.length; i++) {
landingCounters[i] = 0;
}
// 6. 重置高度稳定性监测
isHeightStableMonitoring = false;
lastHeightCheckTime = 0;
lastUltrasonicHeight = 0;
// 7. 重置丢失计时器
arucoNotFoundTag = false;
startTime = 0;
endTime = 0;
// 8. 重置复降计数
dropTimesTag = false;
dropTimes = 0;
// 9. PID 复位
if (pidControlX != null) pidControlX.reset();
if (pidControlY != null) pidControlY.reset();
LogUtil.log(TAG_LOG, "【急停清理】所有状态和定时器已重置");
}
private int handlerCallbackCount = 0; private int handlerCallbackCount = 0;
private Handler handler = new Handler(Looper.getMainLooper()); private Handler handler = new Handler(Looper.getMainLooper());
private Runnable runnable = new Runnable() { private Runnable runnable = new Runnable() {
@Override @Override
public void run() { public void run() {
performOperation(); performOperation();
if (handlerCallbackCount < 15) { if (handlerCallbackCount < 18) {
handler.postDelayed(this, 50); handler.postDelayed(this, 50);
} else { } else {
performNextStep(); performNextStep();

View File

@ -899,6 +899,34 @@ public class ApronArucodownmany {
} }
} }
/** ★ 急停清理 */
public void stopAndReset() {
isStartAruco = false;
startFastStick = false;
frameCounter = 0;
if (lastFuture != null && !lastFuture.isDone()) {
lastFuture.cancel(true);
lastFuture = null;
}
handler.removeCallbacks(runnable);
handlerCallbackCount = 0;
counterRound = 0;
for (int i = 0; i < landingCounters.length; i++) {
landingCounters[i] = 0;
}
isHeightStableMonitoring = false;
lastHeightCheckTime = 0;
lastUltrasonicHeight = 0;
arucoNotFoundTag = false;
startTime = 0;
endTime = 0;
dropTimesTag = false;
dropTimes = 0;
if (pidControlX != null) pidControlX.reset();
if (pidControlY != null) pidControlY.reset();
LogUtil.log(TAG_LOG, "【急停清理】已重置");
}
private double calculateYawErrorFromCorners(Point[] pts) { private double calculateYawErrorFromCorners(Point[] pts) {
double dxTop = pts[1].x - pts[0].x; double dxTop = pts[1].x - pts[0].x;

View File

@ -40,10 +40,26 @@ public class MqttManager {
} }
public void needConnect() { public void needConnect() {
// 已经连接就不要再建新的避免生成新handle导致旧handle失效
if (mqttAndroidClient != null && mqttAndroidClient.isConnected()) {
LogUtil.log(TAG, "MQTT已连接跳过重复needConnect");
return;
}
initMqttClientParams(); initMqttClientParams();
} }
private void initMqttClientParams() { private void initMqttClientParams() {
mqttAndroidClient = new MqttAndroidClient(ApronApp.Companion.getApplication(), AMSConfig.getInstance().getMqttServerUri(), generateRandomString(10)); // 先关闭旧的client释放MqttService里的旧handle防止"Invalid ClientHandle"
if (mqttAndroidClient != null) {
try {
mqttAndroidClient.close();
} catch (Exception e) {
LogUtil.log(TAG, "关闭旧MQTT客户端异常:" + e);
}
}
// 使用固定的clientId前缀 + 时间戳避免每次随机导致Service端残留多个无效handle
String clientId = "ams_" + System.currentTimeMillis();
mqttAndroidClient = new MqttAndroidClient(ApronApp.Companion.getApplication(), AMSConfig.getInstance().getMqttServerUri(), clientId);
mMqttConnectOptions = new MqttConnectOptions(); mMqttConnectOptions = new MqttConnectOptions();
mMqttConnectOptions.setAutomaticReconnect(true); mMqttConnectOptions.setAutomaticReconnect(true);
mMqttConnectOptions.setMaxInflight(1000);// 避免消息积压导致连接拥塞 mMqttConnectOptions.setMaxInflight(1000);// 避免消息积压导致连接拥塞

View File

@ -156,7 +156,7 @@ public class PsdkWidgetScheduler {
report.setData(data); report.setData(data);
String jsonPayload = new Gson().toJson(report); String jsonPayload = new Gson().toJson(report);
LogUtil.log(TAG, "PSDK widget JSON payload: " + jsonPayload); // LogUtil.log(TAG, "PSDK widget JSON payload: " + jsonPayload);
MqttMessage mqttMessage = new MqttMessage(jsonPayload.getBytes("UTF-8")); MqttMessage mqttMessage = new MqttMessage(jsonPayload.getBytes("UTF-8"));
mqttMessage.setQos(1); mqttMessage.setQos(1);
client.publish(AMSConfig.UP_UAV_EVENT, mqttMessage); client.publish(AMSConfig.UP_UAV_EVENT, mqttMessage);

View File

@ -268,6 +268,34 @@ public class ApriltagDetector {
} }
} }
// 联合 PnP tag 时使用所有角点一次求解
if (detections.size() >= 2) {
try {
int n = detections.size();
ApriltagDetection[] arr = new ApriltagDetection[n];
double[] sizes = new double[n];
double[] offsX = new double[n];
double[] offsY = new double[n];
for (int i = 0; i < n; i++) {
arr[i] = detections.get(i);
sizes[i] = getTagSize(arr[i].id);
double[] off = getTagOffset(arr[i].id);
offsX[i] = off[0];
offsY[i] = off[1];
}
ApriltagPose joint = ApriltagNative.estimate_joint_pose(
arr, sizes, offsX, offsY, fx, fy, cx, cy);
if (joint != null) {
result.setJointPose(joint);
}
} catch (Exception e) {
Log.w(TAG, "joint PnP failed: " + e.getMessage());
}
} else if (detections.size() == 1) {
ApriltagPose lp = result.getBestLandingPose();
if (lp != null) result.setJointPose(lp);
}
// 统计 // 统计
long elapsed = System.currentTimeMillis() - t0; long elapsed = System.currentTimeMillis() - t0;
synchronized (lock) { synchronized (lock) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -318,6 +318,12 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
/> />
<Button
android:id="@+id/btn_test3"
android:text="DJI急停"
android:layout_width="100dp"
android:layout_height="100dp"
/>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -315,6 +315,12 @@
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
/> />
<Button
android:id="@+id/btn_test3"
android:text="DJI急停"
android:layout_width="100dp"
android:layout_height="100dp"
/>
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>