diff --git a/app/src/main/java/com/aros/apron/activity/MainActivity.kt b/app/src/main/java/com/aros/apron/activity/MainActivity.kt index a44347c5..20cef3f3 100644 --- a/app/src/main/java/com/aros/apron/activity/MainActivity.kt +++ b/app/src/main/java/com/aros/apron/activity/MainActivity.kt @@ -171,6 +171,11 @@ open class MainActivity : BaseActivity() { fun updateVtxHeartbeat() { lastVtxFrameTime = System.currentTimeMillis() } + + @JvmStatic + fun setStartArucoType(type: Int) { + instance?.startArucoType = type + } } protected var primaryFpvWidget: FPVWidget? = null @@ -236,12 +241,13 @@ open class MainActivity : BaseActivity() { private var cameraManager: ICameraStreamManager = MediaDataCenter.getInstance().cameraStreamManager - private var startArucoType = 0 //1执行机库二维码识别 2执行备降点二维码识别 + private var startArucoType = 0 //1执行机库二维码识别 2执行备降点二维码识别 3扫到任何码触发刹停 private var dictionary: Dictionary? = null override fun useEventBus(): Boolean { return true } + override fun onResume() { super.onResume() @@ -500,13 +506,13 @@ open class MainActivity : BaseActivity() { secondaryFPVWidget?.post { secondaryFPVWidget?.performClick() } - LogUtil.log(TAG, "智能刷新:有云台,模拟点击 FPV Widget") + //LogUtil.log(TAG, "智能刷新:有云台,模拟点击 FPV Widget") } else { // 没有云台:直接刷新当前流 primaryFpvWidget?.post { refreshVideoStream() } - LogUtil.log(TAG, "智能刷新:无云台,直接刷新主视频流") + //LogUtil.log(TAG, "智能刷新:无云台,直接刷新主视频流") } } @@ -793,8 +799,9 @@ open class MainActivity : BaseActivity() { enableStream() initFpvStream() startVtxHeartbeat() - SpeakerManager.getInstance().initMegaphoneInfo() + SpeakerManager.getInstance().addMegaphoneInfoListener() GimbalManager.getInstance().setmode() + PayloadWidgetManager.getInstance().initPayloadInfo() LogUtil.log(TAG, "自定义推流方式:" + PreferenceUtils.getInstance().customStreamType) @@ -834,9 +841,10 @@ open class MainActivity : BaseActivity() { enableStream() initMixedStream() startVtxHeartbeat() - SpeakerManager.getInstance().initMegaphoneInfo() + SpeakerManager.getInstance().addMegaphoneInfoListener() GeoidManager.getInstance().init(this) GimbalManager.getInstance().setmode() + PayloadWidgetManager.getInstance().initPayloadInfo() LogUtil.log(TAG, "自定义推流方式:" + PreferenceUtils.getInstance().customStreamType) @@ -876,9 +884,11 @@ open class MainActivity : BaseActivity() { enableStream() initCameraStream() startVtxHeartbeat() - SpeakerManager.getInstance().initMegaphoneInfo() + + SpeakerManager.getInstance().addMegaphoneInfoListener() GeoidManager.getInstance().init(this) GimbalManager.getInstance().setmode() + PayloadWidgetManager.getInstance().initPayloadInfo() LogUtil.log(TAG, "自定义推流方式:" + PreferenceUtils.getInstance().customStreamType) @@ -905,6 +915,8 @@ open class MainActivity : BaseActivity() { //val mixedLanding = MixedVisionLanding.getInstance() // 为 PORT_1(云台相机)添加帧监听器 + + cameraManager.addFrameListener( ComponentIndexType.PORT_1, ICameraStreamManager.FrameFormat.YUV420_888 @@ -933,6 +945,13 @@ open class MainActivity : BaseActivity() { frameData, dictionary ) + } else if (startArucoType == 3) { + Aprongim.getInstance()?.detectForceTriggerTags( + height, + width, + frameData, + dictionary + ) } } finally { Synchronizedstatus.setIsruningframe(false) @@ -972,6 +991,13 @@ open class MainActivity : BaseActivity() { frameData, dictionary ) + } else if (startArucoType == 3) { + Aprondown.getInstance()?.detectForceTriggerTags( + height, + width, + frameData, + dictionary + ) } } finally { Synchronizedstatus.setIsruningframe(false) @@ -1013,6 +1039,13 @@ open class MainActivity : BaseActivity() { frameData, dictionary ) + } else if (startArucoType == 3) { + ApronArucoDetect.getInstance()?.detectForceTriggerTags( + height, + width, + frameData, + dictionary + ) } } finally { Synchronizedstatus.setIsruningframe(false) @@ -1023,6 +1056,9 @@ open class MainActivity : BaseActivity() { } } + + + @SuppressLint("SuspiciousIndentation") private fun initCameraStream() { cameraManager.addFrameListener( @@ -1053,6 +1089,13 @@ open class MainActivity : BaseActivity() { frameData, dictionary ) + } else if (startArucoType == 3) { + ApronArucoDetectPort.getInstance()?.detectForceTriggerTags( + height, + width, + frameData, + dictionary + ) } } finally { Synchronizedstatus.setIsruningframe(false) diff --git a/app/src/main/java/com/aros/apron/base/BaseManager.java b/app/src/main/java/com/aros/apron/base/BaseManager.java index b0ff29be..f6df38f1 100644 --- a/app/src/main/java/com/aros/apron/base/BaseManager.java +++ b/app/src/main/java/com/aros/apron/base/BaseManager.java @@ -30,6 +30,7 @@ import com.google.gson.Gson; import org.eclipse.paho.android.service.MqttAndroidClient; import org.eclipse.paho.client.mqttv3.MqttMessage; +import java.util.List; import java.util.Locale; import java.util.UUID; @@ -201,6 +202,37 @@ public abstract class BaseManager { } } + /** + * 发送飞往备降点event事件 + */ + public void sendFlyToAlternateLandPointEvent() { + try { + if (MqttManager.getInstance().mqttAndroidClient.isConnected()) { + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setBid(UUID.randomUUID().toString()); + messageEvent.setTid(UUID.randomUUID().toString()); + messageEvent.setTimestamp(System.currentTimeMillis()); + messageEvent.setMethod(Constant.FLY_TO_ALTERNATE_LAND_POINT); + MessageEvent.Data data = new MessageEvent.Data(); + if (TextUtils.isEmpty(PreferenceUtils.getInstance().getFlightId())) { + data.setFlight_id("null"); + } else { + data.setFlight_id(PreferenceUtils.getInstance().getFlightId()); + } + messageEvent.setData(data); + MqttMessage mqttMessage = new MqttMessage(new Gson().toJson(messageEvent).getBytes("UTF-8")); + mqttMessage.setQos(0); + MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_EVENT, mqttMessage); + LogUtil.log(TAG, "发送fly_to_alternate_land_point event成功:" + new Gson().toJson(messageEvent)); + } else { + LogUtil.log(TAG, "发送fly_to_alternate_land_point event失败:mqtt 未连接"); + } + } catch (Exception e) { + e.printStackTrace(); + LogUtil.log(TAG, "发送fly_to_alternate_land_point event异常:" + e.toString()); + } + } + /** * 发送媒体文件上传事件 */ diff --git a/app/src/main/java/com/aros/apron/callback/MqttActionCallBack.java b/app/src/main/java/com/aros/apron/callback/MqttActionCallBack.java index a4ef9410..3a4d5765 100644 --- a/app/src/main/java/com/aros/apron/callback/MqttActionCallBack.java +++ b/app/src/main/java/com/aros/apron/callback/MqttActionCallBack.java @@ -1,6 +1,7 @@ package com.aros.apron.callback; import android.os.Handler; +import android.os.Looper; import com.aros.apron.constant.AMSConfig; import com.aros.apron.tools.LogUtil; @@ -17,6 +18,11 @@ public class MqttActionCallBack implements IMqttActionListener { private final String TAG = "MqttActionCallBack"; private MqttAndroidClient mqttAndroidClient; private MqttConnectOptions options; + private int reconnectAttempt = 0; + private static final int INITIAL_RETRY_INTERVAL = 3000; // 初始重试间隔 3 秒 + private static final int MAX_RETRY_INTERVAL = 30000; // 最大重试间隔 30 秒 + private static final Handler retryHandler = new Handler(Looper.getMainLooper()); + private Runnable retryRunnable; public MqttActionCallBack(MqttAndroidClient mqttAndroidClient, MqttConnectOptions options) { this.mqttAndroidClient = mqttAndroidClient; @@ -26,10 +32,14 @@ public class MqttActionCallBack implements IMqttActionListener { @Override public void onSuccess(IMqttToken asyncActionToken) { LogUtil.log(TAG, "MQtt连接成功:-------"); - // 异步显示Toast,避免Binder IPC阻塞当前主线程消息导致ANR - new Handler().post(() -> ToastUtil.showToast("MQtt连接成功")); + reconnectAttempt = 0; + if (retryRunnable != null) { + retryHandler.removeCallbacks(retryRunnable); + retryRunnable = null; + } + new Handler(Looper.getMainLooper()).post(() -> ToastUtil.showToast("MQtt连接成功")); try { - mqttAndroidClient.subscribe(AMSConfig.DOWN_UAV_SERVICES, 1);//订阅主题:注册 + mqttAndroidClient.subscribe(AMSConfig.DOWN_UAV_SERVICES, 1); } catch (MqttException e) { e.printStackTrace(); } @@ -39,19 +49,32 @@ public class MqttActionCallBack implements IMqttActionListener { public void onFailure(IMqttToken asyncActionToken, Throwable exception) { LogUtil.log(TAG, "MQtt连接失败:" + exception.toString()); try { - if (!mqttAndroidClient.isConnected()){ - new Handler().postDelayed(new Runnable() { + if (!mqttAndroidClient.isConnected()) { + reconnectAttempt++; + + // 指数退避:3s → 6s → 12s → 24s → 30s(封顶) + long retryDelay = Math.min(INITIAL_RETRY_INTERVAL * (1L << Math.min(reconnectAttempt, 4)), MAX_RETRY_INTERVAL); + + if (reconnectAttempt % 10 == 0 || reconnectAttempt <= 3) { + LogUtil.log(TAG, "第 " + reconnectAttempt + " 次重连,间隔 " + retryDelay + "ms"); + } + + if (retryRunnable != null) { + retryHandler.removeCallbacks(retryRunnable); + } + + final long finalDelay = retryDelay; + retryRunnable = new Runnable() { @Override public void run() { try { - mqttAndroidClient.connect(options, null, MqttActionCallBack.this); // 再次尝试连接 + mqttAndroidClient.connect(options, null, MqttActionCallBack.this); } catch (MqttException e) { - LogUtil.log(TAG,"mqtt重连异常:"+e.toString()); - e.printStackTrace(); + LogUtil.log(TAG, "mqtt重连异常:" + e.toString()); } - } - },1500); + }; + retryHandler.postDelayed(retryRunnable, finalDelay); } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/com/aros/apron/callback/MqttCallBack.java b/app/src/main/java/com/aros/apron/callback/MqttCallBack.java index 68495102..ecd9436a 100644 --- a/app/src/main/java/com/aros/apron/callback/MqttCallBack.java +++ b/app/src/main/java/com/aros/apron/callback/MqttCallBack.java @@ -208,8 +208,10 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.FLIGHTTASK_RECOVERY: LogUtil.log(TAG, "收到:航线继续" + jsonString); + clearVirtualStickHeartbeat(); MissionV3Manager.getInstance().resumeMission(message); + break; case Constant.RETURN_HOME: LogUtil.log(TAG, "收到:返航" + jsonString); @@ -428,12 +430,16 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.IR_METERING_MODE_SET: LogUtil.log(TAG, "收到:负载控制—红外测温模式设置" + jsonString); + CameraManager.getInstance().setThermalTemperatureMeasureMode(message); break; case Constant.IR_METERING_POINT_SET: LogUtil.log(TAG, "收到:负载控制—红外测温点设置" + jsonString); + + CameraManager.getInstance().setThermalSpotMetersurePoint(message); break; case Constant.IR_METERING_AREA_SET: LogUtil.log(TAG, "收到:负载控制—红外测温区域设置" + jsonString); + CameraManager.getInstance().setThermalRegionMetersureArea(message); break; case Constant.POI_MODE_ENTER: LogUtil.log(TAG, "收到:飞行控制—进入 POI 环绕模式" + jsonString); @@ -456,8 +462,12 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.SPEAKER_AUDIO_PLAY_START: LogUtil.log(TAG, "收到:喊话器-开始播放音频" + jsonString); + Movement.getInstance().setMd5(message.getData().getFile().getMd5()); + Movement.getInstance().setPlay_file_md5(message.getData().getFile().getName()); + Movement.getInstance().setStep_key("download"); + SpeakerProgressReporter.getInstance().startAudioReport(2); - SpeakerManager.getInstance().setindex(); + //SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerAudioPlayStart(message); @@ -470,8 +480,12 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.SPEAKER_TTS_PLAY_START: LogUtil.log(TAG, "收到:喊话器-开始播放TTS文本" + jsonString); + Movement.getInstance().setMd5(message.getData().getTts().getMd5()); + Movement.getInstance().setPlay_file_md5(message.getData().getTts().getMd5()); + Movement.getInstance().setPlay_file_name(message.getData().getTts().getText()); + SpeakerProgressReporter.getInstance().startTTSReport(2); - SpeakerManager.getInstance().setindex(); + //SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerTTSPlayStart(message, 0); @@ -484,7 +498,7 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.SPEAKER_REPLAY: LogUtil.log(TAG, "收到:喊话器-重新播放" + jsonString); - SpeakerManager.getInstance().setindex(); + // SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerReply(message); @@ -497,7 +511,7 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.SPEAKER_PLAY_STOP: LogUtil.log(TAG, "收到:喊话器-停止播放" + jsonString); - SpeakerManager.getInstance().setindex(); + // SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerStop(message); @@ -511,7 +525,7 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.SPEAKER_PLAY_MODE_SET: LogUtil.log(TAG, "收到:喊话器-设置播放模式" + jsonString); - SpeakerManager.getInstance().setindex(); + // SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerPlayModeSet(message); @@ -524,7 +538,7 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.SPEAKER_PLAY_VOLUME_SET: LogUtil.log(TAG, "收到:喊话器-设置音量" + jsonString); - SpeakerManager.getInstance().setindex(); + //SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerPlayVolumeSet(message); @@ -537,7 +551,7 @@ public class MqttCallBack extends BaseManager implements MqttCallbackExtended { break; case Constant.DRC_SPEAKER_TTS_SET: LogUtil.log(TAG, "收到:喊话器-TTS喊话设置" + jsonString); - SpeakerManager.getInstance().setindex(); + //SpeakerManager.getInstance().setindex(); synchronized (lock) { if (!Synchronizedstatus.isSpeakrunning()) { SpeakerManager.getInstance().speakerTTSPlayStart(message, 1); diff --git a/app/src/main/java/com/aros/apron/constant/Constant.java b/app/src/main/java/com/aros/apron/constant/Constant.java index 7482d847..205f3eb6 100644 --- a/app/src/main/java/com/aros/apron/constant/Constant.java +++ b/app/src/main/java/com/aros/apron/constant/Constant.java @@ -322,6 +322,11 @@ public class Constant { */ public static final String DRC_SPEAKER_TTS_SET="drc_speaker_tts_set"; + /** + * 飞向备降点 + */ + public static final String FLY_TO_ALTERNATE_LAND_POINT="fly_to_alternate_land_point"; + /** * 推流切换fpv */ diff --git a/app/src/main/java/com/aros/apron/entity/MessageDown.java b/app/src/main/java/com/aros/apron/entity/MessageDown.java index 3d1f18af..38488b2d 100644 --- a/app/src/main/java/com/aros/apron/entity/MessageDown.java +++ b/app/src/main/java/com/aros/apron/entity/MessageDown.java @@ -1,5 +1,6 @@ package com.aros.apron.entity; +import java.security.PrivateKey; import java.util.List; public class MessageDown { @@ -111,6 +112,8 @@ public class MessageDown { private float width; + + private int psdk_index; private int play_mode; private int play_volume; @@ -132,6 +135,10 @@ public class MessageDown { // saved: 是否保存 0-否 1-是 (bool) private boolean saved; + + + + public int getGroup() { return group; } @@ -628,6 +635,33 @@ public class MessageDown { public static class File { private String fingerprint; private String url; + private String md5; + private String name; + private String format; + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } public String getFingerprint() { return fingerprint; diff --git a/app/src/main/java/com/aros/apron/entity/Movement.java b/app/src/main/java/com/aros/apron/entity/Movement.java index b5a794c0..045d76e6 100644 --- a/app/src/main/java/com/aros/apron/entity/Movement.java +++ b/app/src/main/java/com/aros/apron/entity/Movement.java @@ -3,10 +3,8 @@ package com.aros.apron.entity; import java.util.List; + public class Movement { - - - private static class MovementHolder { private static final Movement INSTANCE = new Movement(); } @@ -57,7 +55,7 @@ public class Movement { private int landingPower;//降落电量所需百分比 private int lowBatteryRTHState;//智能低电量返航状态 0未触发智能低电量返航 1触发智能低电量返航,飞行器正在倒计时 2执行智能低电量返航 3智能低电量返航被取消 private String missionName;//当前正在执行的航线名 - private int currentWaypointIndex=0;//当前航点下标 + private int currentWaypointIndex = 0;//当前航点下标 private boolean planeWing;//飞机是否在飞 private boolean isMotorsOn;//电机是否起转 @@ -111,12 +109,12 @@ public class Movement { private boolean resumeMissionStatus;//前端判断是否显示/隐藏继续航线 private boolean isVirtualStickQuitMission;//用户手动后退出航线 - private boolean virtualcontrollget=false;//是否已经获得过控制权 + private boolean virtualcontrollget = false;//是否已经获得过控制权 private int GPSSatelliteCount; //卫星个数 - private boolean Alternate=false; + private boolean Alternate = false; - private boolean camera3stick=false;//3号相机模式释放控制权 + private boolean camera3stick = false;//3号相机模式释放控制权 //适配上云格式参数,拿到后再进行组装 @@ -197,12 +195,12 @@ public class Movement { private int wind_direction; private int wind_speed; - private int camera_mode=0; + private int camera_mode = 0; private int ir_metering_mode; private int temperature; private double x; private double y; - private int ir_zoom_factor=2; + private int ir_zoom_factor = 2; private double bottom; private double left; private double right; @@ -212,7 +210,7 @@ public class Movement { private int record_time; private int recording_state; private int remain_photo_num; - private int remain_record_duration=3000; + private int remain_record_duration = 3000; private boolean screen_split_enable; private int wide_exposure_mode; private int wide_exposure_value; @@ -222,7 +220,7 @@ public class Movement { private int zoom_calibrate_nearest_focus_value; private int zoom_exposure_mode; private int zoom_exposure_value; - private double zoom_factor=2; + private double zoom_factor = 2; private int zoom_focus_mode; private int zoom_focus_state; private int zoom_focus_value; @@ -278,9 +276,9 @@ public class Movement { private String gnd_quality_4g; private String sdr_freq_band; private String freq_band_4g; - private boolean istakeoffex=false; + private boolean istakeoffex = false; - private boolean waylineinterpter=true; + private boolean waylineinterpter = true; public boolean isWaylineinterpter() { @@ -318,12 +316,12 @@ public class Movement { } //航线上报状态 - private int result=0; + private int result = 0; // ========== 一键起飞相关字段 ========== private boolean takeofftopointmission; - private volatile String takeoff_status="task_ready"; // task_ready/wayline_progress/wayline_ok/wayline_failed/wayline_cancel/task_finish + private volatile String takeoff_status = "task_ready"; // task_ready/wayline_progress/wayline_ok/wayline_failed/wayline_cancel/task_finish private int takeoff_result; // 0成功,非0错误 private float takeoff_remaining_distance; // 剩余距离(米) private float takeoff_remaining_time; // 剩余时间(秒) @@ -334,7 +332,7 @@ public class Movement { private double takeofftargetlongitude; private double takeofftargetheight; - private int mission_type; + private int mission_type; public int getMission_type() { return mission_type; @@ -355,6 +353,163 @@ public class Movement { private String step_key; private String md5; + //==================================psdk上报 + + private String psdk_index; + private String psdk_lib_version; + private String psdk_name="Speaker"; + private String psdk_sn="8V2CP1X00A1ZPA"; + private String psdk_type="5"; + private String psdk_version="01.00.01.11"; + private String play_file_md5; + private String play_file_name; + private String play_mode; + private String play_volume; + private String system_state; + private String work_mode; + private int[] values; + + private String tts_language; + private String tts_speed; + private String tts_type; + private String tts_volume; + + + public String getTts_language() { + return tts_language; + } + + public void setTts_language(String tts_language) { + this.tts_language = tts_language; + } + + public String getTts_speed() { + return tts_speed; + } + + public void setTts_speed(String tts_speed) { + this.tts_speed = tts_speed; + } + + public String getTts_type() { + return tts_type; + } + + public void setTts_type(String tts_type) { + this.tts_type = tts_type; + } + + public String getTts_volume() { + return tts_volume; + } + + public void setTts_volume(String tts_volume) { + this.tts_volume = tts_volume; + } + + public String getPsdk_index() { + return psdk_index; + } + + public void setPsdk_index(String psdk_index) { + this.psdk_index = psdk_index; + } + + public String getPsdk_lib_version() { + return psdk_lib_version; + } + + public void setPsdk_lib_version(String psdk_lib_version) { + this.psdk_lib_version = psdk_lib_version; + } + + public String getPsdk_name() { + return psdk_name; + } + + public void setPsdk_name(String psdk_name) { + this.psdk_name = psdk_name; + } + + public String getPsdk_sn() { + return psdk_sn; + } + + public void setPsdk_sn(String psdk_sn) { + this.psdk_sn = psdk_sn; + } + + public String getPsdk_type() { + return psdk_type; + } + + public void setPsdk_type(String psdk_type) { + this.psdk_type = psdk_type; + } + + public String getPsdk_version() { + return psdk_version; + } + + public void setPsdk_version(String psdk_version) { + this.psdk_version = psdk_version; + } + + public String getPlay_file_md5() { + return play_file_md5; + } + + public void setPlay_file_md5(String play_file_md5) { + this.play_file_md5 = play_file_md5; + } + + public String getPlay_file_name() { + return play_file_name; + } + + public void setPlay_file_name(String play_file_name) { + this.play_file_name = play_file_name; + } + + public String getPlay_mode() { + return play_mode; + } + + public void setPlay_mode(String play_mode) { + this.play_mode = play_mode; + } + + public String getPlay_volume() { + return play_volume; + } + + public void setPlay_volume(String play_volume) { + this.play_volume = play_volume; + } + + public String getSystem_state() { + return system_state; + } + + public void setSystem_state(String system_state) { + this.system_state = system_state; + } + + public String getWork_mode() { + return work_mode; + } + + public void setWork_mode(String work_mode) { + this.work_mode = work_mode; + } + + public int[] getValues() { + return values; + } + + public void setValues(int[] values) { + this.values = values; + } public boolean isCamera3stick() { return camera3stick; @@ -413,7 +568,6 @@ public class Movement { } - public String getTTS_status() { return TTS_status; } @@ -423,7 +577,6 @@ public class Movement { } - public String getStep_key() { return step_key; } @@ -433,7 +586,6 @@ public class Movement { } - public int getGPSSatelliteCount() { return GPSSatelliteCount; } @@ -445,6 +597,7 @@ public class Movement { public double getSpeed() { return speed; } + public void setSpeed(double speed) { this.speed = speed; } @@ -508,7 +661,6 @@ public class Movement { } - public float getTakeoff_remaining_distance() { return takeoff_remaining_distance; } @@ -550,9 +702,9 @@ public class Movement { } //用来判断是什么起飞type - private volatile int flightmode=0; //0代表没有操作 1代表航线飞行 2一键代表指令飞行 3flyto(一键和flyto) + private volatile int flightmode = 0; //0代表没有操作 1代表航线飞行 2一键代表指令飞行 3flyto(一键和flyto) private boolean takeofftopoint; - private boolean opendrc=false; //true 是开启 false 是关闭 + private boolean opendrc = false; //true 是开启 false 是关闭 public boolean isTakeofftopoint() { return takeofftopoint; @@ -1743,7 +1895,6 @@ public class Movement { } - public String getFirmware_version() { return firmware_version; } @@ -2182,7 +2333,6 @@ public class Movement { } - public boolean isPlaneWing() { return planeWing; } @@ -2366,7 +2516,6 @@ public class Movement { } - public String getFlightPathName() { return flightPathName; } diff --git a/app/src/main/java/com/aros/apron/entity/Osd.java b/app/src/main/java/com/aros/apron/entity/Osd.java index cede74b8..840bd0d7 100644 --- a/app/src/main/java/com/aros/apron/entity/Osd.java +++ b/app/src/main/java/com/aros/apron/entity/Osd.java @@ -911,6 +911,7 @@ public class Osd { private int camera_mode; private int ir_metering_mode; private IrMeteringPoint ir_metering_point; + private IrMeteringArea ir_metering_area; private int ir_zoom_factor; private LiveviewWorldRegion liveview_world_region; private String payload_index; @@ -962,6 +963,14 @@ public class Osd { this.ir_metering_point = ir_metering_point; } + public IrMeteringArea getIr_metering_area() { + return ir_metering_area; + } + + public void setIr_metering_area(IrMeteringArea ir_metering_area) { + this.ir_metering_area = ir_metering_area; + } + public int getIr_zoom_factor() { return ir_zoom_factor; } @@ -1200,6 +1209,102 @@ public class Osd { } } + public static class IrMeteringArea { + private double x; + private double y; + private double width; + private double height; + private double aver_temperature; + private IrTemperaturePoint min_temperature_point; + private IrTemperaturePoint max_temperature_point; + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + } + + public double getAver_temperature() { + return aver_temperature; + } + + public void setAver_temperature(double aver_temperature) { + this.aver_temperature = aver_temperature; + } + + public IrTemperaturePoint getMin_temperature_point() { + return min_temperature_point; + } + + public void setMin_temperature_point(IrTemperaturePoint min_temperature_point) { + this.min_temperature_point = min_temperature_point; + } + + public IrTemperaturePoint getMax_temperature_point() { + return max_temperature_point; + } + + public void setMax_temperature_point(IrTemperaturePoint max_temperature_point) { + this.max_temperature_point = max_temperature_point; + } + } + + public static class IrTemperaturePoint { + private double x; + private double y; + private double temperature; + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getTemperature() { + return temperature; + } + + public void setTemperature(double temperature) { + this.temperature = temperature; + } + } + public static class LiveviewWorldRegion { private double bottom; private double left; diff --git a/app/src/main/java/com/aros/apron/manager/AlternateLandingManager.java b/app/src/main/java/com/aros/apron/manager/AlternateLandingManager.java index 1793bbd8..c2575fc3 100644 --- a/app/src/main/java/com/aros/apron/manager/AlternateLandingManager.java +++ b/app/src/main/java/com/aros/apron/manager/AlternateLandingManager.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.aros.apron.base.BaseManager; +import com.aros.apron.constant.Constant; import com.aros.apron.entity.FlightMission; import com.aros.apron.entity.MessageDown; import com.aros.apron.entity.MissionPoint; @@ -309,6 +310,13 @@ public class AlternateLandingManager extends BaseManager { PreferenceUtils.getInstance().setNeedTriggerAlterArucoLand(false); PreferenceUtils.getInstance().setNeedTriggerApronArucoLand(false); sendEvent2Server( "开始飞往备降点",1); + + + sendFlyToAlternateLandPointEvent(); + + Movement.getInstance().setTakeoff_result(316052); + Movement.getInstance().setResult(316052); + //设置为未开始识别二维码状态 FlightManager.getInstance().setSendDetect(false); EventBus.getDefault().post(FLAG_STOP_ARUCO); diff --git a/app/src/main/java/com/aros/apron/manager/BatteryManager.java b/app/src/main/java/com/aros/apron/manager/BatteryManager.java index 36ce3ff2..fa6f1999 100644 --- a/app/src/main/java/com/aros/apron/manager/BatteryManager.java +++ b/app/src/main/java/com/aros/apron/manager/BatteryManager.java @@ -3,15 +3,24 @@ package com.aros.apron.manager; import static dji.sdk.keyvalue.key.KeyTools.createKey; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.aros.apron.base.BaseManager; import com.aros.apron.entity.Movement; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; + +import com.aros.apron.tools.LogUtil; +import com.aros.apron.tools.PreferenceUtils; + import dji.sdk.keyvalue.key.BatteryKey; import dji.sdk.keyvalue.key.FlightControllerKey; import dji.sdk.keyvalue.key.KeyTools; import dji.sdk.keyvalue.value.battery.IndustryBatteryType; +import dji.sdk.keyvalue.value.common.EmptyMsg; import dji.sdk.keyvalue.value.flightcontroller.LowBatteryRTHInfo; import dji.v5.common.callback.CommonCallbacks; import dji.v5.manager.KeyManager; @@ -33,10 +42,13 @@ public class BatteryManager extends BaseManager { } private boolean sendLowBatteryRTHPosition2Server; + //标识是否已触发强制返航,避免重复触发 + private boolean forcedRTHTriggered; private static final long SECONDS_PER_DAY = 24 * 60 * 60; // 86400 + public void initBatteryInfo() { Boolean isConnect = KeyManager.getInstance().getValue(createKey(FlightControllerKey.KeyConnection)); if (isConnect != null && isConnect) { @@ -80,6 +92,7 @@ public class BatteryManager extends BaseManager { if (newValue != null) { Movement.getInstance().setBattery_a_capacity_percent(newValue); Movement.getInstance().setCapacity_percent(newValue); + checkForcedBatteryRTH(newValue, Movement.getInstance().getBattery_b_capacity_percent()); } } }); @@ -90,6 +103,7 @@ public class BatteryManager extends BaseManager { public void onValueChange(@Nullable Integer oldValue, @Nullable Integer newValue) { if (newValue != null) { Movement.getInstance().setBattery_b_capacity_percent(newValue); + checkForcedBatteryRTH(Movement.getInstance().getBattery_a_capacity_percent(), newValue); } } }); @@ -273,6 +287,64 @@ public class BatteryManager extends BaseManager { + /** + * 检查双电池平均电量是否低于强制返航阈值,低于则触发返航 + */ + private void checkForcedBatteryRTH(int batteryA, int batteryB) { + + + if (forcedRTHTriggered) return; + + if (!getGimbalAndCameraEnabled()) { + return; + } + + + // 过滤空值/无效电量(0或负数不参与计算) + if (batteryA <= 0 || batteryB <= 0) return; + + String forcedBatteryStr = PreferenceUtils.getInstance().getForcedBattery(); + if (TextUtils.isEmpty(forcedBatteryStr)) return; + + int forcedBattery; + try { + forcedBattery = Integer.parseInt(forcedBatteryStr); + } catch (NumberFormatException e) { + return; + } + + int avgBattery = (batteryA + batteryB) / 2; + if (avgBattery >= forcedBattery) return; + + // 仅在飞行中触发 + if (!Movement.getInstance().isPlaneWing()) return; + + forcedRTHTriggered = true; + LogUtil.log(TAG, "平均电量" + avgBattery + "%低于强制返航阈值" + forcedBattery + "%,触发自动返航"); + sendEvent2Server("触发低电量返航",1); + + Movement.getInstance().setTakeoff_result(321773); + Movement.getInstance().setResult(321773); + + new Handler(Looper.getMainLooper()).postDelayed(() -> { + dji.v5.manager.KeyManager.getInstance().performAction( + dji.sdk.keyvalue.key.KeyTools.createKey(dji.sdk.keyvalue.key.FlightControllerKey.KeyStartGoHome), + new CommonCallbacks.CompletionCallbackWithParam() { + @Override + public void onSuccess(EmptyMsg emptyMsg) { + LogUtil.log(TAG, "强制返航触发成功"); + } + + @Override + public void onFailure(@NonNull dji.v5.common.error.IDJIError error) { + LogUtil.log(TAG, "强制返航触发失败: " + error); + } + } + ); + }, 1000); + } + + public void releaseBatteryKey() { KeyManager.getInstance().cancelListen(this); } diff --git a/app/src/main/java/com/aros/apron/manager/CameraManager.java b/app/src/main/java/com/aros/apron/manager/CameraManager.java index fade1a4d..fa9c581c 100644 --- a/app/src/main/java/com/aros/apron/manager/CameraManager.java +++ b/app/src/main/java/com/aros/apron/manager/CameraManager.java @@ -52,6 +52,7 @@ import dji.sdk.keyvalue.value.camera.ZoomTargetPointInfo; import dji.sdk.keyvalue.value.common.CameraLensType; import dji.sdk.keyvalue.value.common.ComponentIndexType; import dji.sdk.keyvalue.value.common.DoublePoint2D; +import dji.sdk.keyvalue.value.common.DoubleRect; import dji.sdk.keyvalue.value.common.EmptyMsg; import dji.sdk.keyvalue.value.product.ProductType; import dji.v5.common.callback.CommonCallbacks; @@ -313,11 +314,13 @@ public class CameraManager extends BaseManager { @Override public void onValueChange(@Nullable ThermalTemperatureMeasureMode thermalTemperatureMeasureMode, @Nullable ThermalTemperatureMeasureMode t1) { if (t1 != null && thermalTemperatureMeasureMode != null) { - Movement.getInstance().setIr_metering_mode(thermalTemperatureMeasureMode.value()); + if(t1!=ThermalTemperatureMeasureMode.CENTRAL_POINT_METERING&&t1!=ThermalTemperatureMeasureMode.UNKNOWN){ + Movement.getInstance().setIr_metering_mode(t1.value()); + } + } } }); - //当前测温的点的位置 KeyManager.getInstance().listen(KeyTools.createCameraKey(CameraKey.KeyThermalSpotMetersurePoint, ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), this, new CommonCallbacks.KeyListener() { @@ -1791,85 +1794,85 @@ public class CameraManager extends BaseManager { // } // } -// //设置测温模式 -// public void setThermalTemperatureMeasureMode(MQMessage message) { -// Boolean isConnect =KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey. -// KeyConnection,ComponentIndexType.PORT_1)); -// if (isConnect != null && isConnect && getGimbalAndCameraEnabled()) { -// KeyManager.getInstance().setValue(KeyTools.createCameraKey(CameraKey.KeyThermalTemperatureMeasureMode, -// ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), -// ThermalTemperatureMeasureMode.find(message.getThermalTemperatureMeasureMode()), new CommonCallbacks.CompletionCallback() { -// @Override -// public void onSuccess() { -// sendMsg2Server(message); -// } + //设置测温模式 + public void setThermalTemperatureMeasureMode(MessageDown message) { + Boolean isConnect =KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey. + KeyConnection,ComponentIndexType.PORT_1)); + if (isConnect != null && isConnect && getGimbalAndCameraEnabled()) { + KeyManager.getInstance().setValue(KeyTools.createCameraKey(CameraKey.KeyThermalTemperatureMeasureMode, + ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), + ThermalTemperatureMeasureMode.find(message.getData().getMode()), new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG,"设置测温模式:"+new Gson().toJson(error)); + sendFailMsg2Server(message, "设置测温模式:" + getIDJIErrorMsg(error)); + } + }); + } else { + LogUtil.log(TAG, "设置测温模式失败:当前状态相机禁止操作"); + } + } // -// @Override -// public void onFailure(@NonNull IDJIError error) { -// LogUtil.log(TAG,"设置测温模式:"+new Gson().toJson(error)); -// sendMsg2Server(message, "设置测温模式:" + getIDJIErrorMsg(error)); -// } -// }); -// } else { -// LogUtil.log(TAG, "设置测温模式失败:当前状态相机禁止操作"); -// } -// } + //设置需要测温的点的位置 + public void setThermalSpotMetersurePoint(MessageDown message) { + Boolean isConnect =KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey. + KeyConnection,ComponentIndexType.PORT_1)); + if (isConnect != null && isConnect && getGimbalAndCameraEnabled()) { + DoublePoint2D doublePoint2D = new DoublePoint2D(); + doublePoint2D.setX(Double.parseDouble(message.getData().getX())); + doublePoint2D.setY(Double.parseDouble(message.getData().getY())); + KeyManager.getInstance().setValue(KeyTools.createCameraKey(CameraKey.KeyThermalSpotMetersurePoint, + ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), doublePoint2D, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG,"设置点测温失败:"+new Gson().toJson(error)); + sendFailMsg2Server(message, "设置点测温失败:" + getIDJIErrorMsg(error)); + + } + }); + + } else { + LogUtil.log(TAG, "测温点设置失败:当前状态相机禁止操作"); + } + } // -// //设置需要测温的点的位置 -// public void setThermalSpotMetersurePoint(MQMessage message) { -// Boolean isConnect =KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey. -// KeyConnection,ComponentIndexType.PORT_1)); -// if (isConnect != null && isConnect && getGimbalAndCameraEnabled()) { -// DoublePoint2D doublePoint2D = new DoublePoint2D(); -// doublePoint2D.setX(Double.parseDouble(message.getMetersurePointX())); -// doublePoint2D.setY(Double.parseDouble(message.getMetersurePointY())); -// KeyManager.getInstance().setValue(KeyTools.createCameraKey(CameraKey.KeyThermalSpotMetersurePoint, -// ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), doublePoint2D, new CommonCallbacks.CompletionCallback() { -// @Override -// public void onSuccess() { -// sendMsg2Server(message); -// } -// -// @Override -// public void onFailure(@NonNull IDJIError error) { -// LogUtil.log(TAG,"设置点测温失败:"+new Gson().toJson(error)); -// sendMsg2Server(message, "设置点测温失败:" + getIDJIErrorMsg(error)); -// -// } -// }); -// -// } else { -// LogUtil.log(TAG, "测温点设置失败:当前状态相机禁止操作"); -// } -// } -// -// //设置需要测温的区域位置 -// public void setThermalRegionMetersureArea(MQMessage message) { -// Boolean isConnect =KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey. -// KeyConnection,ComponentIndexType.PORT_1)); -// if (isConnect != null && isConnect) { -// DoubleRect doubleRect = new DoubleRect(); -// doubleRect.setX(Double.parseDouble(message.getMetersureAreaX())); -// doubleRect.setY(Double.parseDouble(message.getMetersureAreaY())); -// doubleRect.setHeight(Double.parseDouble(message.getMetersureAreaHeight())); -// doubleRect.setWidth(Double.parseDouble(message.getMetersureAreaWidth())); -// KeyManager.getInstance().setValue(KeyTools.createCameraKey(CameraKey.KeyThermalRegionMetersureArea, -// ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), doubleRect, new CommonCallbacks.CompletionCallback() { -// @Override -// public void onSuccess() { -// sendMsg2Server(message); -// } -// -// @Override -// public void onFailure(@NonNull IDJIError error) { -// LogUtil.log(TAG,"设置区域测温失败:"+new Gson().toJson(error)); -// sendMsg2Server(message, "设置区域测温失败:" + getIDJIErrorMsg(error)); -// } -// }); -// } else { -// LogUtil.log(TAG, "测温区域设置失败:当前状态相机禁止操作"); -// } -// } + //设置需要测温的区域位置 + public void setThermalRegionMetersureArea(MessageDown message) { + Boolean isConnect =KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey. + KeyConnection,ComponentIndexType.PORT_1)); + if (isConnect != null && isConnect) { + DoubleRect doubleRect = new DoubleRect(); + doubleRect.setX(Double.parseDouble(message.getData().getX())); + doubleRect.setY(Double.parseDouble(message.getData().getY())); + doubleRect.setHeight((double) message.getData().getHeight()); + doubleRect.setWidth((double) message.getData().getWidth()); + KeyManager.getInstance().setValue(KeyTools.createCameraKey(CameraKey.KeyThermalRegionMetersureArea, + ComponentIndexType.PORT_1, CameraLensType.CAMERA_LENS_THERMAL), doubleRect, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG,"设置区域测温失败:"+new Gson().toJson(error)); + sendFailMsg2Server(message, "设置区域测温失败:" + getIDJIErrorMsg(error)); + } + }); + } else { + LogUtil.log(TAG, "测温区域设置失败:当前状态相机禁止操作"); + } + } } diff --git a/app/src/main/java/com/aros/apron/manager/FlightManager.java b/app/src/main/java/com/aros/apron/manager/FlightManager.java index ed2ef74b..22cb4396 100644 --- a/app/src/main/java/com/aros/apron/manager/FlightManager.java +++ b/app/src/main/java/com/aros/apron/manager/FlightManager.java @@ -16,6 +16,7 @@ import com.aros.apron.constant.AMSConfig; import com.aros.apron.entity.ApronExecutionStatus; import com.aros.apron.entity.MessageDown; import com.aros.apron.entity.Movement; +import com.aros.apron.activity.MainActivity; import com.aros.apron.mix.Aprondown; import com.aros.apron.mix.Aprongim; import com.aros.apron.tools.AlternateArucoDetect; @@ -26,6 +27,7 @@ import com.aros.apron.tools.LocationUtils; import com.aros.apron.tools.LogUtil; import com.aros.apron.tools.MqttManager; import com.aros.apron.tools.PreferenceUtils; +import com.aros.apron.tools.PsdkWidgetScheduler; import com.aros.apron.tools.TakeoffProgressScheduler; import com.google.gson.Gson; @@ -36,6 +38,7 @@ import java.util.Objects; import dji.sdk.keyvalue.key.AirLinkKey; import dji.sdk.keyvalue.key.CameraKey; +import dji.sdk.keyvalue.key.FlightAssistantKey; import dji.sdk.keyvalue.key.FlightControllerKey; import dji.sdk.keyvalue.key.GimbalKey; import dji.sdk.keyvalue.key.KeyTools; @@ -51,6 +54,7 @@ import dji.sdk.keyvalue.value.common.EmptyMsg; import dji.sdk.keyvalue.value.common.LocationCoordinate2D; import dji.sdk.keyvalue.value.common.LocationCoordinate3D; import dji.sdk.keyvalue.value.common.Velocity3D; +import dji.sdk.keyvalue.value.flightassistant.AuxiliaryLightMode; import dji.sdk.keyvalue.value.flightassistant.VisionAssistDirection; import dji.sdk.keyvalue.value.flightcontroller.FailsafeAction; import dji.sdk.keyvalue.value.flightcontroller.FlightMode; @@ -120,8 +124,12 @@ public class FlightManager extends BaseManager { OSDManager.getInstance().pushFlightAttitude(); Movement.getInstance().setMissionFinish(true); + PsdkWidgetScheduler.getInstance().stop(); + } else { + PsdkWidgetScheduler.getInstance().start(); + PerceptionManager.getInstance().setPerceptionEnable(false); PerceptionManager.getInstance().setObstacleAvoidanceupEnabled(false); PerceptionManager.getInstance().setObstacleAvoidancedownEnabled(false); @@ -790,6 +798,8 @@ public class FlightManager extends BaseManager { private int goHomeExecutionState; //确保每次流程只发送触发降落一次,降落完成后设置为false,而不是最后开始landing时触发,如果不再landing时设置为false,那么在landing的途中,也可能再次触发landing private boolean isSendDetect; + // 强制触发ArUco检测标志(无视高度条件,与高度条件 || 关系) + public static boolean forceTriggerDetection; //飞机飞走后是否发送关舱门(确保只发送一次) private boolean sendCloseCabinDoorMsg; //飞机飞回后是否发送开舱门(确保只发送一次) @@ -904,16 +914,19 @@ public class FlightManager extends BaseManager { double distance = Movement.getInstance().getHome_distance(); double flyingHeight = Movement.getInstance().getElevation(); boolean isDebugMode = PreferenceUtils.getInstance().getIsDebugMode(); + boolean isDistanceAndHeightValid = distance < DISTANCE_THRESHOLD && flyingHeight > FLYING_HEIGHT_THRESHOLD && !sendOpenCabinDoorMsg; + + if (!PreferenceUtils.getInstance().getTriggerToAlternatePoint() && isReturningHome && isDistanceAndHeightValid && !isDebugMode) { LogUtil.log(TAG, "返航距离:" + distance + "---当前高度:" + flyingHeight); + sendOpenCabinDoorMsg = true; DockCloseManager.getInstance().stopRetry(); DockOpenManager.getInstance().sendDockOpenMsg2Server(); - } } @@ -921,9 +934,9 @@ public class FlightManager extends BaseManager { //降落时将云台朝下 private void gimbalDownwards() { if (goHomeExecutionState == GoHomeState.LANDING.value() - && Movement.getInstance().getElevation() > 11 && !isGimbalDownwards) { - - + && Movement.getInstance().getElevation() > 5 && !isGimbalDownwards) { + // 切换为强制扫码模式:扫到任何ArUco码就触发刹停 + MainActivity.setStartArucoType(3); MediaDataCenter.getInstance().getCameraStreamManager().setVisionAssistViewDirection(VisionAssistDirection.DOWN, new CommonCallbacks.CompletionCallback() { @Override @@ -941,6 +954,7 @@ public class FlightManager extends BaseManager { leDsSettings.setNavigationLEDsOn(false); leDsSettings.setStatusIndicatorLEDsOn(true); leDsSettings.setFrontLEDsOn(true); + KeyManager.getInstance().setValue(KeyTools.createKey(FlightControllerKey.KeyLEDsSettings), leDsSettings, new CommonCallbacks.CompletionCallback() { @Override public void onSuccess() { @@ -953,12 +967,28 @@ public class FlightManager extends BaseManager { } }); + KeyManager.getInstance().setValue(KeyTools.createKey(FlightAssistantKey.KeyBottomAuxiliaryLightMode), AuxiliaryLightMode.OFF, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + LogUtil.log(TAG,"下视辅助灯开启成功"); + } - DroneHelper.getInstance().setGimbalPitchDegree(); + @Override + public void onFailure(@NonNull IDJIError idjiError) { + LogUtil.log(TAG,"夜航灯关闭失败"); + } + }); + + //DroneHelper.getInstance().setGimbalPitchDegree(); //将镜头设置为自动对焦 - DroneHelper.getInstance().setCameraFocusMode(); - + //DroneHelper.getInstance().setCameraFocusMode(); + new android.os.Handler().postDelayed(() -> { + DroneHelper.getInstance().setGimbalPitchDegree(); + }, 500); + new android.os.Handler().postDelayed(() -> { + DroneHelper.getInstance().setCameraFocusMode(); + }, 500); new android.os.Handler().postDelayed(() -> { @@ -967,7 +997,6 @@ public class FlightManager extends BaseManager { new android.os.Handler().postDelayed(() -> { setCameraExposureMode(); }, 500); - new android.os.Handler().postDelayed(() -> { EventBus.getDefault().post("REFRESH_VIDEO_SOURCE"); }, 2000); @@ -986,13 +1015,21 @@ public class FlightManager extends BaseManager { isGimbalDownwards = true; + + //重复设置防止重复 + new android.os.Handler().postDelayed(() -> { + DroneHelper.getInstance().setGimbalPitchDegree(); + }, 1000); + PerceptionManager.getInstance().setPerceptionEnable(false); PerceptionManager.getInstance().setObstacleAvoidanceupEnabled(false); PerceptionManager.getInstance().setObstacleAvoidancedownEnabled(false); PerceptionManager.getInstance().closeRadarManager(false); + if (Movement.getInstance().getIsRecording() == 1) { CameraManager.getInstance().stopRecordVideo(null); } + } } @@ -1136,6 +1173,10 @@ public class FlightManager extends BaseManager { private static final double FLYING_HEIGHT_THRESHOLD_MIN = -2; private static final double FLYING_HEIGHT_THRESHOLD_MIN_ALTERNATE = 2.0; + // 打印相对高度的节流计数器(约每秒一次) + private int heightLogCounter = 0; + private static final int HEIGHT_LOG_INTERVAL = 30; // startVisionLanding大约每秒回调一次 + private void startVisionLanding() { boolean isDebugMode = PreferenceUtils.getInstance().getIsDebugMode(); @@ -1143,29 +1184,36 @@ public class FlightManager extends BaseManager { boolean needTriggerApronArucoLand = PreferenceUtils.getInstance().getNeedTriggerApronArucoLand(); boolean needTriggerAlterArucoLand = PreferenceUtils.getInstance().getNeedTriggerAlterArucoLand(); double thresholdMax = triggerToAlternatePoint ? FLYING_HEIGHT_THRESHOLD_MAX_ALTERNATE : FLYING_HEIGHT_THRESHOLD_MAX; - //LogUtil.log(TAG,"相对高度"+Movement.getInstance().getElevation()+"超声波高度"+Movement.getInstance().getUltrasonicHeight()); + + // 节流打印相对高度和超声波高度(约每秒1次) + if (++heightLogCounter >= HEIGHT_LOG_INTERVAL) { + LogUtil.log(TAG, "[高度监控] 相对高度=" + Movement.getInstance().getElevation() + "m, 超声波高度=" + Movement.getInstance().getUltrasonicHeight() + "dm"); + heightLogCounter = 0; + } - if (isFlying && (Movement.getInstance().getElevation() < 15 && Movement.getInstance().getUltrasonicHeight() < 50) && !isSendDetect) { - // LogUtil.log(TAG,"相对高度"+Movement.getInstance().getElevation()+"超声波高度"+Movement.getInstance().getUltrasonicHeight()); + + + if (isFlying && (Movement.getInstance().getElevation() < 15 && Movement.getInstance().getUltrasonicHeight() < 50 || forceTriggerDetection) && !isSendDetect) { double flyingHeight = Movement.getInstance().getElevation(); double thresholdMin = triggerToAlternatePoint ? FLYING_HEIGHT_THRESHOLD_MIN_ALTERNATE : FLYING_HEIGHT_THRESHOLD_MIN; - - - if (flyingHeight > thresholdMin) { - //LogUtil.log(TAG,"相对高度"+Movement.getInstance().getElevation()+"超声波高度"+Movement.getInstance().getUltrasonicHeight()); + if (flyingHeight > thresholdMin || forceTriggerDetection) { boolean shouldTriggerDetection; if (isDebugMode) { + shouldTriggerDetection = goHomeExecutionState == 2; + } else { shouldTriggerDetection = goHomeExecutionState == 2 || needTriggerApronArucoLand || needTriggerAlterArucoLand; } - if (shouldTriggerDetection) { + if (shouldTriggerDetection || forceTriggerDetection) { + triggerArucoDetection(); + } } } @@ -1174,6 +1222,8 @@ public class FlightManager extends BaseManager { private void triggerArucoDetection() { //当电池电量大于40时,允许复降次数设置为10次 + isSendDetect = true; + if (Movement.getInstance().getBattery_a_capacity_percent() > 40) { AMSConfig.getInstance().setAlternateLandingTimes(10 + ""); } @@ -1192,7 +1242,7 @@ public class FlightManager extends BaseManager { LogUtil.log(TAG, "开始识别机库二维码,椭球高度:" + Movement.getInstance().getElevation() + "米" + "--超声波高度:" + Movement.getInstance().getUltrasonicHeight() + "分米"); sendEvent2Server("开始视觉降落", 1); } - isSendDetect = true; + //PerceptionManager.getInstance().setPerceptionEnable(false); } @@ -1242,6 +1292,8 @@ public class FlightManager extends BaseManager { isGimbalReset = false; isTriggerLanding = true; isGimbalDownwards = false; + forceTriggerDetection = false; + MainActivity.setStartArucoType(0); } @Override @@ -1283,6 +1335,8 @@ public class FlightManager extends BaseManager { isSendDetect = false; isTriggerLanding = false; sendCloseCabinDoorMsg = false; + forceTriggerDetection = false; + MainActivity.setStartArucoType(0); ApronArucoDetect.getInstance().setCanLanding(false); ApronArucoDetectPort.getInstance().setCanLanding(false); Aprondown.getInstance().setCanLanding(false); @@ -1325,6 +1379,7 @@ public class FlightManager extends BaseManager { } // 上传媒体文件 SystemManager.getInstance().upLoadMedia(MqttManager.getInstance().mqttAndroidClient); + sendEvent2Server("开始上传日志文件", 1); //上传日志文件 LogUploadManager.getInstance().uploadTodayLog(); diff --git a/app/src/main/java/com/aros/apron/manager/MediaManager.java b/app/src/main/java/com/aros/apron/manager/MediaManager.java index bdbf466d..166c0c5a 100644 --- a/app/src/main/java/com/aros/apron/manager/MediaManager.java +++ b/app/src/main/java/com/aros/apron/manager/MediaManager.java @@ -102,27 +102,44 @@ public class MediaManager extends BaseManager { private int enterPlayBackFailTimes; private boolean isEnablePlayback; + private volatile boolean isPlaybackEnabling; // 防止并发调用 + private Handler playbackTimeoutHandler = new Handler(android.os.Looper.getMainLooper()); + private Runnable playbackTimeoutRunnable = null; + private static final int PLAYBACK_ENABLE_TIMEOUT_MS = 15000; // 进入媒体模式超时 15 秒 public void enablePlayback() { - // 每次进入媒体模式时清空已上传文件集合 - uploadedFileNames.clear(); - bucketChecked = false; - downloadFailTimes = 0; - // 重置失败计数和标志 - enterPlayBackFailTimes = 0; - isEnablePlayback = false; - // 重置拉取文件列表相关的计数器 - pullMediaFileListFromCameraFailTimes = 0; - updatingWaitCount = 0; - pullqwq = false; - pullStartTime = System.currentTimeMillis(); // 开始计时 + if (isPlaybackEnabling) { + LogUtil.log(TAG, "媒体模式已在启用中,跳过重复调用"); + return; + } + boolean isFirstEntry = !isPlaybackEnabling; + isPlaybackEnabling = true; - LogUtil.log(TAG, "已清空上传文件集合"); + // 停止端口扫描和 RTSP 刷新,关闭 RTSP 推流,确保媒体上传稳定 + StreamManager.getInstance().stopStreamRefreshTimer(); + com.aros.apron.tools.SimplePortScanner.getInstance().stopScan(); + StreamManager.getInstance().stopstream(); + + // 仅首次调用时清理状态,重试时保留计数器 + 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 public void onSuccess() { LogUtil.log(TAG, "进入媒体模式成功"); + isPlaybackEnabling = false; // 成功后释放锁 new Handler().postDelayed(new Runnable() { @Override public void run() { @@ -149,8 +166,60 @@ public class MediaManager extends BaseManager { public void run() { if (enterPlayBackFailTimes < 10) { enterPlayBackFailTimes++; - enablePlayback(); + 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); } diff --git a/app/src/main/java/com/aros/apron/manager/MissionV3Manager.java b/app/src/main/java/com/aros/apron/manager/MissionV3Manager.java index e435d771..1cb6d8e8 100644 --- a/app/src/main/java/com/aros/apron/manager/MissionV3Manager.java +++ b/app/src/main/java/com/aros/apron/manager/MissionV3Manager.java @@ -909,9 +909,15 @@ public class MissionV3Manager extends BaseManager { Boolean isConnect = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey. KeyConnection)); if (isConnect != null && isConnect) { + if (isConnect == null || !isConnect || !getGimbalAndCameraEnabled()) { + LogUtil.log(TAG,"当前状态禁止恢复"); + sendFailMsg2Server(message, "当前状态禁止恢复"); + return; + } if (Movement.getInstance().getFlightmode() == 1) { Movement.getInstance().setMode_code(5); } + IWaypointMissionManager missionManager = WaypointMissionManager.getInstance(); //如果航线是暂停状态,直接恢复航线,否则查询断点信息 if (Movement.getInstance().getMissionStateCode() == 7) { diff --git a/app/src/main/java/com/aros/apron/manager/OSDManager.java b/app/src/main/java/com/aros/apron/manager/OSDManager.java index 60345124..d2db938f 100644 --- a/app/src/main/java/com/aros/apron/manager/OSDManager.java +++ b/app/src/main/java/com/aros/apron/manager/OSDManager.java @@ -185,6 +185,23 @@ public class OSDManager extends BaseManager { irMeteringPoint.setX(Movement.getInstance().getX()); irMeteringPoint.setY(Movement.getInstance().getY()); cameraA.setIr_metering_point(irMeteringPoint); + Osd.Data.Cameras.IrMeteringArea irMeteringArea = new Osd.Data.Cameras.IrMeteringArea(); + irMeteringArea.setX(0); + irMeteringArea.setY(0); + irMeteringArea.setWidth(0); + irMeteringArea.setHeight(0); + irMeteringArea.setAver_temperature(0); + Osd.Data.Cameras.IrTemperaturePoint irMinTemp = new Osd.Data.Cameras.IrTemperaturePoint(); + irMinTemp.setX(0); + irMinTemp.setY(0); + irMinTemp.setTemperature(0); + irMeteringArea.setMin_temperature_point(irMinTemp); + Osd.Data.Cameras.IrTemperaturePoint irMaxTemp = new Osd.Data.Cameras.IrTemperaturePoint(); + irMaxTemp.setX(0); + irMaxTemp.setY(0); + irMaxTemp.setTemperature(0); + irMeteringArea.setMax_temperature_point(irMaxTemp); + cameraA.setIr_metering_area(irMeteringArea); cameraA.setIr_zoom_factor(Movement.getInstance().getIr_zoom_factor()); cameraA.setPayload_index("53-0-0"); cameraA.setPhoto_state(Movement.getInstance().getPhoto_state()); diff --git a/app/src/main/java/com/aros/apron/manager/PayloadWidgetManager.java b/app/src/main/java/com/aros/apron/manager/PayloadWidgetManager.java index 89c76e2a..9497dd0e 100644 --- a/app/src/main/java/com/aros/apron/manager/PayloadWidgetManager.java +++ b/app/src/main/java/com/aros/apron/manager/PayloadWidgetManager.java @@ -2,9 +2,18 @@ package com.aros.apron.manager; import androidx.annotation.NonNull; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + import com.aros.apron.base.BaseManager; +import com.aros.apron.constant.AMSConfig; +import com.aros.apron.entity.Movement; import com.aros.apron.entity.PayloadInfo; +import com.aros.apron.entity.PsdkWidgetValuesReport; import com.aros.apron.tools.LogUtil; +import com.aros.apron.tools.MqttManager; import com.aros.apron.xclog.XcFileLog; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -12,6 +21,7 @@ import com.google.gson.GsonBuilder; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; import dji.sdk.keyvalue.key.FlightControllerKey; import dji.sdk.keyvalue.key.KeyTools; @@ -33,6 +43,16 @@ public class PayloadWidgetManager extends BaseManager { private List payloadInfos = new ArrayList<>(); + private PayloadBasicInfo cachedBasicInfo; + private PayloadWidgetInfo cachedWidgetInfo; + private Handler psdkReportHandler; + private Runnable psdkReportRunnable; + private static final long PSDK_REPORT_INTERVAL = 5000; // 5秒 + + private StringBuffer payloadBuffer = new StringBuffer(); + private volatile boolean hasSpeaker = false; + private volatile boolean speakerPortSet = false; + private PayloadWidgetManager() { } @@ -58,7 +78,6 @@ public class PayloadWidgetManager extends BaseManager { if (payloadManager != null) { IPayloadManager iPayloadManager = payloadManager.get(PayloadIndexType.PORT_2); if (iPayloadManager != null) { - PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_2).pullWidgetInfoFromPayload(new CommonCallbacks.CompletionCallback() { @Override public void onSuccess() { @@ -66,16 +85,42 @@ public class PayloadWidgetManager extends BaseManager { PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_2).addPayloadBasicInfoListener(new PayloadBasicInfoListener() { @Override public void onPayloadBasicInfoUpdate(PayloadBasicInfo info) { - LogUtil.log(TAG, "左侧负载基础信息:" + info.toString()); - XcFileLog.getInstace().w(TAG, "左侧负载基础信息:" + info.toString()); + //LogUtil.log(TAG, "左侧负载基础信息:" + info.toString()); + // XcFileLog.getInstace().w(TAG, "左侧负载基础信息:" + info.toString()); + //cachedBasicInfo = info; + //startPsdkWidgetReport(); } }); PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_2).addPayloadDataListener(new PayloadDataListener() { @Override public void onDataFromPayloadUpdate(byte[] data) { - LogUtil.log(TAG, "左侧负载数据信息:" +new String(data, java.nio.charset.StandardCharsets.UTF_8).replaceAll("[^\\x20-\\x7E\\u4e00-\\u9fa5]", "")); - XcFileLog.getInstace().w(TAG, "左侧负载数据信息:" +new String(data, java.nio.charset.StandardCharsets.UTF_8).replaceAll("[^\\x20-\\x7E\\u4e00-\\u9fa5]", "")); + String cleanData = new String(data, java.nio.charset.StandardCharsets.UTF_8) + .replaceAll("[^\\x20-\\x7E\\u4e00-\\u9fa5]", ""); + // LogUtil.log(TAG, "左侧负载数据信息:" + cleanData); + // XcFileLog.getInstace().w(TAG, "左侧负载数据信息:" + cleanData); + + + if (cleanData.contains("jwD")) { + payloadBuffer.setLength(0); + payloadBuffer.append(cleanData); + // LogUtil.log(TAG, "负载数据开始,清空缓存"); + } + + + if (cleanData.contains("}")) { + if (payloadBuffer.length() > 0) { + payloadBuffer.append(cleanData); + String fullJson = payloadBuffer.toString(); + payloadBuffer.setLength(0); // 清空,准备下次 + // LogUtil.log(TAG, "负载完整数据: " + fullJson); + sendFloatingWindowText(fullJson); + } else { + //LogUtil.log(TAG, "负载数据: 缓存为空,忽略孤立}"); + } + } + + } }); @@ -83,9 +128,12 @@ public class PayloadWidgetManager extends BaseManager { PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_2).addPayloadWidgetInfoListener(new PayloadWidgetInfoListener() { @Override public void onPayloadWidgetInfoUpdate(PayloadWidgetInfo info) { - LogUtil.log(TAG, "左侧负载控件信息:" + info.toString()); - XcFileLog.getInstace().w(TAG, "左侧负载控件信息:" + info.toString()); + //LogUtil.log(TAG, "左侧负载控件信息:" + info.toString()); + //XcFileLog.getInstace().w(TAG, "左侧负载控件信息:" + info.toString()); + //cachedWidgetInfo = info; //如果负载为空 + detectSpeaker(info, 2); + if (info.getConfigInterfaceWidgetList() == null || info.getMainInterfaceWidgetList() == null) { PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_2).pullWidgetInfoFromPayload(new CommonCallbacks.CompletionCallback() { @Override @@ -112,13 +160,52 @@ public class PayloadWidgetManager extends BaseManager { } else { LogUtil.log(TAG, "监听LEFT_OR_MAIN PSDK数据失败:设备未连接"); } + + + IPayloadManager iPayloadManager3 = payloadManager.get(PayloadIndexType.PORT_3); + if (iPayloadManager3 != null) { + //基础信息 + PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_3).addPayloadBasicInfoListener(new PayloadBasicInfoListener() { + @Override + public void onPayloadBasicInfoUpdate(PayloadBasicInfo info) { + // LogUtil.log(TAG, "3号负载基础信息:" + info.toString()); + } + }); + PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_3).addPayloadDataListener(new PayloadDataListener() { + @Override + public void onDataFromPayloadUpdate(byte[] data) { + // LogUtil.log(TAG, "3号Data负载基础信息:" + data); + } + }); + //可以把负载设备控件打印 + PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_3).addPayloadWidgetInfoListener(new PayloadWidgetInfoListener() { + @Override + public void onPayloadWidgetInfoUpdate(PayloadWidgetInfo info) { + // LogUtil.log(TAG, "3号负载控件信息:" + info.toString()); + detectSpeaker(info, 3); + if (info.getConfigInterfaceWidgetList() == null || info.getMainInterfaceWidgetList() == null) { + PayloadCenter.getInstance().getPayloadManager().get(PayloadIndexType.PORT_3).pullWidgetInfoFromPayload(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + // LogUtil.log(TAG, "负载重复拉取成功"); + } + @Override + public void onFailure(@NonNull IDJIError idjiError) { + //LogUtil.log(TAG, "负载重复拉取失败"); + } + }); + } + } + }); + } else { + LogUtil.log(TAG, "监听right_OR_MAIN PSDK数据失败:设备未连接"); + } + + + } else { LogUtil.log(TAG, "监听psdk数据失败:未检测到设备"); } - - //负载基础信息 - - } else { LogUtil.log(TAG, "设备未连接"); } @@ -425,6 +512,53 @@ public class PayloadWidgetManager extends BaseManager { // } + /** + * 检测负载控件信息中是否有喊话器,检测到后自动设置喊话器位置 + * @param info 负载控件信息 + * @param portIndex 端口编号 2或3 + */ + private void detectSpeaker(PayloadWidgetInfo info, int portIndex) { + if (info == null || speakerPortSet) return; + try { + Object speakerWidget = info.getSpeakerWidget(); + if (speakerWidget != null) { + String infoStr = info.toString(); + // speakerWidget=SpeakerWidget{isTTSEnabled=true, isVoiceEnabled=true} 代表有喊话器 + if (infoStr.contains("SpeakerWidget") && infoStr.contains("isTTSEnabled=true")) { + hasSpeaker = true; + LogUtil.log(TAG, "检测到喊话器负载(SpeakerWidget)在 PORT_" + portIndex); + // 自动设置喊话器位置,设置成功后不再重复设置 + dji.v5.manager.aircraft.megaphone.MegaphoneIndex targetIndex = + portIndex == 2 ? dji.v5.manager.aircraft.megaphone.MegaphoneIndex.PORT_2 + : dji.v5.manager.aircraft.megaphone.MegaphoneIndex.PORT_3; + LogUtil.log(TAG, "自动设置喊话器位置到: " + targetIndex); + dji.v5.manager.aircraft.megaphone.MegaphoneManager.getInstance().setMegaphoneIndex(targetIndex, + new dji.v5.common.callback.CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + speakerPortSet = true; + LogUtil.log(TAG, "喊话器位置设置成功(从负载控件自动设置): " + targetIndex + ",不再重复设置"); + // 同步到 SpeakerManager + com.aros.apron.manager.SpeakerManager.psdkindex = portIndex; + com.aros.apron.manager.SpeakerManager.getInstance().getMegaphoneIndex(); + } + + @Override + public void onFailure(@NonNull dji.v5.common.error.IDJIError error) { + LogUtil.log(TAG, "喊话器位置设置失败(从负载控件自动设置): " + error); + } + }); + } + } + } catch (Exception e) { + LogUtil.log(TAG, "检测喊话器异常: " + e.getMessage()); + } + } + + public boolean hasSpeaker() { + return hasSpeaker; + } + /** * 检查列表中是否已经存在指定payloadIndexType的PayloadInfo对象。 */ @@ -436,4 +570,140 @@ public class PayloadWidgetManager extends BaseManager { } return false; } + + + /** + * 启动5秒定时上报 + */ + private void startPsdkWidgetReport() { + if (psdkReportHandler == null) { + psdkReportHandler = new Handler(Looper.getMainLooper()); + } + if (psdkReportRunnable != null) { + return; // 已经在运行 + } + psdkReportRunnable = new Runnable() { + @Override + public void run() { + + psdkReportHandler.postDelayed(this, PSDK_REPORT_INTERVAL); + } + }; + psdkReportHandler.post(psdkReportRunnable); + LogUtil.log(TAG, "启动PSDK负载控件5秒定时上报"); + } + + /** + * 停止定时上报 + */ + private void stopPsdkWidgetReport() { + if (psdkReportHandler != null && psdkReportRunnable != null) { + psdkReportHandler.removeCallbacks(psdkReportRunnable); + psdkReportRunnable = null; + LogUtil.log(TAG, "停止PSDK负载控件定时上报"); + } + } + + /** + * 发送PSDK负载控件值到MQTT + */ +// private void sendPsdkWidgetValues() { +// try { +// if (MqttManager.getInstance().mqttAndroidClient == null || !MqttManager.getInstance().mqttAndroidClient.isConnected()) { +// return; +// } +// if (cachedBasicInfo == null && cachedWidgetInfo == null) { +// return; +// } +// +// PsdkWidgetValuesReport report = new PsdkWidgetValuesReport(); +// report.setTid(UUID.randomUUID().toString()); +// report.setBid(UUID.randomUUID().toString()); +// report.setTimestamp(System.currentTimeMillis()); +// report.setMethod("psdk_widget_values"); +// report.setGateway(Movement.getInstance().getSerialNumber()); +// +// PsdkWidgetValuesReport.Data data = new PsdkWidgetValuesReport.Data(); +// List widgetList = new ArrayList<>(); +// +// // 从cachedBasicInfo和cachedWidgetInfo组装数据 +// if (cachedBasicInfo != null) { +// PsdkWidgetValuesReport.PsdkWidgetValue w = new PsdkWidgetValuesReport.PsdkWidgetValue(); +// w.setPsdk_index(2); +// w.setPsdk_lib_version(cachedBasicInfo.getVersion() != null ? cachedBasicInfo.getVersion() : ""); +// w.setPsdk_name(cachedBasicInfo.getProductName() != null ? cachedBasicInfo.getProductName() : ""); +// w.setPsdk_sn(cachedBasicInfo.getDeviceSN() != null ? cachedBasicInfo.getDeviceSN() : ""); +// w.setPsdk_type(5); // Speaker type +// w.setPsdk_version(cachedBasicInfo.getVersion() != null ? cachedBasicInfo.getVersion() : ""); +// +// // Speaker数据 +// PsdkWidgetValuesReport.SpeakerData speaker = new PsdkWidgetValuesReport.SpeakerData(); +// speaker.setPlay_file_md5(""); +// speaker.setPlay_file_name(""); +// speaker.setPlay_mode(1); +// speaker.setPlay_volume(30); +// speaker.setSystem_state(0); +// speaker.setWork_mode(1); +// +// // 从widgetInfo中尝试获取实际的widget值 +// if (cachedWidgetInfo != null) { +// if (cachedWidgetInfo.getMainInterfaceWidgetList() != null) { +// for (Object widget : cachedWidgetInfo.getMainInterfaceWidgetList()) { +// // 根据widget类型设置speaker字段 +// } +// } +// } +// w.setSpeaker(speaker); +// w.setValues(new ArrayList<>()); +// widgetList.add(w); +// } +// +// data.setPsdk_widget_values(widgetList); +// report.setData(data); +// +// String json = gson.toJson(report); +// LogUtil.log(TAG, "上报psdk_widget_values: " + json); +// +// MqttMessage msg = new MqttMessage(json.getBytes("UTF-8")); +// msg.setQos(0); +// MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_EVENT, msg); +// } catch (Exception e) { +// LogUtil.log(TAG, "发送psdk_widget_values异常: " + e.getMessage()); +// e.printStackTrace(); +// } +// } + + /** + * 发送负载浮窗文本到MQTT + */ + private void sendFloatingWindowText(String value) { + try { + if (MqttManager.getInstance().mqttAndroidClient == null || !MqttManager.getInstance().mqttAndroidClient.isConnected()) { + LogUtil.log(TAG, "MQTT未连接,跳过浮窗文本上报"); + return; + } + + java.util.Map report = new java.util.LinkedHashMap<>(); + report.put("bid", UUID.randomUUID().toString()); + report.put("tid", UUID.randomUUID().toString()); + report.put("timestamp", System.currentTimeMillis()); + report.put("method", "psdk_floating_window_text_jdw"); + report.put("gateway", Movement.getInstance().getSerialNumber()); + + java.util.Map data = new java.util.LinkedHashMap<>(); + data.put("psdk_index", 2); + data.put("value", value); + report.put("data", data); + + String json = new Gson().toJson(report); + LogUtil.log(TAG, "发送浮窗文本: " + json); + + org.eclipse.paho.client.mqttv3.MqttMessage msg = new org.eclipse.paho.client.mqttv3.MqttMessage(json.getBytes("UTF-8")); + msg.setQos(1); + MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_EVENT, msg); + } catch (Exception e) { + LogUtil.log(TAG, "发送浮窗文本异常: " + e.getMessage()); + e.printStackTrace(); + } + } } diff --git a/app/src/main/java/com/aros/apron/manager/SpeakerManager.java b/app/src/main/java/com/aros/apron/manager/SpeakerManager.java index 5a2f6483..6532746a 100644 --- a/app/src/main/java/com/aros/apron/manager/SpeakerManager.java +++ b/app/src/main/java/com/aros/apron/manager/SpeakerManager.java @@ -37,9 +37,16 @@ import okhttp3.Request; import okhttp3.Response; + public class SpeakerManager extends BaseManager { - private boolean issetok=false; + private volatile boolean issetok=false; + private android.os.Handler retryHandler; + private Runnable psdkIndexRetry; + public static int psdkindex=0; + + private static final int MAX_MEGAPHONE_RETRIES = 10; + private int megaphoneRetryCount = 0; private SpeakerManager() { } private static class SpeakerHolder { @@ -52,36 +59,215 @@ public class SpeakerManager extends BaseManager { private XTtsPcmGenerator ttsGenerator; public void initMegaphoneInfo() { - MegaphoneManager.getInstance().addMegaphoneInfoListener(new MegaphoneInfoListener() { - @Override - public void onUpdateMegaphoneInfo(MegaphoneInfo megaphoneInfo) { - if (megaphoneInfo != null) { - MegaphoneStatus megaphoneStatus = megaphoneInfo.getStatus(); - //LogUtil.log(TAG,"喊话器"+megaphoneStatus); - // 当喊话器状态为"已连接"时(status=2),再设置位置 - if (megaphoneStatus == MegaphoneStatus.IDLE && issetok==false) { - - MegaphoneManager.getInstance().setMegaphoneIndex(MegaphoneIndex.PORT_2, new CommonCallbacks.CompletionCallback() { - @Override - public void onSuccess() { - issetok=true; - LogUtil.log(TAG,"喊话器位置设置成功"); - } - - @Override - public void onFailure(@NonNull IDJIError error) { - LogUtil.log(TAG,"喊话器位置设置失败:"+getIDJIErrorMsg(error)); - } - }); + addMegaphoneInfoListener(); + // 延迟10秒再触发查询,等待喊话器硬件完全初始化 + if (!issetok) { + if (retryHandler == null) { + retryHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + } + retryHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!issetok) { + startMegaphoneIndexQuery(); } } + }, 10000); + } + } + + /** + * 主动查询并设置喊话器位置,失败后3秒重试,成功后不再重试 + */ + public void startMegaphoneIndexQuery() { + MegaphoneManager.getInstance().getMegaphoneIndex(new CommonCallbacks.CompletionCallbackWithParam() { + @Override + public void onSuccess(MegaphoneIndex megaphoneIndex) { + LogUtil.log(TAG,"喊话器位置"+megaphoneIndex); + if (megaphoneIndex == MegaphoneIndex.UNKNOWN || megaphoneIndex == MegaphoneIndex.PORTSIDE) { + LogUtil.log(TAG, "喊话器位置查询返回不确定(" + megaphoneIndex + "),直接进入重试"); + if (!issetok) { + retryMegaphoneIndex(); + } + return; + } + MegaphoneIndex targetIndex = megaphoneIndex; + MegaphoneManager.getInstance().setMegaphoneIndex(targetIndex, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + issetok=true; + LogUtil.log(TAG,"喊话器位置设置成功"); + getMegaphoneIndex(); + } + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG,"喊话器位置设置失败:"+getIDJIErrorMsg(error)); + if (!issetok) { + retryMegaphoneIndex(); + } + } + }); + } + @Override + public void onFailure(@NonNull IDJIError idjiError) { + LogUtil.log(TAG, "startMegaphoneIndexQuery失败: " + getIDJIErrorMsg(idjiError) + ",3秒后重试"); + if (!issetok) { + retryMegaphoneIndex(); + } } }); } + + private void retryMegaphoneIndex() { + if (megaphoneRetryCount >= MAX_MEGAPHONE_RETRIES) { + LogUtil.log(TAG, "喊话器位置设置已达到最大重试次数(" + MAX_MEGAPHONE_RETRIES + "),尝试盲设 PORT_2 / PORT_3"); + blindSetMegaphoneIndex(); + return; + } + if (retryHandler == null) { + retryHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + } + retryHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!issetok) { + megaphoneRetryCount++; + LogUtil.log(TAG, "喊话器位置设置重试第 " + megaphoneRetryCount + " 次"); + startMegaphoneIndexQuery(); + } + } + }, 3000); + } + + private boolean isBlindSetting = false; + + /** + * 10次查询重试都失败后,依次盲试 PORT_2 和 PORT_3,只要有一个成功即可 + */ + private void blindSetMegaphoneIndex() { + if (issetok || isBlindSetting) return; + isBlindSetting = true; + MegaphoneIndex[] candidates = {MegaphoneIndex.PORT_2, MegaphoneIndex.PORT_3}; + blindSetWithIndex(candidates, 0); + } + + private void blindSetWithIndex(MegaphoneIndex[] candidates, int index) { + if (index >= candidates.length || issetok) { + if (!issetok) { + LogUtil.log(TAG, "盲设喊话器位置全部失败,PORT_2 和 PORT_3 均不可用"); + + } + return; + } + final MegaphoneIndex target = candidates[index]; + final int targetIdx = (target == MegaphoneIndex.PORT_2) ? 2 : 3; + LogUtil.log(TAG, "盲设喊话器位置: " + target); + MegaphoneManager.getInstance().setMegaphoneIndex(target, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + issetok = true; + psdkindex = targetIdx; + Movement.getInstance().setPsdk_index(String.valueOf(targetIdx)); + LogUtil.log(TAG, "盲设喊话器位置成功: " + target + ",psdk_index=" + targetIdx); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + psdkindex=0; + LogUtil.log(TAG, "盲设 " + target + " 失败,尝试下一个"); + blindSetWithIndex(candidates, index + 1); + } + }); + } + + //喊话器位置 + public void getMegaphoneIndex(){ + MegaphoneManager.getInstance().getMegaphoneIndex(new CommonCallbacks.CompletionCallbackWithParam() { + @Override + public void onSuccess(MegaphoneIndex megaphoneIndex) { + int index = switch (megaphoneIndex) { + case PORT_1 -> 1; + case PORT_2 -> 2; + case PORT_3 -> 3; + case PORTSIDE, UNKNOWN -> 0; // PORTSIDE和UNKNOWN都视为不确定,进入盲设 + default -> 0; + }; + if (index != 0) { + Movement.getInstance().setPsdk_index(String.valueOf(index)); + issetok = true; + LogUtil.log(TAG, "psdk_index已设置并保持: " + index); + psdkindex=index; + } else { + LogUtil.log(TAG, "getMegaphoneIndex回调位置不确定(" + megaphoneIndex + "),进入盲设流程"); + blindSetMegaphoneIndex(); + } + + + } + + @Override + public void onFailure(@NonNull IDJIError idjiError) { + LogUtil.log(TAG, "getMegaphoneIndex失败: " + getIDJIErrorMsg(idjiError) + ",10秒后重试"); + if (retryHandler == null) { + retryHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + } + retryHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!issetok) { + getMegaphoneIndex(); + } + } + }, 10000); + } + }); + } + //喊话器基础信息 + public void addMegaphoneInfoListener(){ + MegaphoneManager.getInstance().addMegaphoneInfoListener(new MegaphoneInfoListener() { + @Override + public void onUpdateMegaphoneInfo(MegaphoneInfo megaphoneInfo) { + if (megaphoneInfo == null) return; + //工作模式 音量 状态 播放模式 + Movement.getInstance().setPlay_mode(String.valueOf(megaphoneInfo.getPlayMode().value)); + Movement.getInstance().setPlay_volume(String.valueOf(megaphoneInfo.getVolume())); + Movement.getInstance().setWork_mode(String.valueOf(megaphoneInfo.getWorkMode().co_a)); + Movement.getInstance().setSystem_state(String.valueOf(megaphoneInfo.getStatus().co_a)); + + MegaphoneStatus status = megaphoneInfo.getStatus(); + if (status == MegaphoneStatus.PLAYING) { + LogUtil.log(TAG, "喊话器状态: 正在播放中"); + Movement.getInstance().setStep_key("play"); + Movement.getInstance().setSpeakerpercent(100); + Movement.getInstance().setTTS_status("success"); + + } else if (status == MegaphoneStatus.IDLE) { + LogUtil.log(TAG, "喊话器状态: 空闲"); + speakerStop();//暂停上传 + + } else if (status == MegaphoneStatus.IN_TRANSMISSION) { + LogUtil.log(TAG, "喊话器状态: 数据传输"); + Movement.getInstance().setStep_key("upload"); + Movement.getInstance().setTTS_status("in_progress"); + + + } else if (status == MegaphoneStatus.TTS_IN_CONVERSION) { + LogUtil.log(TAG, "喊话器状态: tTTS正在转换成语音中"); + Movement.getInstance().setTTS_status("in_progress"); + + } + + + } + }); + } + // + public void speakerAudioPlayStart(MessageDown message) { - sendMsg2Server(message); - //暂停播放 - stop(); + + + stopspeaker(); + if (TextUtils.isEmpty(message.getData().getFile().getUrl())) { sendFailMsg2Server(message, "喊话文件下载地址为空"); @@ -133,61 +319,61 @@ public class SpeakerManager extends BaseManager { null); MegaphoneManager.getInstance().startPushingFileToMegaphone(fileInfo, new CommonCallbacks.CompletionCallbackWithProgress() { - private boolean isCallbackExecuted = false; // 确保回调只执行一次 + private boolean hasTriggeredPlay = false; @Override public void onProgressUpdate(Integer integer) { Movement.getInstance().setSpeakerpercent(integer); LogUtil.log(TAG, "喊话器内容上传进度:" + integer + "%"); + + if (integer >= 99 && !hasTriggeredPlay) { + hasTriggeredPlay = true; + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + MegaphoneManager.getInstance().startPlay(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + Movement.getInstance().setStep_key("play"); + Movement.getInstance().setTTS_status("success"); + sendMsg2Server(message); + SpeakerProgressReporter.getInstance().sendNowAudioProgress(psdkindex); + sendEvent2Server("喊话器播放音频成功", 1); + LogUtil.log(TAG,"喊话器播放音频成功"); + Synchronizedstatus.setSpeakrunning(false); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + + sendFailMsg2Server(message, "喊话器播放音频失败:" + getIDJIErrorMsg(error)); + sendEvent2Server("喊话器播放音频失败:" + getIDJIErrorMsg(error), 2); + Synchronizedstatus.setSpeakrunning(false); + LogUtil.log(TAG,"喊话器播放音频失败"+ getIDJIErrorMsg(error)); + } + }); + } + }, 1000); + } } @Override public void onSuccess() { - // 确保回调只执行一次 - if (isCallbackExecuted) { - return; - } - isCallbackExecuted = true; - LogUtil.log(TAG, "喊话器内容上传成功"); - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - MegaphoneManager.getInstance().startPlay(new CommonCallbacks.CompletionCallback() { - @Override - public void onSuccess() { - Movement.getInstance().setTTS_status("ok"); - sendEvent2Server("喊话器播放音频成功", 1); - LogUtil.log(TAG,"喊话器播放音频成功"); - Synchronizedstatus.setSpeakrunning(false); - } - - @Override - public void onFailure(@NonNull IDJIError error) { - sendEvent2Server("喊话器播放音频失败:" + getIDJIErrorMsg(error), 2); - Synchronizedstatus.setSpeakrunning(false); - LogUtil.log(TAG,"喊话器播放音频失败"+ getIDJIErrorMsg(error)); - } - }); - } - }, 1000); + Synchronizedstatus.setSpeakrunning(false); } @Override public void onFailure(@NonNull IDJIError error) { - // 确保回调只执行一次 - if (isCallbackExecuted) { - return; - } - isCallbackExecuted = true; - sendFailMsg2Server(message, "喊话器内容上传失败:" + getIDJIErrorMsg(error)); + Synchronizedstatus.setSpeakrunning(false); } }); } catch (Exception e) { sendEvent2Server("喊话.pcm文件下载失败:" + e.toString(), 2); + Synchronizedstatus.setSpeakrunning(false); } } } @@ -213,6 +399,22 @@ public class SpeakerManager extends BaseManager { } + public void stopspeaker(){ + MegaphoneManager.getInstance().stopPlay(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + Synchronizedstatus.setSpeaksetrunning(false); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + Synchronizedstatus.setSpeaksetrunning(false); + } + }); + + + } + public void speakerStop(MessageDown message) { MegaphoneManager.getInstance().stopPlay(new CommonCallbacks.CompletionCallback() { @Override @@ -232,7 +434,8 @@ public class SpeakerManager extends BaseManager { } }); } - public void stop(){ + + public void speakerStop() { MegaphoneManager.getInstance().stopPlay(new CommonCallbacks.CompletionCallback() { @Override public void onSuccess() { @@ -246,9 +449,10 @@ public class SpeakerManager extends BaseManager { SpeakerProgressReporter.getInstance().stopTTSReport(); } }); - } + + public void speakerPlayModeSet(MessageDown message) { MegaphoneManager.getInstance().setPlayMode(message.getData().getPlay_mode() == 0 ? PlayMode.SINGLE : PlayMode.LOOP, new CommonCallbacks.CompletionCallback() { @@ -285,8 +489,8 @@ public class SpeakerManager extends BaseManager { private String text; public void speakerTTSPlayStart(MessageDown message, int type) { - //暂停播放 - stop(); + + stopspeaker(); LogUtil.log(TAG, "开始执行 speakerTTSPlayStart 方法,type: " + type); // 检查是否正在运行 @@ -376,7 +580,7 @@ public class SpeakerManager extends BaseManager { LogUtil.log(TAG, "开始上传文件到喊话器"); MegaphoneManager.getInstance().startPushingFileToMegaphone(fileInfo, new CommonCallbacks.CompletionCallbackWithProgress() { - private boolean isCallbackExecuted = false; // 确保回调只执行一次 + @Override public void onProgressUpdate(Integer integer) { @@ -388,11 +592,6 @@ public class SpeakerManager extends BaseManager { @Override public void onSuccess() { - // 确保回调只执行一次 - if (isCallbackExecuted) { - return; - } - isCallbackExecuted = true; LogUtil.log(TAG, "喊话器内容上传成功"); new Handler().postDelayed(new Runnable() { @@ -403,8 +602,10 @@ public class SpeakerManager extends BaseManager { @Override public void onSuccess() { sendMsg2Server(message); - Movement.getInstance().setTTS_status("ok"); + Movement.getInstance().setTTS_status("success"); Movement.getInstance().setStep_key("play"); + + SpeakerProgressReporter.getInstance().sendNowTTSProgress(psdkindex); sendEvent2Server("喊话器播放TTS音频成功", 1); LogUtil.log(TAG,"喊话器播放TTS音频成功"); Synchronizedstatus.setSpeakTTSrunning(false); @@ -424,36 +625,37 @@ public class SpeakerManager extends BaseManager { @Override public void onFailure(@NonNull IDJIError error) { - // 确保回调只执行一次 - if (isCallbackExecuted) { - return; - } - isCallbackExecuted = true; - sendFailMsg2Server(message, "喊话器内容上传失败:" + getIDJIErrorMsg(error)); LogUtil.log(TAG, "喊话器内容上传失败:" + getIDJIErrorMsg(error)); + Synchronizedstatus.setSpeakTTSrunning(false); } }); } else { LogUtil.log(TAG, "合成失败,PCM 文件为空"); sendFailMsg2Server(message, "合成失败,PCM 文件为空"); + Synchronizedstatus.setSpeakTTSrunning(false); } LogUtil.log(TAG, "设置 speakTTSrunning 为 false"); LogUtil.log(TAG, "speakerTTSPlayStart 方法执行完成"); + Synchronizedstatus.setSpeakTTSrunning(false); } public void setindex(){ - MegaphoneManager.getInstance().setMegaphoneIndex(MegaphoneIndex.PORT_2, new CommonCallbacks.CompletionCallback() { + MegaphoneIndex targetIndex = psdkindex == 2 ? MegaphoneIndex.PORT_2 : + psdkindex == 3 ? MegaphoneIndex.PORT_3 : MegaphoneIndex.PORT_2; + LogUtil.log(TAG, "setindex 根据 psdkindex=" + psdkindex + " 设置位置: " + targetIndex); + MegaphoneManager.getInstance().setMegaphoneIndex(targetIndex, new CommonCallbacks.CompletionCallback() { @Override public void onSuccess() { - issetok=true; - LogUtil.log(TAG,"喊话器位置设置成功"); + issetok = true; + LogUtil.log(TAG, "喊话器位置设置成功"); + getMegaphoneIndex(); } @Override public void onFailure(@NonNull IDJIError error) { - LogUtil.log(TAG,"喊话器位置设置失败:"+getIDJIErrorMsg(error)); + LogUtil.log(TAG, "喊话器位置设置失败:" + getIDJIErrorMsg(error)); } }); } diff --git a/app/src/main/java/com/aros/apron/manager/StreamManager.java b/app/src/main/java/com/aros/apron/manager/StreamManager.java index 2db6970b..df803a81 100644 --- a/app/src/main/java/com/aros/apron/manager/StreamManager.java +++ b/app/src/main/java/com/aros/apron/manager/StreamManager.java @@ -52,7 +52,7 @@ public class StreamManager extends BaseManager { private final Handler streamRefreshHandler = new Handler(Looper.getMainLooper()); private Runnable streamRefreshRunnable = null; private volatile boolean isStreamRefreshing = false; - private static final long STREAM_REFRESH_INTERVAL_MS = 5000; + private static final long STREAM_REFRESH_INTERVAL_MS = 8000; private StreamManager() { } @@ -112,10 +112,12 @@ public class StreamManager extends BaseManager { // ========== 【新增】重置推流状态,用于端口关闭后重启 ========== public void resetStreamState() { - stopStreamRefreshTimer(); // 重置时也停止定时器 + stopStreamRefreshTimer(); + SimplePortScanner.getInstance().stopScan(); // 同时停止端口扫描 + mainHandler.removeCallbacksAndMessages(null); // 清理所有待执行的回调 startLiveFailTimes = 0; isLiveStreamAlreadyStart = false; - isStartingRTSP = false; // 重置并发标志 + isStartingRTSP = false; LogUtil.log(TAG, "推流状态已重置"); } @@ -201,9 +203,14 @@ public class StreamManager extends BaseManager { } - private int startLiveFailTimes; - private boolean isLiveStreamAlreadyStart; - private boolean isStartingRTSP = false; // 防止并发调用 + private volatile int startLiveFailTimes; + private volatile boolean isLiveStreamAlreadyStart; + private volatile boolean isStartingRTSP = false; // 防止并发调用 + + // 无限重试:指数退避控制 + private static final long RETRY_BASE_MS = 3000; // 初始 3 秒 + private static final long RETRY_MAX_MS = 30000; // 最大 30 秒 + private static final int LOG_INTERVAL = 10; // 每 10 次打一次日志 // 知眸测试 public void startLiveWithCustom() { @@ -247,16 +254,18 @@ public class StreamManager extends BaseManager { @Override public void onFailure(@NonNull IDJIError error) { mainHandler.post(() -> { - LogUtil.log(TAG, "第" + startLiveFailTimes + "次开始推流失败:" + new Gson().toJson(error)); + LogUtil.log(TAG, "第" + startLiveFailTimes + "次自定义推流失败:" + new Gson().toJson(error)); if (!isLiveStreamAlreadyStart) { + 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; mainHandler.postDelayed(() -> { - streamExecutor.execute(() -> { - if (startLiveFailTimes < 10) { - startLiveFailTimes++; - startLiveWithCustom(); - } - }); - }, 3000); + streamExecutor.execute(() -> startLiveWithCustom()); + }, retryDelay); } }); } @@ -321,8 +330,9 @@ public class StreamManager extends BaseManager { streamExecutor.execute(() -> { isStartingRTSP = true; - // 重置失败计数(每次新尝试时) + // 每次新推流尝试:清零所有状态标志(修复 Bug 1, 2, 5) if (startLiveFailTimes == 0) { + isLiveStreamAlreadyStart = false; LogUtil.log(TAG, "========== 开始 RTSP 推流流程 =========="); } @@ -345,8 +355,6 @@ public class StreamManager extends BaseManager { LogUtil.log(TAG, "RTSP 推流已在运行,无需重复启动"); isLiveStreamAlreadyStart = true; isStartingRTSP = false; - startStreamRefreshTimer(); // 推流在运行就启动定时器 - SimplePortScanner.getInstance().startScan(); // 确保端口扫描在运行 return; } @@ -362,16 +370,11 @@ public class StreamManager extends BaseManager { mainHandler.postDelayed(() -> { streamExecutor.execute(() -> { - if (startLiveFailTimes < 3) { - startLiveFailTimes++; + startLiveFailTimes++; + if (startLiveFailTimes <= 3 || startLiveFailTimes % LOG_INTERVAL == 0) { LogUtil.log(TAG, "相机流未准备好,第" + startLiveFailTimes + "次重试"); - isStartingRTSP = false; // 先重置标志,让重试调用能正常进入 - startLiveWithRTSP(); - } else { - LogUtil.log(TAG, "相机流未准备好,重试次数已达上限,强制启动"); - forceStartLive(); - isStartingRTSP = false; // 重置并发标志 } + startLiveWithRTSP(); }); }, 2000); isStartingRTSP = false; // 释放锁,让重试能正常进入 @@ -386,7 +389,7 @@ public class StreamManager extends BaseManager { if (rtspUser == null || rtspPort == null || rtspPass == null || rtspUser.isEmpty() || rtspPort.isEmpty() || rtspPass.isEmpty()) { LogUtil.log(TAG, "RTSP 配置参数有误:user=" + rtspUser + ", port=" + rtspPort); - isStartingRTSP = false; // 重置并发标志 + isStartingRTSP = false; return; } @@ -430,8 +433,7 @@ public class StreamManager extends BaseManager { if (finalLiveStreamManager.isStreaming()) { LogUtil.log(TAG, "推流已在运行,跳过启动"); isLiveStreamAlreadyStart = true; - isStartingRTSP = false; // 重置并发标志 - SimplePortScanner.getInstance().startScan(); // 确保端口扫描在运行 + isStartingRTSP = false; return; } @@ -445,63 +447,6 @@ public class StreamManager extends BaseManager { - private void forceStartLive() { - streamExecutor.execute(() -> { - if (PreferenceUtils.getInstance().getRtspUserName() != null && - PreferenceUtils.getInstance().getRtspPort() != null && - PreferenceUtils.getInstance().getRtspPassWord() != null) { - - ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); - - // 直接设置参数并启动,不检查相机流状态 - mainHandler.post(() -> { - 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(isliveindex == 1 ? ComponentIndexType.PORT_1 : ComponentIndexType.FPV); - liveStreamManager.setLiveStreamQuality(StreamQuality.FULL_HD); - liveStreamManager.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO); - - // 直接启动 - liveStreamManager.startStream(new CommonCallbacks.CompletionCallback() { - @Override - public void onSuccess() { - mainHandler.post(() -> { - LogUtil.log(TAG, "强制启动 RTSP 推流成功"); - // 统一刷新视频流 - MainActivity mainActivity = MainActivity.Companion.getInstance(); - if (mainActivity != null) { - mainActivity.smartRefreshVideoStream(); - } - isLiveStreamAlreadyStart = true; - isStartingRTSP = false; // 重置并发标志 - startStreamRefreshTimer(); // 强制启动成功也启动定时器 - SimplePortScanner.getInstance().startScan(); - }); - } - - @Override - public void onFailure(@NonNull IDJIError error) { - mainHandler.post(() -> { - LogUtil.log(TAG, "强制启动 RTSP 推流失败:" + new Gson().toJson(error)); - isStartingRTSP = false; // 重置并发标志 - // 失败后也尝试重置状态并重启 - resetStreamState(); - }); - } - }); - }); - } else { - LogUtil.log(TAG, "RTSP 配置参数有误,无法强制启动"); - isStartingRTSP = false; // 重置并发标志 - } - }); - } - // ========== 【新增】实际启动流的私有方法,确保在子线程执行 ========== private void doStartLiveWithRTSP(ILiveStreamManager liveStreamManager, boolean isRestart) { // 启动前再次检查状态 @@ -509,8 +454,6 @@ public class StreamManager extends BaseManager { LogUtil.log(TAG, "推流已在运行,跳过启动 (isRestart=" + isRestart + ")"); isLiveStreamAlreadyStart = true; isStartingRTSP = false; // 重置并发标志 - startStreamRefreshTimer(); - SimplePortScanner.getInstance().startScan(); // 确保端口扫描在运行 return; } @@ -540,34 +483,25 @@ public class StreamManager extends BaseManager { LogUtil.log(TAG, detailedError); LogUtil.log(TAG, "完整错误:" + new Gson().toJson(error)); - // 错误类型判断 - if (error.hint() != null) { - if (error.hint().contains("CAMERA")) { - LogUtil.log(TAG, "[诊断] 相机相关错误:可能是相机模式不对/相机被占用/SD 卡异常"); - } else if (error.hint().contains("LIVE")) { - LogUtil.log(TAG, "[诊断] 推流相关错误:可能是推流配置问题/端口被占用"); - } else if (error.hint().contains("PORT") || error.hint().contains("port")) { - LogUtil.log(TAG, "[诊断] 端口相关错误:请检查 RTSP 端口是否被占用"); - } - } - if (!isLiveStreamAlreadyStart) { - // 延时 3 秒后重试,重试也提交到线程池 + // 计算指数退避间隔: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(() -> { - streamExecutor.execute(() -> { - if (startLiveFailTimes < 10) { - startLiveFailTimes++; - LogUtil.log(TAG, "准备第" + startLiveFailTimes + "次重试 RTSP 推流"); - startLiveWithRTSP(); - } else { - LogUtil.log(TAG, "RTSP 推流重试次数已达上限 (10 次),放弃"); - LogUtil.log(TAG, "========== RTSP 推流失败 =========="); - isStartingRTSP = false; // 重置并发标志 - } - }); - }, 3000); + startLiveWithRTSP(); + }, retryDelay); } else { - isStartingRTSP = false; // 重置并发标志 + isStartingRTSP = false; } }); } diff --git a/app/src/main/java/com/aros/apron/manager/TakeOffToPointManager.java b/app/src/main/java/com/aros/apron/manager/TakeOffToPointManager.java index ad46c33b..6958053f 100644 --- a/app/src/main/java/com/aros/apron/manager/TakeOffToPointManager.java +++ b/app/src/main/java/com/aros/apron/manager/TakeOffToPointManager.java @@ -385,8 +385,11 @@ public class TakeOffToPointManager extends BaseManager { PreferenceUtils.getInstance().setTriggerToAlternatePoint(false); // 重置开舱门标志,避免上次任务未正常落地导致本次无法开舱 - FlightManager.getInstance().resetOpenCabinDoorState(); - DockOpenManager.getInstance().resetState(); + // 只在首次尝试时重置,避免重试时反复触发 + if (startMissionFailTimes == 0) { + FlightManager.getInstance().resetOpenCabinDoorState(); + DockOpenManager.getInstance().resetState(); + } PreferenceUtils.getInstance().setFlightId(message.getData().getFlight_id()); CommonCallbacks.CompletionCallback callback = new CommonCallbacks.CompletionCallback() { diff --git a/app/src/main/java/com/aros/apron/mix/Aprondown.java b/app/src/main/java/com/aros/apron/mix/Aprondown.java index a4533bdc..2f99ec3d 100644 --- a/app/src/main/java/com/aros/apron/mix/Aprondown.java +++ b/app/src/main/java/com/aros/apron/mix/Aprondown.java @@ -4,7 +4,9 @@ import android.os.Looper; import com.aros.apron.constant.AMSConfig; import com.aros.apron.entity.ArucoMarker; import com.aros.apron.entity.Movement; +import com.aros.apron.entity.Synchronizedstatus; import com.aros.apron.manager.AlternateLandingManager; +import com.aros.apron.manager.FlightManager; import com.aros.apron.tools.DroneHelper; import com.aros.apron.tools.LogUtil; import com.aros.apron.tools.PIDControl; @@ -66,6 +68,21 @@ public class Aprondown { isStartAruco = startAruco; } + /** + * 切换相机时停止检测 + */ + public void stopDetection() { + if (lastFuture != null && !lastFuture.isDone()) { + lastFuture.cancel(true); + } + isStartAruco = false; + startFastStick = false; + arucoNotFoundTag = false; + isHeightStableMonitoring = false; + frameCounter = 0; + LogUtil.log(TAG_LOG, "【停止检测】下视相机模块已停止"); + } + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); Future lastFuture = null; private String TAG_LOG = getClass().getSimpleName(); @@ -131,12 +148,50 @@ public class Aprondown { pidControlX.reset(); pidControlY.reset(); } - public boolean startFastStick; public boolean canLanding; - // ========== 【核心修改】主入口:2帧一处理 + 预处理回退 ========== + public void detectForceTriggerTags(int height, int width, byte[] data, Dictionary dictionary) { + Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1); + yuvMat.put(0, 0, data); + Mat grayImgMat = yuvMat.submat(0, height, 0, width); + grayImgMat = grayImgMat.clone(); + + MatOfInt ids = new MatOfInt(); + List corners = new ArrayList<>(); + DetectorParameters parameters = new DetectorParameters(); + ArucoDetector detector = new ArucoDetector(dictionary, parameters); + + detector.detectMarkers(grayImgMat, corners, ids); + + if (!ids.empty()) { + int[] idArray = ids.toArray(); + double relativeHeight = Movement.getInstance().getElevation(); + boolean has6 = false; + + for (int id : idArray) { + if (id == 6) { + has6 = true; + break; + } + } + + if (has6 && relativeHeight < 20) { + LogUtil.log(TAG_LOG, "【强制刹停】高度=" + relativeHeight + ",检测到6号,满足条件"); + FlightManager.forceTriggerDetection = true; + } else { + LogUtil.log(TAG_LOG, "【强制刹停】不满足条件:高度=" + relativeHeight + ", 6号=" + has6); + } + } + + yuvMat.release(); + grayImgMat.release(); + ids.release(); + } + + public void detectArucoTags(int height, int width, byte[] data, Dictionary dictionary) { + if (Synchronizedstatus.isAprongim()) return; isTriggerSuccess = true; Movement.getInstance().setVirtualStickEnableReason(2); @@ -149,12 +204,12 @@ public class Aprondown { return; } - // ========== 【关键】2帧一处理:0处理-1跳过-2处理-3跳过 ========== + int currentFrame = frameCounter++; boolean shouldProcess = (currentFrame % 2 == 0); // 偶数帧处理,奇数帧跳过 if (!shouldProcess) { - // 【关键】跳过的帧:啥也不干,直接return,让飞机自己飘 + // 让飞机自己飘 LogUtil.log(TAG_LOG, "【跳过帧】" + currentFrame + " 让飞机自稳"); return; } @@ -168,7 +223,7 @@ public class Aprondown { lastFuture.cancel(true); } - // 动态偏移(原有) + // 动态偏移 int ultHeight = Movement.getInstance().getUltrasonicHeight(); if (ultHeight >= 30) { LENS_OFFSET_X = 0; LENS_OFFSET_Y = 0; } else if (ultHeight >= 20) { LENS_OFFSET_X = 0; LENS_OFFSET_Y = 0; } @@ -176,7 +231,7 @@ public class Aprondown { else if (ultHeight >= 5) { LENS_OFFSET_X = 30; LENS_OFFSET_Y = 20; } else { LENS_OFFSET_X = 55; LENS_OFFSET_Y = 35; } - // ========== 【核心逻辑】先预处理检测,失败回退原图 ========== + lastFuture = executor.schedule(new Runnable() { @Override public void run() { @@ -195,12 +250,12 @@ public class Aprondown { DetectorParameters parameters = new DetectorParameters(); ArucoDetector detector = new ArucoDetector(dictionary, parameters); - // ========== 【关键修改】第1次:预处理图检测 ========== + Mat processedMat = fixedPreprocess(grayImgMat); detector.detectMarkers(processedMat, corners, ids); LogUtil.log(TAG_LOG, "预处理图检测: " + (ids.empty() ? "未检出" : "成功,数量=" + corners.size())); - // ========== 【关键修改】第2次:如果失败,回退原图检测 ========== + if (ids.empty()) { LogUtil.log(TAG_LOG, "预处理失败,回退原图检测"); corners.clear(); // 清空之前的结果 @@ -208,17 +263,17 @@ public class Aprondown { LogUtil.log(TAG_LOG, "原图检测: " + (ids.empty() ? "仍未检出" : "成功,数量=" + corners.size())); } - // 释放预处理图(必须!避免内存泄漏) + processedMat.release(); - // 只保留6号码 + ids = keepOnly6(ids, corners); boolean marker6Found = false; int ultrasonicHeight = Movement.getInstance().getUltrasonicHeight(); - // ========== 【新增】高度稳定性监测 ========== + if (ultrasonicHeight > 0) { if (!isHeightStableMonitoring) { // 开始监测 diff --git a/app/src/main/java/com/aros/apron/mix/Aprongim.java b/app/src/main/java/com/aros/apron/mix/Aprongim.java index d7710e74..4fefcd04 100644 --- a/app/src/main/java/com/aros/apron/mix/Aprongim.java +++ b/app/src/main/java/com/aros/apron/mix/Aprongim.java @@ -6,7 +6,9 @@ import android.os.Looper; import com.aros.apron.constant.AMSConfig; import com.aros.apron.entity.ArucoMarker; import com.aros.apron.entity.Movement; +import com.aros.apron.entity.Synchronizedstatus; import com.aros.apron.manager.AlternateLandingManager; +import com.aros.apron.manager.FlightManager; import com.aros.apron.tools.DroneHelper; import com.aros.apron.tools.LogUtil; import com.aros.apron.tools.PIDControl; @@ -115,6 +117,22 @@ public class Aprongim { public void setStartAruco(boolean startAruco) { isStartAruco = startAruco; } + + /** + * 切换相机时停止检测 + */ + public void stopDetection() { + if (lastFuture != null && !lastFuture.isDone()) { + lastFuture.cancel(true); + } + isStartAruco = false; + startFastStick = false; + arucoNotFoundTag = false; + isHeightStableMonitoring = false; + frameCounter = 0; + LogUtil.log(TAG_LOG, "【停止检测】云台相机模块已停止"); + } + public void setCanLanding(boolean v) { startTime = 0; endTime = 0; @@ -155,8 +173,50 @@ public class Aprongim { return 0.0; } } + + public void detectForceTriggerTags(int height, int width, byte[] data, Dictionary dictionary) { + Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1); + yuvMat.put(0, 0, data); + Mat grayImgMat = yuvMat.submat(0, height, 0, width); + grayImgMat = grayImgMat.clone(); + + MatOfInt ids = new MatOfInt(); + List corners = new ArrayList<>(); + DetectorParameters parameters = new DetectorParameters(); + ArucoDetector detector = new ArucoDetector(dictionary, parameters); + + detector.detectMarkers(grayImgMat, corners, ids); + + if (!ids.empty()) { + int ultHeight = Movement.getInstance().getUltrasonicHeight(); + int[] idArray = ids.toArray(); + boolean has6 = false; + int count1to4 = 0; + + for (int id : idArray) { + if (id == 6) { + has6 = true; + } else if (id >= 1 && id <= 4) { + count1to4++; + } + } + + if (has6 && count1to4 >= 3 && ultHeight <= 60) { + LogUtil.log(TAG_LOG, "【强制刹停】满足条件:6号 + 1-4号中" + count1to4 + "个 高度=" + ultHeight + "dm"); + FlightManager.forceTriggerDetection = true; + } else { + LogUtil.log(TAG_LOG, "【强制刹停】不满足条件:6号=" + has6 + ", 1-4号数量=" + count1to4 + ", 高度=" + ultHeight + "dm(阈值80)"); + } + } + + yuvMat.release(); + grayImgMat.release(); + ids.release(); + } + // ========== 【核心】主入口:3帧一处理 ========== public void detectArucoTags(int height, int width, byte[] data, Dictionary dictionary) { + if (!Synchronizedstatus.isAprongim()) return; isTriggerSuccess = true; Movement.getInstance().setVirtualStickEnableReason(2); @@ -245,12 +305,12 @@ public class Aprongim { // 检查高度变化 long currentTime = System.currentTimeMillis(); double heightChange = Math.abs(ultHeight - lastUltrasonicHeight); - + if (heightChange > HEIGHT_CHANGE_THRESHOLD) { // 高度有明显变化,重置计时器 lastHeightCheckTime = currentTime; lastUltrasonicHeight = ultHeight; - // LogUtil.log(TAG_LOG, "高度变化: " + heightChange + " 分米,重置计时器"); + // LogUtil.log(TAG_LOG, "高度变化: " + heightChange + " 分米,重置计时器"); } else if (currentTime - lastHeightCheckTime > HEIGHT_STABLE_THRESHOLD) { // 45秒内高度没有变动,执行拉高切换 LogUtil.log(TAG_LOG, "45秒内高度未变动,执行拉高切换到下视觉"); @@ -262,11 +322,11 @@ public class Aprongim { Movement.getInstance().setAlternate(true); return; } - + // 切换到下视觉降落 Apronmixvalue.getInstance().switchthemmodefpv(); Apronmixvalue.getInstance().pullup(); - + // 重置监测状态 isHeightStableMonitoring = false; return; diff --git a/app/src/main/java/com/aros/apron/mix/Apronmixvalue.java b/app/src/main/java/com/aros/apron/mix/Apronmixvalue.java index 0e8fd143..74332a79 100644 --- a/app/src/main/java/com/aros/apron/mix/Apronmixvalue.java +++ b/app/src/main/java/com/aros/apron/mix/Apronmixvalue.java @@ -33,9 +33,11 @@ public class Apronmixvalue { public void switchthemmodefpv(){ Synchronizedstatus.setAprongim(false); + Aprongim.getInstance().stopDetection(); } public void switchthemmodeGime(){ Synchronizedstatus.setAprongim(true); + Aprondown.getInstance().stopDetection(); } public boolean isIsaglinetrue() { @@ -57,13 +59,13 @@ public class Apronmixvalue { Synchronizedstatus.setSwitchtime(false); Aprongim.getInstance().setDropTimes(0); Aprondown.getInstance().setDropTimes(0); - + // 如果高度已经大于 40 分米,直接切换,不用上升 if (Movement.getInstance().getUltrasonicHeight() >= 40) { Synchronizedstatus.setSwitchtime(true); return; } - + Runnable runnable = new Runnable() { @Override public void run() { diff --git a/app/src/main/java/com/aros/apron/tools/ApronArucoDetect.java b/app/src/main/java/com/aros/apron/tools/ApronArucoDetect.java index 08b1ced0..93017d33 100644 --- a/app/src/main/java/com/aros/apron/tools/ApronArucoDetect.java +++ b/app/src/main/java/com/aros/apron/tools/ApronArucoDetect.java @@ -1,14 +1,16 @@ package com.aros.apron.tools; +import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.text.TextUtils; import com.aros.apron.constant.AMSConfig; import com.aros.apron.entity.ArucoMarker; import com.aros.apron.entity.Movement; import com.aros.apron.manager.AlternateLandingManager; +import com.aros.apron.manager.FlightManager; -import org.opencv.calib3d.Calib3d; import org.opencv.core.Core; import org.opencv.core.CvType; import org.opencv.core.Mat; @@ -17,11 +19,13 @@ import org.opencv.core.MatOfPoint2f; import org.opencv.core.Point; import org.opencv.core.Scalar; import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; import org.opencv.imgproc.Imgproc; import org.opencv.objdetect.ArucoDetector; import org.opencv.objdetect.DetectorParameters; import org.opencv.objdetect.Dictionary; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executors; @@ -93,6 +97,19 @@ public class ApronArucoDetect { private static final double HEIGHT_CHANGE_THRESHOLD =1; // 10厘米 private boolean isHeightStableMonitoring = false; + // ========== 【新增】强光预处理动态回退 ========== + // 连续"检测到轮廓但无法解码"的次数 + private int undecodableFrameCount = 0; + private static final int UNDECODABLE_THRESHOLD = 5; // 连续5帧解码失败则跳过预处理 + private boolean forceSkipPreprocess = false; // 强制跳过预处理标志 + private int skipPreprocessFrameCount = 0; // 强制跳过预处理的持续帧数 + private static final int SKIP_PREPROCESS_DURATION = 30; // 强制跳过30帧后恢复尝试预处理 + + // ========== 【新增】识别失败截图保存 ========== + private boolean saveFailScreenshot = false; // 是否开启识别失败截图保存 + private int failScreenshotIndex = 0; // 截图文件序号 + private static final int MAX_SCREENSHOTS = 50; // 最多保存50张截图 + // ========== 原有方法 ========== public int getCheckThrowingErrorsTimes() { return checkThrowingErrorsTimes; } public void setCheckThrowingErrorsTimes(int v) { checkThrowingErrorsTimes = v; } @@ -100,6 +117,8 @@ public class ApronArucoDetect { public void setTriggerSuccess(boolean v) { isTriggerSuccess = v; } public boolean isDoublePayload() { return isDoublePayload; } public void setDoublePayload(boolean v) { isDoublePayload = v; } + public boolean isSaveFailScreenshot() { return saveFailScreenshot; } + public void setSaveFailScreenshot(boolean v) { saveFailScreenshot = v; } public boolean isStartFastStick() { return startFastStick; } public void setStartFastStick(boolean v) { startFastStick = v; } public boolean isCanLanding() { return canLanding; } @@ -174,10 +193,12 @@ public class ApronArucoDetect { try { Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1); yuvMat.put(0, 0, data); - Mat rgbMat = new Mat(); - Imgproc.cvtColor(yuvMat, rgbMat, Imgproc.COLOR_YUV2BGR_I420); - Mat grayImgMat = new Mat(); - Imgproc.cvtColor(rgbMat, grayImgMat, Imgproc.COLOR_BGR2GRAY); + + // ========== 【关键修改】直接用Y通道,跳过RGB转换 ========== + // YUV420的Y通道就是灰度图,质量比YUV→BGR→GRAY高很多 + // BGR转换需要插值UV通道,在低分辨率FPV上损失细节 + Mat grayImgMat = yuvMat.submat(0, height, 0, width); + grayImgMat = grayImgMat.clone(); // 克隆,避免和yuvMat共享数据 MatOfInt ids = new MatOfInt(); List corners = new ArrayList<>(); @@ -186,21 +207,67 @@ public class ApronArucoDetect { DetectorParameters parameters = new DetectorParameters(); ArucoDetector detector = new ArucoDetector(dictionary, parameters); - // ========== 【关键修改】第1次:预处理图检测 ========== - Mat processedMat = fixedPreprocess(grayImgMat); - detector.detectMarkers(processedMat, corners, ids); - LogUtil.log(TAG_LOG, "预处理图检测: " + (ids.empty() ? "未检出" : "成功,数量=" + corners.size())); + // ========== 【关键修改】计算亮度,决定预处理策略 ========== + Scalar meanValue = Core.mean(grayImgMat); + double currentBrightness = meanValue.val[0]; - // ========== 【关键修改】第2次:如果失败,回退原图检测 ========== - if (ids.empty()) { + // ========== 【关键修改】第1次:预处理图检测 ========== + boolean shouldUsePreprocess = !forceSkipPreprocess; + Mat processedMat; + boolean preprocessed = false; + + if (shouldUsePreprocess) { + processedMat = fixedPreprocess(grayImgMat, currentBrightness); + preprocessed = true; + } else { + processedMat = grayImgMat; + LogUtil.log(TAG_LOG, "【强光回退】连续解码失败,跳过预处理,使用原图(" + (int)currentBrightness + ")"); + } + + detector.detectMarkers(processedMat, corners, ids); + LogUtil.log(TAG_LOG, (preprocessed ? "预处理图检测" : "原图检测(回退)") + + ": " + (ids.empty() ? "未检出" : "成功,数量=" + corners.size())); + + // ========== 【关键修改】第2次:如果预处理失败,回退原图检测 ========== + if (ids.empty() && preprocessed) { LogUtil.log(TAG_LOG, "预处理失败,回退原图检测"); - corners.clear(); // 清空之前的结果 + corners.clear(); detector.detectMarkers(grayImgMat, corners, ids); LogUtil.log(TAG_LOG, "原图检测: " + (ids.empty() ? "仍未检出" : "成功,数量=" + corners.size())); } // 释放预处理图(必须!避免内存泄漏) - processedMat.release(); + if (preprocessed && processedMat != null) { + processedMat.release(); + } + + // 【新增】追踪连续解码失败帧 + if (ids.empty()) { + undecodableFrameCount++; + if (undecodableFrameCount >= UNDECODABLE_THRESHOLD && !forceSkipPreprocess) { + forceSkipPreprocess = true; + skipPreprocessFrameCount = 0; + LogUtil.log(TAG_LOG, "【强光检测】连续" + UNDECODABLE_THRESHOLD + "帧解码失败,强制跳过预处理" + + "(当前亮度=" + (int)currentBrightness + ")"); + } + } else { + undecodableFrameCount = 0; + } + + // 【新增】强制跳过预处理模式:持续一定帧数后恢复尝试 + if (forceSkipPreprocess) { + skipPreprocessFrameCount++; + if (skipPreprocessFrameCount >= SKIP_PREPROCESS_DURATION) { + forceSkipPreprocess = false; + undecodableFrameCount = 0; + LogUtil.log(TAG_LOG, "【强光检测】恢复预处理尝试,观察效果"); + } + } + + // 【新增】高亮度曝光补偿提示 + if (currentBrightness > 130) { + LogUtil.log(TAG_LOG, "【曝光提示】亮度过高(" + (int)currentBrightness + "),建议降低EV或锁定AE"); + } // 只保留6号码 ids = keepOnly6(ids, corners); @@ -273,9 +340,9 @@ public class ApronArucoDetect { double pixelWidth = calculateDistance(points[0], points[1]); double centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4.0 + LENS_OFFSET_X; - double errX = Math.abs(centerX - rgbMat.width() / 2.0); + double errX = Math.abs(centerX - width / 2.0); double centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4.0 + LENS_OFFSET_Y; - double errY = Math.abs(centerY - rgbMat.height() / 2.0); + double errY = Math.abs(centerY - height / 2.0); LogUtil.log(TAG_LOG, "像素" + (int) pixelWidth + " errX=" + (int) errX + " errY=" + (int) errY + " (含偏移" + LENS_OFFSET_X + ")"); @@ -318,13 +385,19 @@ public class ApronArucoDetect { // 原有修正逻辑 if (marker6Found) { - moveOnArucoDetected(new ArucoMarker(1, corner6, 0.24f), rgbMat.width(), rgbMat.height()); + moveOnArucoDetected(new ArucoMarker(1, corner6, 0.24f), width, height); } dropTimesTag = true; } else { // 原有丢失处理 LogUtil.log(TAG_LOG, "找不到了二维码"); + + // 【新增】识别失败截图保存 + if (saveFailScreenshot) { + saveFailScreenshot(grayImgMat, width, height); + + } if (!arucoNotFoundTag) { startTime = System.currentTimeMillis(); arucoNotFoundTag = true; @@ -359,7 +432,6 @@ public class ApronArucoDetect { // 释放资源 grayImgMat.release(); - rgbMat.release(); yuvMat.release(); ids.release(); @@ -395,13 +467,108 @@ public class ApronArucoDetect { return new MatOfInt(newIds); } - private Mat fixedPreprocess(Mat src) { + /** + * 识别失败时保存原图截图到外部存储 + */ + private void saveFailScreenshot(Mat grayMat, int width, int height) { + if (failScreenshotIndex >= MAX_SCREENSHOTS) { + LogUtil.log(TAG_LOG, "【截图保存】已达到最大数量 " + MAX_SCREENSHOTS + ",停止保存"); + return; + } + + try { + String dirPath = getScreenshotDirPath(); + if (dirPath == null) { + LogUtil.log(TAG_LOG, "【截图保存】无法获取存储目录"); + return; + } + + String fileName = String.format("aruco_fail_%03d_%d.png", + failScreenshotIndex, System.currentTimeMillis()); + String fullPath = dirPath + "/" + fileName; + + boolean success = Imgcodecs.imwrite(fullPath, grayMat); + if (success) { + failScreenshotIndex++; + LogUtil.log(TAG_LOG, "【截图保存】已保存: " + fileName + + " (亮度=" + (int) Core.mean(grayMat).val[0] + ")"); + } else { + LogUtil.log(TAG_LOG, "【截图保存】保存失败: " + fullPath); + } + } catch (Exception e) { + LogUtil.log(TAG_LOG, "【截图保存】异常: " + e.getMessage()); + } + } + + /** + * 获取截图保存目录 + */ + private String getScreenshotDirPath() { + File storageDir; + if (checkSDCard()) { + storageDir = new File(Environment.getExternalStorageDirectory(), "apronPic/aruco_fail_screenshots"); + } else { + storageDir = new File(Environment.getExternalStorageDirectory().getParentFile(), "apronPic/aruco_fail_screenshots"); + } + + if (!storageDir.exists()) { + boolean created = storageDir.mkdirs(); + if (!created) { + LogUtil.log(TAG_LOG, "【截图保存】创建目录失败: " + storageDir.getAbsolutePath()); + return null; + } + } + return storageDir.getAbsolutePath(); + } + + private boolean checkSDCard() { + return TextUtils.equals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState()); + } + + /** + * startArucoType == 3 专用:扫到6号 + 相对高度小于20才触发刹停 + */ + public void detectForceTriggerTags(int height, int width, byte[] data, Dictionary dictionary) { + Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1); + yuvMat.put(0, 0, data); + Mat grayImgMat = yuvMat.submat(0, height, 0, width); + grayImgMat = grayImgMat.clone(); + + MatOfInt ids = new MatOfInt(); + List corners = new ArrayList<>(); + DetectorParameters parameters = new DetectorParameters(); + ArucoDetector detector = new ArucoDetector(dictionary, parameters); + + detector.detectMarkers(grayImgMat, corners, ids); + + if (!ids.empty()) { + int[] idArray = ids.toArray(); + double relativeHeight = Movement.getInstance().getElevation(); + boolean has6 = false; + + for (int id : idArray) { + if (id == 6) { + has6 = true; + break; + } + } + + if (has6 && relativeHeight < 20) { + LogUtil.log(TAG_LOG, "【强制刹停】高度=" + relativeHeight + ",检测到6号,满足条件"); + FlightManager.forceTriggerDetection = true; + } else { + LogUtil.log(TAG_LOG, "【强制刹停】不满足条件:高度=" + relativeHeight + ", 6号=" + has6); + } + } + + yuvMat.release(); + grayImgMat.release(); + ids.release(); + } + + private Mat fixedPreprocess(Mat src, double meanBrightness) { Mat result = new Mat(); try { - // 【新增】判断亮度,区分白天/夜景 - Scalar meanValue = Core.mean(src); - double meanBrightness = meanValue.val[0]; - if (meanBrightness < 80) { // 夜景模式(FPV晚上通常<80) LogUtil.log(TAG_LOG, "【夜景预处理】亮度=" + (int)meanBrightness); @@ -442,34 +609,58 @@ public class ApronArucoDetect { return result; } - // 【原逻辑】白天模式(亮度>=80) + // ========== 【关键修改】白天模式:分亮度区间处理 ========== LogUtil.log(TAG_LOG, "【白天预处理】亮度=" + (int)meanBrightness); - // Step 1: 双边滤波(保边缘降噪) + // Step 1: 双边滤波(保边缘降噪)— 所有白天场景通用 Mat filtered = new Mat(); Imgproc.bilateralFilter(src, filtered, 9, 75, 75); - // Step 2: CLAHE 局部对比度增强 - Mat clahe = new Mat(); - Imgproc.createCLAHE(4.0, new Size(8, 8)).apply(filtered, clahe); + Mat clahe; + double amount; + + if (meanBrightness > 130) { + // 【强光模式】亮度>130:大幅降低CLAHE,避免眩光被增强成伪marker + LogUtil.log(TAG_LOG, "【强光处理】亮度过高,降低CLAHE增益,跳过锐化"); + clahe = new Mat(); + // clipLimit降到1.5(默认4.0),tileSize加大到16x16(更平滑) + Imgproc.createCLAHE(1.5, new Size(16, 16)).apply(filtered, clahe); + amount = 0; // 强光下跳过锐化,避免放大眩光边缘 + } else if (meanBrightness > 100) { + // 【中亮模式】亮度100-130:适度CLAHE,轻度锐化 + LogUtil.log(TAG_LOG, "【中亮处理】适度CLAHE+轻度锐化"); + clahe = new Mat(); + Imgproc.createCLAHE(2.5, new Size(8, 8)).apply(filtered, clahe); + amount = 0.8; + } else { + // 【正常白天】亮度80-100:原逻辑 + clahe = new Mat(); + Imgproc.createCLAHE(4.0, new Size(8, 8)).apply(filtered, clahe); + amount = 1.2; + } + filtered.release(); - // Step 3: Unsharp Mask(反锐化掩膜) - Mat blurred = new Mat(); - Imgproc.GaussianBlur(clahe, blurred, new Size(0, 0), 2.0); + // Step 3: Unsharp Mask(反锐化掩膜)— 强光模式跳过 + if (amount > 0) { + Mat blurred = new Mat(); + Imgproc.GaussianBlur(clahe, blurred, new Size(0, 0), 2.0); - Mat detail = new Mat(); - Core.subtract(clahe, blurred, detail); + Mat detail = new Mat(); + Core.subtract(clahe, blurred, detail); - double amount = 1.2; // 白天可以用1.2 - Core.multiply(detail, new Scalar(amount), detail); - Core.add(clahe, detail, result); + Core.multiply(detail, new Scalar(amount), detail); + Core.add(clahe, detail, result); + + blurred.release(); + detail.release(); + } else { + clahe.copyTo(result); + } Core.min(result, new Scalar(255), result); Core.max(result, new Scalar(0), result); - blurred.release(); - detail.release(); clahe.release(); LogUtil.log(TAG_LOG, "白天处理完成:双边滤波+CLAHE+UnsharpMask(amount=" + amount + ")"); @@ -497,6 +688,13 @@ public class ApronArucoDetect { isHeightStableMonitoring = false; lastHeightCheckTime = 0; lastUltrasonicHeight = 0; + // 【新增】重置强光回退状态 + undecodableFrameCount = 0; + forceSkipPreprocess = false; + skipPreprocessFrameCount = 0; + // 【新增】重置截图保存状态 + saveFailScreenshot = false; + failScreenshotIndex = 0; } diff --git a/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java b/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java index 793d6aac..3baf5f0d 100644 --- a/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java +++ b/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java @@ -1,12 +1,15 @@ package com.aros.apron.tools; +import android.os.Environment; import android.os.Handler; import android.os.Looper; +import android.text.TextUtils; import com.aros.apron.constant.AMSConfig; import com.aros.apron.entity.ArucoMarker; import com.aros.apron.entity.Movement; import com.aros.apron.manager.AlternateLandingManager; +import com.aros.apron.manager.FlightManager; import org.opencv.core.CvType; import org.opencv.core.Mat; @@ -43,8 +46,6 @@ public class ApronArucoDetectPort { private static final float SLOW_LAND_SPEED = -0.3f; private static final float SLOW_SUPER_SPEED = -0.15f; private static final int PIXEL_TRIGGER = 360; - private static final int TRIGGER_FRAME_THRESHOLD = 2; - // ========== 原有状态 ========== private boolean isTriggerSuccess; private boolean arucoNotFoundTag = false; @@ -64,7 +65,6 @@ public class ApronArucoDetectPort { public PIDControl pidControlX = null; public PIDControl pidControlY = null; public int checkThrowingErrorsTimes; - private int consecutiveTriggerCount = 0; private int frameCounter = 0; // ========== 【新增】阶段控制标志 ========== @@ -73,9 +73,9 @@ public class ApronArucoDetectPort { // 当前激活的降落模式:0=旋转阶段(6号), 1=小码, 2=1-4大码, 3=6号纯下降 private int currentLandingMode = 0; - // ========== 【新增】连续降落条件判断 ========== - // 记录上一帧满足的降落条件类型:0=无, 1=单码精准降落(15/17号), 2=几何中心降落 - private int lastLandingCondition = 0; + // ========== 【新增】6个独立分支计数器 ========== + private int[] landingCounters = new int[22]; // ID作为索引 + private int counterRound = 0; // ========== 【新增】高度稳定性监测 ========== private long lastHeightCheckTime = 0; @@ -177,19 +177,19 @@ public class ApronArucoDetectPort { if(isDoublePayload){ if (ultHeight <=5) { - LENS_OFFSET_X=-130; + LENS_OFFSET_X=-180; LENS_OFFSET_Y=120; }else if(ultHeight<10){ - LENS_OFFSET_X=-90; + LENS_OFFSET_X=-120; LENS_OFFSET_Y=80; } else if (ultHeight<20) { - LENS_OFFSET_X=-70; + LENS_OFFSET_X=-80; LENS_OFFSET_Y=60; } }else{ if (ultHeight <=5) { - LENS_OFFSET_X=120; + LENS_OFFSET_X=100; LENS_OFFSET_Y = 100; }else if(ultHeight<10){ LENS_OFFSET_X=80; @@ -207,10 +207,9 @@ public class ApronArucoDetectPort { // ========== 图像预处理(原有) ========== Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1); yuvMat.put(0, 0, data); - Mat rgbMat = new Mat(); - Imgproc.cvtColor(yuvMat, rgbMat, Imgproc.COLOR_YUV2BGR_I420); - Mat grayImgMat = new Mat(); - Imgproc.cvtColor(rgbMat, grayImgMat, Imgproc.COLOR_BGR2GRAY); + // ========== 【关键修改】直接用Y通道,跳过RGB转换 ========== + Mat grayImgMat = yuvMat.submat(0, height, 0, width); + grayImgMat = grayImgMat.clone(); MatOfInt ids = new MatOfInt(); List corners = new ArrayList<>(); @@ -317,24 +316,24 @@ public class ApronArucoDetectPort { // 只要看到6号且还没对准,就只用6号旋转,不进入其他任何阶段 if (!bigMarker6.isEmpty() && !isYawAligned) { currentLandingMode = 0; - processMarker6YawOnly(bigMarker6.get(0), rgbMat.width(), rgbMat.height(), ultHeight); + processMarker6YawOnly(bigMarker6.get(0), width, height, ultHeight); } // ========== 【关键】只有旋转完成后(isYawAligned=true),才识别其他码 ========== else if (isYawAligned) { // 优先级1:小码模式(高度<25dm且≥3个码) if (!smallMarkers.isEmpty() && ultHeight < 25) { currentLandingMode = 1; - processSmallMarkers(smallMarkers, rgbMat.width(), rgbMat.height(), ultHeight); + processSmallMarkers(smallMarkers, width, height, ultHeight); } // 优先级2:1-4号几何中心下降 else if (!bigMarkers1_4.isEmpty()) { currentLandingMode = 2; - processBigMarkersCenter(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight); + processBigMarkersCenter(bigMarkers1_4, width, height, ultHeight); } // 优先级3:6号纯下降(旋转完成后,没看到1-4和小码时) else if (!bigMarker6.isEmpty()) { currentLandingMode = 3; - processLandingOnly(bigMarker6.get(0), rgbMat.width(), rgbMat.height(), ultHeight); + processLandingOnly(bigMarker6.get(0), width, height, ultHeight); } // 旋转完成后丢失其他码,但还有6号:继续用6号下降 else { @@ -350,7 +349,7 @@ public class ApronArucoDetectPort { handleLostMarker(ultHeight); } grayImgMat.release(); - rgbMat.release(); + grayImgMat.release(); yuvMat.release(); ids.release(); if (!corners.isEmpty()) { @@ -459,92 +458,209 @@ public class ApronArucoDetectPort { double absY = Math.abs(offsetY); double z = ultHeight / 10.0; - // 降落条件判断(连续帧) + // ========== 【6个独立分支计数器降落判定】 ========== boolean isSpecialLanding = false; double singleErrX = 0, singleErrY = 0; - // 单挂模式:检查15号 - if (!isDoublePayload && target15 != null && ultHeight <= 4) { - Mat corner = target15.getConner(); - double cx = 0, cy = 0; - for (int i = 0; i < 4; i++) { - double[] p = corner.get(0, i); - cx += p[0]; - cy += p[1]; + if (ultHeight <= 4) { + // 当前帧可见码的Map + java.util.HashMap markerMap = new java.util.HashMap<>(); + for (ArucoMarker m : markers) { + markerMap.put(m.getId(), m); } - cx /= 4.0; cy /= 4.0; - singleErrX = cx - imgWidth / 2.0 - LENS_OFFSET_X; - singleErrY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; - if (Math.abs(singleErrX) < 60 && Math.abs(singleErrY) < 60) { - isSpecialLanding = true; - offsetX = singleErrX; - offsetY = singleErrY; - absX = Math.abs(singleErrX); - absY = Math.abs(singleErrY); + boolean foundLanding = false; + int landingId = 0; + + // ===== 单挂模式:13 15 17 14 16 18 ===== + if (!isDoublePayload) { + // ---- 13号 ---- + ArucoMarker marker13 = markerMap.get(13); + if (marker13 != null) { + Mat corner = marker13.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 + 200.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 55 && Math.abs(errY) < 70) { landingCounters[13]++; } else { landingCounters[13] = 0; } + if (landingCounters[13] >= 2 && !foundLanding) { foundLanding = true; landingId = 13; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[13] = 0; } + + // ---- 15号 ---- + ArucoMarker marker15 = markerMap.get(15); + if (marker15 != null) { + Mat corner = marker15.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - LENS_OFFSET_X; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 70 && Math.abs(errY) < 70) { landingCounters[15]++; } else { landingCounters[15] = 0; } + if (landingCounters[15] >= 2 && !foundLanding) { foundLanding = true; landingId = 15; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[15] = 0; } + + // ---- 17号 ---- + ArucoMarker marker17 = markerMap.get(17); + if (marker17 != null) { + Mat corner = marker17.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - 200.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 55 && Math.abs(errY) < 70) { landingCounters[17]++; } else { landingCounters[17] = 0; } + if (landingCounters[17] >= 2 && !foundLanding) { foundLanding = true; landingId = 17; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[17] = 0; } + + // ---- 14号 ---- + ArucoMarker marker14 = markerMap.get(14); + if (marker14 != null) { + Mat corner = marker14.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 + 200.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 70 && Math.abs(errY) < 70) { landingCounters[14]++; } else { landingCounters[14] = 0; } + if (landingCounters[14] >= 2 && !foundLanding) { foundLanding = true; landingId = 14; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[14] = 0; } + + // ---- 16号 ---- + ArucoMarker marker16 = markerMap.get(16); + if (marker16 != null) { + Mat corner = marker16.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - LENS_OFFSET_X; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 70 && Math.abs(errY) < 70) { landingCounters[16]++; } else { landingCounters[16] = 0; } + if (landingCounters[16] >= 2 && !foundLanding) { foundLanding = true; landingId = 16; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[16] = 0; } + + // ---- 18号 ---- + ArucoMarker marker18 = markerMap.get(18); + if (marker18 != null) { + Mat corner = marker18.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - 200.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 55 && Math.abs(errY) < 70) { landingCounters[18]++; } else { landingCounters[18] = 0; } + if (landingCounters[18] >= 2 && !foundLanding) { foundLanding = true; landingId = 18; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[18] = 0; } } - } + // ===== 双挂模式:15 17 19 16 18 20 ===== + else { + // ---- 15号 ---- + ArucoMarker marker15d = markerMap.get(15); + if (marker15d != null) { + Mat corner = marker15d.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 +220.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 55 && Math.abs(errY) < 70) { landingCounters[15]++; } else { landingCounters[15] = 0; } + if (landingCounters[15] >= 2 && !foundLanding) { foundLanding = true; landingId = 15; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[15] = 0; } - // 双挂模式:检查17号 - if (isDoublePayload && target17 != null && ultHeight <= 4) { - Mat corner = target17.getConner(); - double cx = 0, cy = 0; - for (int i = 0; i < 4; i++) { - double[] p = corner.get(0, i); - cx += p[0]; - cy += p[1]; + // ---- 17号 ---- + ArucoMarker marker17d = markerMap.get(17); + if (marker17d != null) { + Mat corner = marker17d.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - LENS_OFFSET_X; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 70 && Math.abs(errY) < 70) { landingCounters[17]++; } else { landingCounters[17] = 0; } + if (landingCounters[17] >= 2 && !foundLanding) { foundLanding = true; landingId = 17; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[17] = 0; } + + // ---- 19号 ---- + ArucoMarker marker19d = markerMap.get(19); + if (marker19d != null) { + Mat corner = marker19d.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - 200.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 55 && Math.abs(errY) < 70) { landingCounters[19]++; } else { landingCounters[19] = 0; } + if (landingCounters[19] >= 2 && !foundLanding) { foundLanding = true; landingId = 19; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[19] = 0; } + + // ---- 16号 ---- + ArucoMarker marker16d = markerMap.get(16); + if (marker16d != null) { + Mat corner = marker16d.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 +220.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 70 && Math.abs(errY) < 70) { landingCounters[16]++; } else { landingCounters[16] = 0; } + if (landingCounters[16] >= 2 && !foundLanding) { foundLanding = true; landingId = 16; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[16] = 0; } + + // ---- 18号 ---- + ArucoMarker marker18d = markerMap.get(18); + if (marker18d != null) { + Mat corner = marker18d.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - LENS_OFFSET_X; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 70 && Math.abs(errY) < 70) { landingCounters[18]++; } else { landingCounters[18] = 0; } + if (landingCounters[18] >= 2 && !foundLanding) { foundLanding = true; landingId = 18; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[18] = 0; } + + // ---- 20号 ---- + ArucoMarker marker20d = markerMap.get(20); + if (marker20d != null) { + Mat corner = marker20d.getConner(); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { double[] p = corner.get(0, i); cx += p[0]; cy += p[1]; } + cx /= 4.0; cy /= 4.0; + double errX = cx - imgWidth / 2.0 - 200.0; + double errY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; + if (Math.abs(errX) < 55 && Math.abs(errY) < 70) { landingCounters[20]++; } else { landingCounters[20] = 0; } + if (landingCounters[20] >= 2 && !foundLanding) { foundLanding = true; landingId = 20; isSpecialLanding = true; singleErrX = errX; singleErrY = errY; offsetX = singleErrX; offsetY = singleErrY; absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); } + } else { landingCounters[20] = 0; } } - cx /= 4.0; cy /= 4.0; - singleErrX = cx - imgWidth / 2.0 - LENS_OFFSET_X; - singleErrY = cy - imgHeight / 2.0 + LENS_OFFSET_Y; - if (Math.abs(singleErrX) < 60 && Math.abs(singleErrY) < 60) { - isSpecialLanding = true; - offsetX = singleErrX; - offsetY = singleErrY; - absX = Math.abs(singleErrX); - absY = Math.abs(singleErrY); - } - } + LogUtil.log(TAG_LOG, "【计数器】13=" + landingCounters[13] + " 14=" + landingCounters[14] + " 15=" + landingCounters[15] + " 16=" + landingCounters[16] + " 17=" + landingCounters[17] + " 18=" + landingCounters[18]+ " 19=" + landingCounters[19]+ " 20=" + landingCounters[20]); - // 连续帧判断 - int currentCondition = 0; - if (isSpecialLanding) { - currentCondition = 1; - } else if (ultHeight <= 4 && absX < 60 && absY < 60 && validCount >= 8) { - currentCondition = 2; - } - - if (currentCondition == 0 || currentCondition != lastLandingCondition) { - if (consecutiveTriggerCount > 0) { - consecutiveTriggerCount = 0; - LogUtil.log(TAG_LOG, "【计数器清零】条件变化 last=" + lastLandingCondition + " curr=" + currentCondition); - } - lastLandingCondition = currentCondition; - } - - if (currentCondition > 0) { - consecutiveTriggerCount++; - if (consecutiveTriggerCount >= TRIGGER_FRAME_THRESHOLD) { + // 触发降落 + if (foundLanding) { + LogUtil.log(TAG_LOG, "【降落触发】ID=" + landingId + " 连续2帧满足条件"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 0f); startFastStick = true; - consecutiveTriggerCount = 0; - lastLandingCondition = 0; - - LogUtil.log(TAG_LOG, currentCondition == 1 ? - "【降落触发】单双挂精准条件满足" : - "【降落触发】几何中心对准,码数量=" + validCount); - + for (int i = 0; i < landingCounters.length; i++) { landingCounters[i] = 0; } handler.postDelayed(() -> handler.post(runnable), 300); return; } + + // 第二帧结束,没有触发 → 重置 + counterRound++; + if (counterRound >= 2) { + counterRound = 0; + for (int i = 0; i < landingCounters.length; i++) { landingCounters[i] = 0; } + LogUtil.log(TAG_LOG, "【计数器重置】2帧未触发,重新从0开始"); + } } + double outX = 0, outY = 0, outZ = 0; // 分段PID控制(原有逻辑保持不变) if(z <= 0.4){ - pidControlX.setInputFilterAll((float)offsetX/1750); - pidControlY.setInputFilterAll(-(float)offsetY/1750); + pidControlX.setInputFilterAll((float)offsetX/1800); + pidControlY.setInputFilterAll(-(float)offsetY/1800); if (pidControlX.get_pid()<0){ if (pidControlX.get_pid()<-0.125){ outX=absX<120?0:-0.125; @@ -691,7 +807,7 @@ public class ApronArucoDetectPort { outY = absY < 50 ? 0 : pidControlY.get_pid(); outX = Math.max(-0.20, Math.min(0.20, outX)); outY = Math.max(-0.20, Math.min(0.20, outY)); - outZ = (absX < 100 && absY < 100) ? -0.4 : 0; + outZ = (absX < 100 && absY < 100) ? -0.8 : 0; }else if(z>=4.0){ pidControlX.setInputFilterAll((float)offsetX/600); pidControlY.setInputFilterAll(-(float)offsetY/600); @@ -748,7 +864,7 @@ public class ApronArucoDetectPort { float vz; if (ultHeight >= 50) { - vz = -0.7f; + vz = -0.8f; } else if(ultHeight<=20){ vz = 0.0f; }else{ @@ -839,19 +955,59 @@ public class ApronArucoDetectPort { return Math.sqrt(dx * dx + dy * dy); } + + public void detectForceTriggerTags(int height, int width, byte[] data, Dictionary dictionary) { + Mat yuvMat = new Mat(height + height / 2, width, CvType.CV_8UC1); + yuvMat.put(0, 0, data); + Mat grayImgMat = yuvMat.submat(0, height, 0, width); + grayImgMat = grayImgMat.clone(); + + MatOfInt ids = new MatOfInt(); + List corners = new ArrayList<>(); + DetectorParameters parameters = new DetectorParameters(); + ArucoDetector detector = new ArucoDetector(dictionary, parameters); + + detector.detectMarkers(grayImgMat, corners, ids); + + if (!ids.empty()) { + int ultHeight = Movement.getInstance().getUltrasonicHeight(); + int[] idArray = ids.toArray(); + boolean has6 = false; + int count1to4 = 0; + + for (int id : idArray) { + if (id == 6) { + has6 = true; + } else if (id >= 1 && id <= 4) { + count1to4++; + } + } + + if (has6 && count1to4 >= 3 && ultHeight <= 60) { + LogUtil.log(TAG_LOG, "【强制刹停】满足条件:6号 + 1-4号中" + count1to4 + "个 高度=" + ultHeight + "dm"); + FlightManager.forceTriggerDetection = true; + } else { + LogUtil.log(TAG_LOG, "【强制刹停】不满足条件:6号=" + has6 + ", 1-4号数量=" + count1to4 + ", 高度=" + ultHeight + "dm(阈值80)"); + } + } + + yuvMat.release(); + grayImgMat.release(); + ids.release(); + } + public void setDetectedBigMarkers() { startFastStick = false; isStartAruco = false; - consecutiveTriggerCount = 0; frameCounter = 0; // 【关键】重置旋转标志,下次降落重新用6号旋转 isYawAligned = false; currentLandingMode = 0; - lastLandingCondition = 0; - // 【新增】重置高度稳定性监测状态 - isHeightStableMonitoring = false; - lastHeightCheckTime = 0; - lastUltrasonicHeight = 0; + // 重置分支计数器 + counterRound = 0; + for (int i = 0; i < landingCounters.length; i++) { + landingCounters[i] = 0; + } } private int handlerCallbackCount = 0; diff --git a/app/src/main/java/com/aros/apron/tools/MqttManager.java b/app/src/main/java/com/aros/apron/tools/MqttManager.java index b7a2de2e..226ae428 100644 --- a/app/src/main/java/com/aros/apron/tools/MqttManager.java +++ b/app/src/main/java/com/aros/apron/tools/MqttManager.java @@ -4,8 +4,12 @@ package com.aros.apron.tools; import android.annotation.SuppressLint; import android.content.Context; import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkInfo; +import android.os.Build; import android.os.Handler; +import android.os.Looper; import com.aros.apron.app.ApronApp; import com.aros.apron.callback.MqttActionCallBack; @@ -59,48 +63,74 @@ public class MqttManager { * 连接MQTT服务器 */ private void doClientConnection() { - if (!mqttAndroidClient.isConnected() && isConnectIsNomarl()) { - try { - mqttAndroidClient.connect(mMqttConnectOptions, null, new MqttActionCallBack(mqttAndroidClient,mMqttConnectOptions)); - } catch (MqttException e) { - LogUtil.log(TAG,"mqtt连接异常:"+e.toString()); - e.printStackTrace(); - } + if (mqttAndroidClient.isConnected()) { + return; } + // 先检查网络状态(仅用于日志和延迟策略,不阻塞连接尝试) + boolean networkAvailable = isConnectIsNomarl(); + if (networkAvailable) { + tryConnect(); + } else { + // 网络检查未通过时,仍尝试连接(防止网络误判导致永久断连) + LogUtil.log(TAG, "网络检查未通过,但仍尝试连接"); + tryConnect(); + } + } + private void tryConnect() { + if (mqttAndroidClient.isConnected()) { + return; + } + try { + mqttAndroidClient.connect(mMqttConnectOptions, null, new MqttActionCallBack(mqttAndroidClient, mMqttConnectOptions)); + } catch (MqttException e) { + LogUtil.log(TAG, "mqtt连接异常:" + e.toString()); + e.printStackTrace(); + } } /** - * 判断网络是否连接 + * 判断网络是否连接(仅做日志记录,不阻塞连接尝试) */ private boolean isConnectIsNomarl() { ConnectivityManager connectivityManager = (ConnectivityManager) ApronApp.Companion.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE); - @SuppressLint("MissingPermission") NetworkInfo info = connectivityManager.getActiveNetworkInfo(); - if (info != null && info.isAvailable()) { - String name = info.getTypeName(); - LogUtil.log(TAG, "当前网络名称:" + name); - return true; - } else { - LogUtil.log(TAG, "没有可用Mqtt网络,延迟三秒后重连"); - /*没有可用网络的时候,延迟3秒再尝试重连*/ - doConnectionDelay(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Network network = connectivityManager.getActiveNetwork(); + if (network != null) { + NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network); + if (capabilities != null) { + String transportInfo; + if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) { + transportInfo = "ETHERNET"; + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + transportInfo = "WIFI"; + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + transportInfo = "CELLULAR"; + } else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { + transportInfo = "VPN"; + } else { + transportInfo = "OTHER"; + } + LogUtil.log(TAG, "当前网络可用:" + transportInfo); + return true; + } + } + LogUtil.log(TAG, "没有可用Mqtt网络(API 23+)"); return false; + } else { + @SuppressLint("MissingPermission") NetworkInfo info = connectivityManager.getActiveNetworkInfo(); + if (info != null && info.isAvailable()) { + String name = info.getTypeName(); + LogUtil.log(TAG, "当前网络名称:" + name); + return true; + } else { + LogUtil.log(TAG, "没有可用Mqtt网络"); + return false; + } } } - /** - * 没有可用网络的时候,延迟5秒再尝试重连 - */ - private void doConnectionDelay() { - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - doClientConnection(); - } - }, 3000); - } - private String generateRandomString(int length) { String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; StringBuilder builder = new StringBuilder(); diff --git a/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java b/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java index 35269e06..ae3db2d0 100644 --- a/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java +++ b/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java @@ -27,6 +27,8 @@ public class SimplePortScanner { private static final long SCAN_INTERVAL = 3000L; private static final int CONNECT_TIMEOUT = 500; + private volatile boolean isScanning = false; // 防止重复启动扫描 + private OnPortCheckListener checkListener; // 主线程 Handler,用于切换回调到 UI 线程和定时扫描 private final Handler mainHandler = new Handler(Looper.getMainLooper()); @@ -51,11 +53,14 @@ public class SimplePortScanner { closedCount++; Log.i(TAG, "端口关闭次数:" + closedCount + "/" + MAX_CLOSED_COUNT); if (closedCount >= MAX_CLOSED_COUNT) { - Log.i(TAG, "端口连续关闭 " + closedCount + " 次,尝试重启 RTSP 推流"); - closedCount = 0; // 重置计数 - // 先重置状态,让 startLiveWithRTSP 能真正执行 + Log.i(TAG, "端口连续关闭 " + closedCount + " 次,停止扫描并重启推流"); + closedCount = 0; + isScanning = false; // 停止自身扫描 + mainHandler.removeCallbacks(scanRunnable); + StreamManager.getInstance().stopstream(); StreamManager.getInstance().resetStreamState(); StreamManager.getInstance().startLiveWithRTSP(); + return; // 阻止 postDelayed 再次循环 } if (checkListener != null) { checkListener.onPortClosed(); @@ -88,14 +93,24 @@ public class SimplePortScanner { } public void startScan() { - stopScan(); - closedCount = 0; // 重置计数 + if (isScanning) { + Log.i(TAG, "扫描已在运行,忽略重复调用"); + return; + } + isScanning = true; + closedCount = 0; + Log.i(TAG, "开始端口扫描"); // 立即执行一次,然后延迟 3 秒循环 mainHandler.post(scanRunnable); } public void stopScan() { + if (!isScanning) { + return; + } + isScanning = false; mainHandler.removeCallbacks(scanRunnable); + Log.i(TAG, "停止端口扫描"); } private boolean checkPort(String host, int port) { diff --git a/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java b/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java index adbb4bda..cae263dc 100644 --- a/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java +++ b/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java @@ -75,6 +75,28 @@ public class SpeakerProgressReporter { LogUtil.log(TAG, "停止音频进度上报"); } + /** + * 立即发送一次音频进度(不启动定时循环) + */ + public void sendNowAudioProgress(int psdkIndex) { + try { + sendAudioProgress(psdkIndex); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 立即发送一次TTS进度(不启动定时循环) + */ + public void sendNowTTSProgress(int psdkIndex) { + try { + sendTTSProgress(psdkIndex); + } catch (Exception e) { + e.printStackTrace(); + } + } + /** * 发送音频进度(使用 SpeakerAudioPlayProgress 实体类) */ diff --git a/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java b/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java index 0bc21af2..630a4aa0 100644 --- a/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java +++ b/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java @@ -1,5 +1,6 @@ package com.aros.apron.tools; +import com.aros.apron.entity.Movement; import com.aros.apron.entity.XTTSParams; import com.iflytek.aikit.core.AeeEvent; import com.iflytek.aikit.core.AiHandle; @@ -67,11 +68,18 @@ public class XTtsPcmGenerator { paramBuilder.param("speed", params.speed); paramBuilder.param("volume", params.volume); - // 构建监听器 + + Movement.getInstance().setTts_speed(String.valueOf(params.speed)); + Movement.getInstance().setTts_language("0"); + Movement.getInstance().setTts_type("0"); + Movement.getInstance().setTts_volume(String.valueOf(params.volume)); + + + AiListener listener = buildListener(); LogUtil.log(TAG, "监听器创建成功"); - // 注册监听器 + try { AiHelper.getInst().registerListener(ABILITY_ID, listener); LogUtil.log(TAG, "监听器注册成功"); @@ -82,7 +90,7 @@ public class XTtsPcmGenerator { return null; } - // 启动 TTS 引擎 + aiHandle = AiHelper.getInst().start(ABILITY_ID, paramBuilder.build(), null); if (aiHandle.getCode() != 0) { LogUtil.log(TAG, "启动 TTS 引擎失败: " + aiHandle.getCode()); @@ -91,7 +99,7 @@ public class XTtsPcmGenerator { } LogUtil.log(TAG, "TTS 引擎启动成功"); - // 构建请求 + AiText aiText = AiText.get("text").data(text).valid(); AiRequest request = AiRequest.builder().payload(aiText).build();