From 984dadb7bf1d28ac8e0532f259bba1b3d8f554f7 Mon Sep 17 00:00:00 2001 From: cxf <2417125293@qq.com> Date: Wed, 8 Apr 2026 13:43:50 +0800 Subject: [PATCH] =?UTF-8?q?=E9=95=9C=E5=A4=B4=E9=99=8D=E8=90=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aros/apron/entity/MessageDown.java | 759 ++++++++++++ .../entity/SpeakerAudioPlayProgress.java | 150 +++ .../apron/entity/SpeakerTTSPlayProgress.java | 150 +++ .../aros/apron/entity/Synchronizedstatus.java | 80 ++ .../com/aros/apron/entity/XTTSParams.java | 12 + .../aros/apron/manager/LogUploadManager.java | 249 ++++ .../NavigationSatelliteSystemManager.java | 94 ++ .../aros/apron/manager/RtspStreamManager.java | 415 +++++++ .../aros/apron/manager/SpeakerManager.java | 314 +++++ .../aros/apron/manager/UdpStreamManager.java | 331 ++++++ .../aros/apron/manager/VlcRtspManager.java | 133 +++ .../apron/tools/ApronArucoDetectPort.java | 827 +++++++++++++ .../aros/apron/tools/ApronArucoStatus.java | 83 ++ .../aros/apron/tools/DualCaptureHelper.java | 270 +++++ .../apron/tools/Generakmzaltheratools.java | 230 ++++ .../com/aros/apron/tools/Gpsdistance.java | 45 + .../aros/apron/tools/MixedVisionLanding.java | 1045 +++++++++++++++++ .../aros/apron/tools/SimplePortScanner.java | 81 ++ .../apron/tools/SpeakerProgressReporter.java | 203 ++++ .../apron/tools/TakeoffProgressScheduler.java | 229 ++++ .../main/java/com/aros/apron/tools/Utils.java | 143 +++ .../aros/apron/tools/XTtsPcmGenerator.java | 154 +++ .../com/aros/apron/tools/mixrongapron.java | 4 + 23 files changed, 6001 insertions(+) create mode 100644 app/src/main/java/com/aros/apron/entity/MessageDown.java create mode 100644 app/src/main/java/com/aros/apron/entity/SpeakerAudioPlayProgress.java create mode 100644 app/src/main/java/com/aros/apron/entity/SpeakerTTSPlayProgress.java create mode 100644 app/src/main/java/com/aros/apron/entity/Synchronizedstatus.java create mode 100644 app/src/main/java/com/aros/apron/entity/XTTSParams.java create mode 100644 app/src/main/java/com/aros/apron/manager/LogUploadManager.java create mode 100644 app/src/main/java/com/aros/apron/manager/NavigationSatelliteSystemManager.java create mode 100644 app/src/main/java/com/aros/apron/manager/RtspStreamManager.java create mode 100644 app/src/main/java/com/aros/apron/manager/SpeakerManager.java create mode 100644 app/src/main/java/com/aros/apron/manager/UdpStreamManager.java create mode 100644 app/src/main/java/com/aros/apron/manager/VlcRtspManager.java create mode 100644 app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java create mode 100644 app/src/main/java/com/aros/apron/tools/ApronArucoStatus.java create mode 100644 app/src/main/java/com/aros/apron/tools/DualCaptureHelper.java create mode 100644 app/src/main/java/com/aros/apron/tools/Generakmzaltheratools.java create mode 100644 app/src/main/java/com/aros/apron/tools/Gpsdistance.java create mode 100644 app/src/main/java/com/aros/apron/tools/MixedVisionLanding.java create mode 100644 app/src/main/java/com/aros/apron/tools/SimplePortScanner.java create mode 100644 app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java create mode 100644 app/src/main/java/com/aros/apron/tools/TakeoffProgressScheduler.java create mode 100644 app/src/main/java/com/aros/apron/tools/Utils.java create mode 100644 app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java create mode 100644 app/src/main/java/com/aros/apron/tools/mixrongapron.java diff --git a/app/src/main/java/com/aros/apron/entity/MessageDown.java b/app/src/main/java/com/aros/apron/entity/MessageDown.java new file mode 100644 index 00000000..4962ebce --- /dev/null +++ b/app/src/main/java/com/aros/apron/entity/MessageDown.java @@ -0,0 +1,759 @@ +package com.aros.apron.entity; + +import java.util.List; + +public class MessageDown { + + private String tid; + private String bid; + private long timestamp; + private String method; + private Data data; + + + + public String getTid() { + return tid; + } + + public void setTid(String tid) { + this.tid = tid; + } + + public String getBid() { + return bid; + } + + public void setBid(String bid) { + this.bid = bid; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } + + public static class Data { + private int result; + private String url; + private int video_quality; + private int ideo_quality; + private AlternateLandPoint alternate_land_point; + private int rth_mode; + private int task_type; + private int wayline_precision_type; + private String video_type; + private long execute_time; + private int exit_wayline_when_rc_lost; + private File file; + private String flight_id; + private int flight_safety_advance_check; + private int out_of_control_action; + private boolean takeoffToPointTask; + private BreakPoint break_point; + private int camera_mode=0;//初始化为拍照 + private String h; + private String seq; + private String w; + //摇杆和云台 + private String x; + private String y; + + private String camera_type; + private String payload_index; + + private float height; + private double latitude; + private boolean locked; + private double longitude; + private double zoom_factor; + private boolean enable; + private int exposure_mode; + private int exposure_value; + private int focus_mode; + private int focus_value; + + private double commander_flight_height; + private int commander_mode_lost_action; + private String max_speed; + private int rc_lost_action; + private int rth_altitude; + private String security_takeoff_height; + private double target_height; + private double target_latitude; + private double target_longitude; + private String fly_to_id; + private List points; + private int reset_mode; + + private double pitch_speed; + + private double yaw_speed; + + private float width; + + private int psdk_index; + private int play_mode; + private int play_volume; + private int volume; + private int type; + private int language; + + + + private Tts tts; + + + public Tts getTts() { + return tts; + } + + public void setTts(Tts tts) { + this.tts = tts; + } + + public static class Tts { + private String md5; + private String name; + private String text; + + 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 getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + } + + + + + public int getSpeed() { + return speed; + } + + public void setSpeed(int speed) { + this.speed = speed; + } + + private int speed; + + public void setHeight(float height) { + this.height = height; + } + + public void setCommander_flight_height(double commander_flight_height) { + this.commander_flight_height = commander_flight_height; + } + + public void setTarget_height(double target_height) { + this.target_height = target_height; + } + + public int getPsdk_index() { + return psdk_index; + } + + public void setPsdk_index(int psdk_index) { + this.psdk_index = psdk_index; + } + + public int getPlay_mode() { + return play_mode; + } + + public void setPlay_mode(int play_mode) { + this.play_mode = play_mode; + } + + public int getPlay_volume() { + return play_volume; + } + + public void setPlay_volume(int play_volume) { + this.play_volume = play_volume; + } + + public int getVolume() { + return volume; + } + + public void setVolume(int volume) { + this.volume = volume; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getLanguage() { + return language; + } + + public void setLanguage(int language) { + this.language = language; + } + + public float getWidth() { + return width; + } + + public void setWidth(float width) { + this.width = width; + } + + public double getPitch_speed() { + return pitch_speed; + } + + public void setPitch_speed(double pitch_speed) { + this.pitch_speed = pitch_speed; + } + + public double getYaw_speed() { + return yaw_speed; + } + + public void setYaw_speed(double yaw_speed) { + this.yaw_speed = yaw_speed; + } + + public int getReset_mode() { + return reset_mode; + } + + + public void setReset_mode(int reset_mode) { + this.reset_mode = reset_mode; + } + + public String getFly_to_id() { + return fly_to_id; + } + + public void setFly_to_id(String fly_to_id) { + this.fly_to_id = fly_to_id; + } + + public List getPoints() { + return points; + } + + public void setPoints(List points) { + this.points = points; + } + + public static class Points { + private int height; + private double latitude; + private double longitude; + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + } + public double getCommander_flight_height() { + return commander_flight_height; + } + + public void setCommander_flight_height(int commander_flight_height) { + this.commander_flight_height = commander_flight_height; + } + + public int getCommander_mode_lost_action() { + return commander_mode_lost_action; + } + + public void setCommander_mode_lost_action(int commander_mode_lost_action) { + this.commander_mode_lost_action = commander_mode_lost_action; + } + + public String getMax_speed() { + return max_speed; + } + + public void setMax_speed(String max_speed) { + this.max_speed = max_speed; + } + + public int getRc_lost_action() { + return rc_lost_action; + } + + public void setRc_lost_action(int rc_lost_action) { + this.rc_lost_action = rc_lost_action; + } + + public String getSecurity_takeoff_height() { + return security_takeoff_height; + } + + public void setSecurity_takeoff_height(String security_takeoff_height) { + this.security_takeoff_height = security_takeoff_height; + } + + public double getTarget_height() { + return target_height; + } + + public void setTarget_height(int target_height) { + this.target_height = target_height; + } + + public double getTarget_latitude() { + return target_latitude; + } + + public void setTarget_latitude(double target_latitude) { + this.target_latitude = target_latitude; + } + + public double getTarget_longitude() { + return target_longitude; + } + + public void setTarget_longitude(double target_longitude) { + this.target_longitude = target_longitude; + } + + public int getFocus_value() { + return focus_value; + } + + public void setFocus_value(int focus_value) { + this.focus_value = focus_value; + } + + public int getFocus_mode() { + return focus_mode; + } + + public void setFocus_mode(int focus_mode) { + this.focus_mode = focus_mode; + } + + public int getExposure_value() { + return exposure_value; + } + + public void setExposure_value(int exposure_value) { + this.exposure_value = exposure_value; + } + + public int getExposure_mode() { + return exposure_mode; + } + + public void setExposure_mode(int exposure_mode) { + this.exposure_mode = exposure_mode; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public double getZoom_factor() { + return zoom_factor; + } + + public void setZoom_factor(double zoom_factor) { + this.zoom_factor = zoom_factor; + } + + public float getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public String getCamera_type() { + return camera_type; + } + + public void setCamera_type(String camera_type) { + this.camera_type = camera_type; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + public String getPayload_index() { + return payload_index; + } + + public void setPayload_index(String payload_index) { + this.payload_index = payload_index; + } + + public String getH() { + return h; + } + + public void setH(String h) { + this.h = h; + } + + public String getSeq() { + return seq; + } + + public void setSeq(String seq) { + this.seq = seq; + } + + public String getW() { + return w; + } + + public void setW(String w) { + this.w = w; + } + + public String getX() { + return x; + } + + public void setX(String x) { + this.x = x; + } + + public String getY() { + return y; + } + + public void setY(String y) { + this.y = y; + } + + public int getCamera_mode() { + return camera_mode; + } + + public void setCamera_mode(int camera_mode) { + this.camera_mode = camera_mode; + } + + public BreakPoint getBreak_point() { + return break_point; + } + + public void setBreak_point(BreakPoint break_point) { + this.break_point = break_point; + } + public int getIdeo_quality() { + return ideo_quality; + } + + public void setIdeo_quality(int ideo_quality) { + this.ideo_quality = ideo_quality; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public int getVideo_quality() { + return video_quality; + } + + public void setVideo_quality(int video_quality) { + this.video_quality = video_quality; + } + + public int getResult() { + return result; + } + + public void setResult(int result) { + this.result = result; + } + + public String getVideo_type() { + return video_type; + } + + public void setVideo_type(String video_type) { + this.video_type = video_type; + } + + + public static class File { + private String fingerprint; + private String url; + + public String getFingerprint() { + return fingerprint; + } + + public void setFingerprint(String fingerprint) { + this.fingerprint = fingerprint; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + + + public static class BreakPoint { + private int index; + private double progress; + private int state; + private int wayline_id; + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public double getProgress() { + return progress; + } + + public void setProgress(double progress) { + this.progress = progress; + } + + public int getState() { + return state; + } + + public void setState(int state) { + this.state = state; + } + + public int getWayline_id() { + return wayline_id; + } + + public void setWayline_id(int wayline_id) { + this.wayline_id = wayline_id; + } + } + + public AlternateLandPoint getAlternate_land_point() { + return alternate_land_point; + } + + public void setAlternate_land_point(AlternateLandPoint alternate_land_point) { + this.alternate_land_point = alternate_land_point; + } + + public long getExecute_time() { + return execute_time; + } + + public void setExecute_time(long execute_time) { + this.execute_time = execute_time; + } + + public int getExit_wayline_when_rc_lost() { + return exit_wayline_when_rc_lost; + } + + public void setExit_wayline_when_rc_lost(int exit_wayline_when_rc_lost) { + this.exit_wayline_when_rc_lost = exit_wayline_when_rc_lost; + } + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } + + public String getFlight_id() { + return flight_id; + } + + public void setFlight_id(String flight_id) { + this.flight_id = flight_id; + } + + public int getFlight_safety_advance_check() { + return flight_safety_advance_check; + } + + public void setFlight_safety_advance_check(int flight_safety_advance_check) { + this.flight_safety_advance_check = flight_safety_advance_check; + } + + public int getOut_of_control_action() { + return out_of_control_action; + } + + public void setOut_of_control_action(int out_of_control_action) { + this.out_of_control_action = out_of_control_action; + } + + public int getRth_altitude() { + return rth_altitude; + } + + public void setRth_altitude(int rth_altitude) { + this.rth_altitude = rth_altitude; + } + + public int getRth_mode() { + return rth_mode; + } + + public void setRth_mode(int rth_mode) { + this.rth_mode = rth_mode; + } + + public boolean isTakeoffToPointTask() { + return takeoffToPointTask; + } + + public void setTakeoffToPointTask(boolean takeoffToPointTask) { + this.takeoffToPointTask = takeoffToPointTask; + } + + public int getTask_type() { + return task_type; + } + + public void setTask_type(int task_type) { + this.task_type = task_type; + } + + public int getWayline_precision_type() { + return wayline_precision_type; + } + + public void setWayline_precision_type(int wayline_precision_type) { + this.wayline_precision_type = wayline_precision_type; + } + + public static class AlternateLandPoint { + private double latitude; + private double longitude; + private double safe_land_height; + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public double getSafe_land_height() { + return safe_land_height; + } + + public void setSafe_land_height(double safe_land_height) { + this.safe_land_height = safe_land_height; + } + } + + } +} diff --git a/app/src/main/java/com/aros/apron/entity/SpeakerAudioPlayProgress.java b/app/src/main/java/com/aros/apron/entity/SpeakerAudioPlayProgress.java new file mode 100644 index 00000000..3ad242a6 --- /dev/null +++ b/app/src/main/java/com/aros/apron/entity/SpeakerAudioPlayProgress.java @@ -0,0 +1,150 @@ +package com.aros.apron.entity; + +import com.google.gson.annotations.SerializedName; + +/** + * 喊话器-音频播放进度通知 + * Method: speaker_audio_play_start_progress + */ +public class SpeakerAudioPlayProgress { + + private String bid; + private String tid; + private Long timestamp; + private String method; + private Integer needReply; + private Data data; + + public String getBid() { + return bid; + } + + public void setBid(String bid) { + this.bid = bid; + } + + public String getTid() { + return tid; + } + + public void setTid(String tid) { + this.tid = tid; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public Integer getNeedReply() { + return needReply; + } + + public void setNeedReply(Integer needReply) { + this.needReply = needReply; + } + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } + + public static class Data { + private Integer result; + private Output output; + + public Integer getResult() { + return result; + } + + public void setResult(Integer result) { + this.result = result; + } + + public Output getOutput() { + return output; + } + + public void setOutput(Output output) { + this.output = output; + } + } + + public static class Output { + @SerializedName("psdk_index") + private Integer psdkIndex; + + private String status; + private String md5; + private Progress progress; + + public Integer getPsdkIndex() { + return psdkIndex; + } + + public void setPsdkIndex(Integer psdkIndex) { + this.psdkIndex = psdkIndex; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + public Progress getProgress() { + return progress; + } + + public void setProgress(Progress progress) { + this.progress = progress; + } + } + + public static class Progress { + private Integer percent; + + @SerializedName("step_key") + private String stepKey; + + public Integer getPercent() { + return percent; + } + + public void setPercent(Integer percent) { + this.percent = percent; + } + + public String getStepKey() { + return stepKey; + } + + public void setStepKey(String stepKey) { + this.stepKey = stepKey; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/entity/SpeakerTTSPlayProgress.java b/app/src/main/java/com/aros/apron/entity/SpeakerTTSPlayProgress.java new file mode 100644 index 00000000..51c1b29c --- /dev/null +++ b/app/src/main/java/com/aros/apron/entity/SpeakerTTSPlayProgress.java @@ -0,0 +1,150 @@ +package com.aros.apron.entity; + +import com.google.gson.annotations.SerializedName; + +/** + * 喊话器-TTS播放进度通知 + * Method: speaker_tts_play_start_progress + */ +public class SpeakerTTSPlayProgress { + + private String bid; + private String tid; + private Long timestamp; + private String method; + private Integer needReply; + private Data data; + + public String getBid() { + return bid; + } + + public void setBid(String bid) { + this.bid = bid; + } + + public String getTid() { + return tid; + } + + public void setTid(String tid) { + this.tid = tid; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public Integer getNeedReply() { + return needReply; + } + + public void setNeedReply(Integer needReply) { + this.needReply = needReply; + } + + public Data getData() { + return data; + } + + public void setData(Data data) { + this.data = data; + } + + public static class Data { + private Integer result; + private Output output; + + public Integer getResult() { + return result; + } + + public void setResult(Integer result) { + this.result = result; + } + + public Output getOutput() { + return output; + } + + public void setOutput(Output output) { + this.output = output; + } + } + + public static class Output { + @SerializedName("psdk_index") + private Integer psdkIndex; + + private String status; + private String md5; + private Progress progress; + + public Integer getPsdkIndex() { + return psdkIndex; + } + + public void setPsdkIndex(Integer psdkIndex) { + this.psdkIndex = psdkIndex; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + public Progress getProgress() { + return progress; + } + + public void setProgress(Progress progress) { + this.progress = progress; + } + } + + public static class Progress { + private Integer percent; + + @SerializedName("step_key") + private String stepKey; + + public Integer getPercent() { + return percent; + } + + public void setPercent(Integer percent) { + this.percent = percent; + } + + public String getStepKey() { + return stepKey; + } + + public void setStepKey(String stepKey) { + this.stepKey = stepKey; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/entity/Synchronizedstatus.java b/app/src/main/java/com/aros/apron/entity/Synchronizedstatus.java new file mode 100644 index 00000000..655c9761 --- /dev/null +++ b/app/src/main/java/com/aros/apron/entity/Synchronizedstatus.java @@ -0,0 +1,80 @@ +package com.aros.apron.entity; + +import com.aros.apron.tools.PreferenceUtils; + +public class Synchronizedstatus { + // 1. 状态变量:保持原样 (建议用 volatile 保证可见性,虽然 synchronized 内也能保证,但加上更保险) + private static volatile boolean FLIGHTTASK_EXECUTE_STATUS = false; + + // 2. 初始化状态变量:用于跟踪自检状态 + private static volatile boolean INIT_STATUS = false; + + // 3. 初始化运行状态变量:用于跟踪初始化是否正在进行 + private static volatile boolean INIT_RUNNING = false; + + + private static volatile boolean isruning=false; + + private static volatile boolean isruningframe=false; + + public static boolean isIsruningframe() { + return isruningframe; + } + + public static void setIsruningframe(boolean isruningframe) { + Synchronizedstatus.isruningframe = isruningframe; + } + + // 4. 【新增】专门的锁对象:必须是 Object 类型,且唯一 + // 这个对象什么都不存,只用来当“钥匙” + public static final Object LOCK_OBJ = new Object(); + + // 提供 getter/setter 方便访问状态 + + + public static boolean isIsruning() { + return isruning; + } + + public static void setIsruning(boolean isruning) { + Synchronizedstatus.isruning = isruning; + } + + public static boolean getFlighttaskExecuteStatus() { + return FLIGHTTASK_EXECUTE_STATUS; + } + + public static void setFlighttaskExecuteStatus(boolean status) { + FLIGHTTASK_EXECUTE_STATUS = status; + } + + // 提供 getter/setter 用于初始化状态 + public static boolean getInitStatus() { + return INIT_STATUS; + } + + public static void setInitStatus(boolean status) { + INIT_STATUS = status; + } + + // 提供 getter/setter 用于第一次收到航线指令标志位(持久化存储) + public static boolean isFirstMissionReceived() { + return PreferenceUtils.getInstance().isFirstMissionReceived(); + } + + public static void setFirstMissionReceived(boolean status) { + PreferenceUtils.getInstance().setFirstMissionReceived(status); + } + + // 提供 getter/setter 用于初始化运行状态 + public static boolean isInitRunning() { + return INIT_RUNNING; + } + + public static void setInitRunning(boolean running) { + INIT_RUNNING = running; + } + + + +} diff --git a/app/src/main/java/com/aros/apron/entity/XTTSParams.java b/app/src/main/java/com/aros/apron/entity/XTTSParams.java new file mode 100644 index 00000000..1a22a964 --- /dev/null +++ b/app/src/main/java/com/aros/apron/entity/XTTSParams.java @@ -0,0 +1,12 @@ +package com.aros.apron.entity; + + +public class XTTSParams { + + public String vcn = "xiaoyan"; + public int language =1 ; + public int pitch = 50; + public int speed = 50; + public int volume = 100; + +} diff --git a/app/src/main/java/com/aros/apron/manager/LogUploadManager.java b/app/src/main/java/com/aros/apron/manager/LogUploadManager.java new file mode 100644 index 00000000..ded66b88 --- /dev/null +++ b/app/src/main/java/com/aros/apron/manager/LogUploadManager.java @@ -0,0 +1,249 @@ +package com.aros.apron.manager; + +import android.os.Build; +import android.os.Environment; +import android.text.TextUtils; + +import androidx.annotation.RequiresApi; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.aros.apron.base.BaseManager; +import com.aros.apron.tools.LogUtil; +import com.aros.apron.tools.PreferenceUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.Date; + +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableEmitter; +import io.reactivex.rxjava3.core.ObservableOnSubscribe; +import io.reactivex.rxjava3.core.Observer; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class LogUploadManager extends BaseManager { + + private static final String TAG = "LogUploadManager"; + private static final String BUCKET_NAME = "msdk-log"; + private static final String DEFAULT_ENDPOINT = "http://192.168.20.90:9000"; + private static final String DEFAULT_ACCESS_KEY = "aros"; + private static final String DEFAULT_SECRET_KEY = "Aros2023"; + + // 参数别改:临时目录用于快照 + private static final String TEMP_SNAPSHOT_DIR = "/DJIDemo/cache/temp_upload"; + + private LogUploadManager() {} + + private static class LogUploadManagerHolder { + private static final LogUploadManager INSTANCE = new LogUploadManager(); + } + + public static LogUploadManager getInstance() { + return LogUploadManagerHolder.INSTANCE; + } + + private boolean isUploading; + + public boolean isUploading() { + return isUploading; + } + + public void uploadTodayLog() { + String endpoint = PreferenceUtils.getInstance().getUploadUrl(); + String accessKey = PreferenceUtils.getInstance().getAccessKey(); + String secretKey = PreferenceUtils.getInstance().getSecretKey(); + + if (TextUtils.isEmpty(endpoint)) { + endpoint = DEFAULT_ENDPOINT; + } + if (TextUtils.isEmpty(accessKey)) { + accessKey = DEFAULT_ACCESS_KEY; + } + if (TextUtils.isEmpty(secretKey)) { + secretKey = DEFAULT_SECRET_KEY; + } + + LogUtil.log(TAG, "使用MinIO配置: endpoint=" + endpoint + ", accessKey=" + accessKey); + uploadTodayLog(endpoint, accessKey, secretKey); + } + + public void uploadTodayLog(String endpoint, String accessKey, String secretKey) { + if (isUploading) { + LogUtil.log(TAG, "日志正在上传中"); + return; + } + + isUploading = true; + LogUtil.log(TAG, "开始上传当天日志"); + + File todayLogFile = getTodayLogFile(); + if (todayLogFile == null || !todayLogFile.exists()) { + LogUtil.log(TAG, "当天日志文件不存在"); + isUploading = false; + return; + } + + // 关键修改:先创建快照,再上传快照 + File snapshotFile = null; + try { + snapshotFile = createSnapshot(todayLogFile); + LogUtil.log(TAG, "创建快照成功: " + snapshotFile.getAbsolutePath() + + ", 大小: " + snapshotFile.length()); + + uploadLogFile(snapshotFile, endpoint, accessKey, secretKey); + + } catch (IOException e) { + LogUtil.log(TAG, "创建快照失败: " + e.getMessage()); + isUploading = false; + } + } + + /** + * 创建文件快照(解决文件写入中上传失败问题) + * 参数别改:使用NIO通道拷贝,速度快且稳定 + */ + private File createSnapshot(File source) throws IOException { + // 确保临时目录存在 + File tempDir = new File(Environment.getExternalStorageDirectory() + TEMP_SNAPSHOT_DIR); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } + + // 修复:时间戳放中间,后缀用.tmp,避免和原文件.txt混淆 + // 结果:20260310_1741605600000.tmp + String baseName = source.getName(); // 20260310.txt + String nameWithoutExt = baseName.substring(0, baseName.lastIndexOf('.')); // 20260310 + String snapshotName = nameWithoutExt + "_" + System.currentTimeMillis() + ".tmp"; + + File snapshot = new File(tempDir, snapshotName); + + // NIO零拷贝(快速) + try (FileChannel srcChannel = new FileInputStream(source).getChannel(); + FileChannel dstChannel = new FileOutputStream(snapshot).getChannel()) { + + long transferred = srcChannel.transferTo(0, srcChannel.size(), dstChannel); + LogUtil.log(TAG, "快照拷贝完成: " + transferred + " bytes"); + } + + return snapshot; + } + + private File getTodayLogFile() { + String logDir = getLogDir(); + String todayDate = new SimpleDateFormat("yyyyMMdd").format(new Date()); + String fileName = todayDate + ".txt"; + return new File(logDir, fileName); + } + + private String getLogDir() { + return Environment.getExternalStorageDirectory() + "/DJIDemo/cache/log"; + } + + private void uploadLogFile(File logFile, String endpoint, String accessKey, String secretKey) { + AmazonS3 s3 = new AmazonS3Client(new AWSCredentials() { + @Override + public String getAWSAccessKeyId() { + return accessKey; + } + + @Override + public String getAWSSecretKey() { + return secretKey; + } + }); + + Observable.create(new ObservableOnSubscribe() { + @Override + public void subscribe(ObservableEmitter emitter) throws Exception { + try { + s3.setEndpoint(endpoint); + + boolean bucketExists = s3.doesBucketExist(BUCKET_NAME); + if (!bucketExists) { + LogUtil.log(TAG, "创建桶: " + BUCKET_NAME); + s3.createBucket(BUCKET_NAME); + } + + String objectKey = getObjectKey(logFile.getName()); + // 注意:这里上传的是快照文件,不是原日志文件 + LogUtil.log(TAG, "上传文件: " + logFile.getName() + + " 到: " + objectKey + + ", 大小: " + logFile.length()); + + s3.putObject( + new PutObjectRequest( + BUCKET_NAME, + objectKey, + logFile + ) + ); + + emitter.onNext(true); + emitter.onComplete(); + } catch (Exception e) { + emitter.onError(e); + } + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Observer() { + @Override + public void onSubscribe(Disposable d) {} + + @Override + public void onNext(Boolean success) { + sendEvent2Server("日志上传成功: ",1); + LogUtil.log(TAG, "日志上传成功"); + } + + @Override + public void onError(Throwable e) { + sendEvent2Server("日志上传失败: " + e.getMessage(),2); + LogUtil.log(TAG, "日志上传失败: " + e.getMessage()); + e.printStackTrace(); + // 失败也要清理快照 + deleteSnapshot(logFile); + isUploading = false; + } + + @Override + public void onComplete() { + isUploading = false; + // 关键:上传完成后删除快照,避免堆积 + deleteSnapshot(logFile); + sendEvent2Server("日志上传完成,清理快照",1); + LogUtil.log(TAG, "日志上传完成,清理快照"); + } + }); + } + + /** + * 删除快照文件 + */ + private void deleteSnapshot(File snapshot) { + if (snapshot != null && snapshot.exists() && snapshot.getName().endsWith(".tmp")) { + boolean deleted = snapshot.delete(); + LogUtil.log(TAG, "快照清理" + (deleted ? "成功" : "失败") + ": " + snapshot.getName()); + } + } + + private String getObjectKey(String snapshotName) { + String objectKeyPrefix = PreferenceUtils.getInstance().getObjectKey(); + if (TextUtils.isEmpty(objectKeyPrefix)) { + objectKeyPrefix = "1"; + } + // 修复:从 20260310_1741605600000.tmp 还原为 20260310.txt + String realFileName = snapshotName.replaceAll("_\\d+\\.tmp$", ".txt"); + return objectKeyPrefix + "/" + realFileName; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/manager/NavigationSatelliteSystemManager.java b/app/src/main/java/com/aros/apron/manager/NavigationSatelliteSystemManager.java new file mode 100644 index 00000000..0dc746be --- /dev/null +++ b/app/src/main/java/com/aros/apron/manager/NavigationSatelliteSystemManager.java @@ -0,0 +1,94 @@ +package com.aros.apron.manager; + +import static com.aros.apron.tools.Utils.getIDJIErrorMsg; +import static dji.sdk.keyvalue.key.KeyTools.createKey; + +import android.os.Handler; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.aros.apron.base.BaseManager; +import com.aros.apron.entity.Movement; +import com.aros.apron.tools.LogUtil; +import com.aros.apron.tools.PreferenceUtils; + +import dji.sdk.keyvalue.key.FlightControllerKey; +import dji.sdk.keyvalue.key.KeyTools; +import dji.sdk.keyvalue.value.flightcontroller.NavigationSatelliteSystem; +import dji.v5.common.callback.CommonCallbacks; +import dji.v5.common.error.IDJIError; +import dji.v5.manager.KeyManager; + + +public class NavigationSatelliteSystemManager extends BaseManager { + + + private NavigationSatelliteSystemManager() { + } + + private static class NavigationSatelliteHolder { + private static final NavigationSatelliteSystemManager INSTANCE = new NavigationSatelliteSystemManager(); + } + + public static NavigationSatelliteSystemManager getInstance() { + return NavigationSatelliteHolder.INSTANCE; + } + + public void initNavigationSatelliteSystem() { + Boolean isConnect = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey. + KeyConnection)); + if (isConnect != null && isConnect) { + KeyManager.getInstance().listen(createKey(FlightControllerKey.KeyNavigationSatelliteSystemSource), + this, new CommonCallbacks.KeyListener() { + @Override + public void onValueChange(@Nullable NavigationSatelliteSystem navigationSatelliteSystem, + @Nullable NavigationSatelliteSystem t1) { + if (t1 != null) { + LogUtil.log(TAG, "监听卫星导航系统:" + t1.name()); + Movement.getInstance().setNavigationSatelliteSystem(t1.value()); + } + } + }); + } + } + + private int setLTEEnhancedTransmissionTypeTimes; + private boolean isLTEEnhancedTransmissionLte; + + public void setNavigationSatelliteSystem() { + Boolean isConnect = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey. + KeyConnection)); + if (isConnect != null && isConnect) { + + KeyManager.getInstance().setValue(KeyTools.createKey(FlightControllerKey.KeyNavigationSatelliteSystemSource), + PreferenceUtils.getInstance().getSatelliteSystem() == 1 ? + NavigationSatelliteSystem.GPS_GLONASS : NavigationSatelliteSystem.BEIDOU, + new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + isLTEEnhancedTransmissionLte = true; + LogUtil.log(TAG, "设置卫星系统" + setLTEEnhancedTransmissionTypeTimes + "次成功"); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG, "设置卫星系统第" + setLTEEnhancedTransmissionTypeTimes + "次失败:" + getIDJIErrorMsg(error)); + if (!isLTEEnhancedTransmissionLte) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (setLTEEnhancedTransmissionTypeTimes < 20) { + setLTEEnhancedTransmissionTypeTimes++; + setNavigationSatelliteSystem(); + } + } + }, 3000); + } + } + }); + } + + } + +} diff --git a/app/src/main/java/com/aros/apron/manager/RtspStreamManager.java b/app/src/main/java/com/aros/apron/manager/RtspStreamManager.java new file mode 100644 index 00000000..99f1a856 --- /dev/null +++ b/app/src/main/java/com/aros/apron/manager/RtspStreamManager.java @@ -0,0 +1,415 @@ +package com.aros.apron.manager; + +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import com.aros.apron.activity.MainActivity; +import com.aros.apron.tools.LogUtil; +import com.aros.apron.tools.PreferenceUtils; +import com.aros.apron.tools.SimplePortScanner; +import com.google.gson.Gson; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import dji.sdk.keyvalue.key.DJIKey; +import dji.sdk.keyvalue.key.FlightControllerKey; +import dji.sdk.keyvalue.value.common.ComponentIndexType; +import dji.v5.common.callback.CommonCallbacks; +import dji.v5.common.error.IDJIError; +import dji.v5.manager.KeyManager; +import dji.v5.manager.datacenter.MediaDataCenter; +import dji.v5.manager.datacenter.livestream.LiveStreamSettings; +import dji.v5.manager.datacenter.livestream.LiveStreamStatus; +import dji.v5.manager.datacenter.livestream.LiveStreamStatusListener; +import dji.v5.manager.datacenter.livestream.LiveStreamType; +import dji.v5.manager.datacenter.livestream.LiveVideoBitrateMode; +import dji.v5.manager.datacenter.livestream.StreamQuality; +import dji.v5.manager.datacenter.livestream.settings.RtspSettings; +import dji.v5.manager.interfaces.ILiveStreamManager; + +/** + * RTSP推流管理类 + * 负责RTSP推流的初始化、启动、停止和状态管理 + */ +public class RtspStreamManager { + private static final String TAG = "RtspStreamManager"; + + // 线程池和主线程Handler + private final ExecutorService streamExecutor = Executors.newSingleThreadExecutor(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + // 单例模式 + private static class RtspStreamHolder { + private static final RtspStreamManager INSTANCE = new RtspStreamManager(); + } + + public static RtspStreamManager getInstance() { + return RtspStreamHolder.INSTANCE; + } + + // 状态管理 + private final AtomicBoolean isStreaming = new AtomicBoolean(false); + private final AtomicInteger retryCount = new AtomicInteger(0); + private final AtomicBoolean isInitializing = new AtomicBoolean(false); + private final AtomicBoolean isForceStarting = new AtomicBoolean(false); + + // 配置参数 + private static final int MAX_RETRY_COUNT = 20; + private static final long RETRY_DELAY_MS = 5000; + private static final long SETTLE_DELAY_MS = 1000; + + // 当前相机索引 + private int currentCameraIndex = 1; // 1代表PORT_1,2代表FPV + + private RtspStreamManager() { + initStreamManager(); + } + + /** + * 初始化流管理器 + */ + public void initStreamManager() { + ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); + if (liveStreamManager != null) { + liveStreamManager.addLiveStreamStatusListener(new LiveStreamStatusListener() { + @Override + public void onLiveStreamStatusUpdate(LiveStreamStatus status) { + if (status != null) { + boolean streaming = status.isStreaming(); + isStreaming.set(streaming); + LogUtil.log(TAG, "推流状态: " + streaming + ", 帧率: " + status.getFps() + ", 码率: " + status.getVbps() + ", 延迟: " + status.getRtt()); + } + } + + @Override + public void onError(IDJIError error) { + LogUtil.log(TAG, "推流错误: " + new Gson().toJson(error)); + } + }); + } + } + + /** + * 启动RTSP推流 + */ + public void startRtspStream() { + if (isInitializing.get() || isForceStarting.get()) { + LogUtil.log(TAG, "推流正在初始化中,请勿重复调用"); + return; + } + + isInitializing.set(true); + retryCount.set(0); + + streamExecutor.execute(() -> { + try { + doStartRtspStream(); + } catch (Exception e) { + LogUtil.log(TAG, "启动推流异常: " + e.getMessage()); + e.printStackTrace(); + } finally { + isInitializing.set(false); + } + }); + } + + /** + * 实际启动RTSP推流的方法 + */ + private void doStartRtspStream() { + // 检查飞行器连接状态 + Boolean isAircraftConnected = KeyManager.getInstance().getValue(DJIKey.create(FlightControllerKey.KeyConnection)); + if (isAircraftConnected == null || !isAircraftConnected) { + LogUtil.log(TAG, "飞行器未连接,无法启动推流"); + return; + } + + // 检查RTSP配置 + if (!validateRtspConfig()) { + LogUtil.log(TAG, "RTSP配置参数不完整"); + return; + } + + // 检查相机流状态 + if (!MainActivity.Companion.getStreamReceive() && retryCount.get() < MAX_RETRY_COUNT) { + handleStreamNotReady(); + return; + } + + // 启动推流 + startStream(); + } + + /** + * 处理相机流未准备好的情况 + */ + private void handleStreamNotReady() { + LogUtil.log(TAG, "相机流未准备好,尝试恢复 (重试次数: " + retryCount.get() + ")"); + + // 尝试切换视频源 + mainHandler.post(() -> { + MainActivity mainActivity = MainActivity.Companion.getInstance(); + if (mainActivity != null) { + mainActivity.swapVideoSource(); + } + }); + + // 延迟重试 + mainHandler.postDelayed(() -> { + streamExecutor.execute(() -> { + int count = retryCount.incrementAndGet(); + if (count < MAX_RETRY_COUNT) { + LogUtil.log(TAG, "重试启动推流 (次数: " + count + ")"); + doStartRtspStream(); + } else { + LogUtil.log(TAG, "达到最大重试次数,强制启动推流"); + forceStartStream(); + } + }); + }, RETRY_DELAY_MS); + } + + /** + * 启动推流 + */ + private void startStream() { + ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); + if (liveStreamManager == null) { + LogUtil.log(TAG, "LiveStreamManager获取失败"); + return; + } + + // 配置推流参数 + configureStreamSettings(liveStreamManager); + + // 等待配置生效 + try { + Thread.sleep(SETTLE_DELAY_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + // 启动推流 + if (!liveStreamManager.isStreaming()) { + doStartStream(liveStreamManager, false); + } else { + // 先停止再启动 + liveStreamManager.stopStream(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + try { + Thread.sleep(SETTLE_DELAY_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + doStartStream(liveStreamManager, true); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG, "停止旧流失败: " + new Gson().toJson(error)); + // 即使停止失败也尝试启动 + doStartStream(liveStreamManager, true); + } + }); + } + } + + /** + * 强制启动推流 + */ + private void forceStartStream() { + isForceStarting.set(true); + LogUtil.log(TAG, "开始强制启动推流"); + + streamExecutor.execute(() -> { + try { + ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); + if (liveStreamManager == null) { + LogUtil.log(TAG, "LiveStreamManager获取失败"); + return; + } + + // 配置推流参数 + configureStreamSettings(liveStreamManager); + + // 直接启动 + liveStreamManager.startStream(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + mainHandler.post(() -> { + LogUtil.log(TAG, "强制启动RTSP推流成功"); + isStreaming.set(true); + startPortScanner(); + }); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + mainHandler.post(() -> { + LogUtil.log(TAG, "强制启动RTSP推流失败: " + new Gson().toJson(error)); + }); + } + }); + } finally { + isForceStarting.set(false); + } + }); + } + + /** + * 配置推流设置 + */ + private void configureStreamSettings(ILiveStreamManager liveStreamManager) { + mainHandler.post(() -> { + try { + 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(currentCameraIndex == 1 ? ComponentIndexType.PORT_1 : ComponentIndexType.FPV); + liveStreamManager.setLiveStreamQuality(StreamQuality.FULL_HD); + liveStreamManager.setLiveVideoBitrateMode(LiveVideoBitrateMode.AUTO); + + LogUtil.log(TAG, "推流配置完成,相机索引: " + currentCameraIndex); + } catch (Exception e) { + LogUtil.log(TAG, "配置推流参数异常: " + e.getMessage()); + e.printStackTrace(); + } + }); + } + + /** + * 执行启动流操作 + */ + private void doStartStream(ILiveStreamManager liveStreamManager, boolean isRestart) { + liveStreamManager.startStream(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + mainHandler.post(() -> { + LogUtil.log(TAG, "RTSP推流启动成功" + (isRestart ? "(重启)" : "")); + isStreaming.set(true); + startPortScanner(); + }); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + mainHandler.post(() -> { + int count = retryCount.incrementAndGet(); + LogUtil.log(TAG, "启动RTSP推流失败 (次数: " + count + "): " + new Gson().toJson(error)); + + if (count < MAX_RETRY_COUNT) { + // 延迟重试 + mainHandler.postDelayed(() -> { + streamExecutor.execute(() -> { + LogUtil.log(TAG, "重试启动推流 (次数: " + (count + 1) + ")"); + doStartRtspStream(); + }); + }, RETRY_DELAY_MS); + } else { + LogUtil.log(TAG, "达到最大重试次数,强制启动推流"); + forceStartStream(); + } + }); + } + }); + } + + /** + * 停止推流 + */ + public void stopStream() { + streamExecutor.execute(() -> { + ILiveStreamManager liveStreamManager = MediaDataCenter.getInstance().getLiveStreamManager(); + if (liveStreamManager != null) { + liveStreamManager.stopStream(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + mainHandler.post(() -> { + LogUtil.log(TAG, "推流停止成功"); + isStreaming.set(false); + SimplePortScanner.getInstance().stopScan(); + }); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + mainHandler.post(() -> { + LogUtil.log(TAG, "推流停止失败: " + new Gson().toJson(error)); + }); + } + }); + } + }); + } + + /** + * 切换相机 + */ + public void switchCamera(int cameraIndex) { + if (cameraIndex != 1 && cameraIndex != 2) { + LogUtil.log(TAG, "无效的相机索引: " + cameraIndex); + return; + } + + currentCameraIndex = cameraIndex; + LogUtil.log(TAG, "切换相机索引: " + cameraIndex); + + // 如果正在推流,重启推流 + if (isStreaming.get()) { + LogUtil.log(TAG, "切换相机后重启推流"); + stopStream(); + mainHandler.postDelayed(this::startRtspStream, SETTLE_DELAY_MS); + } + } + + /** + * 启动端口扫描 + */ + private void startPortScanner() { + try { + SimplePortScanner.getInstance().startScan(); + LogUtil.log(TAG, "端口扫描已启动"); + } catch (Exception e) { + LogUtil.log(TAG, "启动端口扫描失败: " + e.getMessage()); + } + } + + /** + * 验证RTSP配置 + */ + private boolean validateRtspConfig() { + return PreferenceUtils.getInstance().getRtspUserName() != null && + PreferenceUtils.getInstance().getRtspPort() != null && + PreferenceUtils.getInstance().getRtspPassWord() != null; + } + + /** + * 获取推流状态 + */ + public boolean isStreaming() { + return isStreaming.get(); + } + + /** + * 获取当前相机索引 + */ + public int getCurrentCameraIndex() { + return currentCameraIndex; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/manager/SpeakerManager.java b/app/src/main/java/com/aros/apron/manager/SpeakerManager.java new file mode 100644 index 00000000..641d9fa0 --- /dev/null +++ b/app/src/main/java/com/aros/apron/manager/SpeakerManager.java @@ -0,0 +1,314 @@ +package com.aros.apron.manager; + +import static com.aros.apron.tools.Utils.getIDJIErrorMsg; +import android.os.Environment; +import android.os.Handler; +import android.text.TextUtils; +import androidx.annotation.NonNull; +import com.aros.apron.base.BaseManager; +import com.aros.apron.entity.MessageDown; +import com.aros.apron.entity.Movement; +import com.aros.apron.entity.XTTSParams; +import com.aros.apron.tools.LogUtil; +import com.aros.apron.tools.SpeakerProgressReporter; +import com.aros.apron.tools.Utils; +import com.aros.apron.tools.XTtsPcmGenerator; +import com.iflytek.aikit.core.AiListener; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import dji.v5.common.callback.CommonCallbacks; +import dji.v5.common.error.IDJIError; +import dji.v5.common.recorder.PCMTools; +import dji.v5.manager.aircraft.megaphone.FileInfo; +import dji.v5.manager.aircraft.megaphone.MegaphoneIndex; +import dji.v5.manager.aircraft.megaphone.MegaphoneInfo; +import dji.v5.manager.aircraft.megaphone.MegaphoneInfoListener; +import dji.v5.manager.aircraft.megaphone.MegaphoneManager; +import dji.v5.manager.aircraft.megaphone.PlayMode; +import dji.v5.manager.aircraft.megaphone.UploadType; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + + +public class SpeakerManager extends BaseManager { + private SpeakerManager() { + } + private static class SpeakerHolder { + private static final SpeakerManager INSTANCE = new SpeakerManager(); + } + public static SpeakerManager getInstance() { + return SpeakerHolder.INSTANCE; + } + private int megaphoneStatus; + public void initMegaphoneInfo() { + MegaphoneManager.getInstance().addMegaphoneInfoListener(new MegaphoneInfoListener() { + @Override + public void onUpdateMegaphoneInfo(MegaphoneInfo megaphoneInfo) { + if (megaphoneInfo != null) { + megaphoneStatus = megaphoneInfo.getStatus().ordinal(); + } + } + }); + MegaphoneManager.getInstance().setMegaphoneIndex(MegaphoneIndex.PORT_2, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + LogUtil.log(TAG,"喊话器位置设置成功"); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG,"喊话器位置设置失败:"+getIDJIErrorMsg(error)); + } + }); + } + public void speakerAudioPlayStart(MessageDown message) { + if (TextUtils.isEmpty(message.getData().getFile().getUrl())) { + sendFailMsg2Server(message, "喊话文件下载地址为空"); + return; + } + Request request = new Request.Builder().url(message.getData().getFile().getUrl()).build(); + new OkHttpClient().newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + sendEvent2Server(".pcm文件下载失败:" + e.toString(), 2); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response != null) { + InputStream is = null; + byte[] buf = new byte[2048]; + int len = 0; + FileOutputStream fos = null; + // 储存下载文件的目录 + File dir = new File(Environment.getExternalStorageDirectory().getPath()); + if (!dir.exists()) { + dir.mkdirs(); + } + File file = new File(dir, "voice.pcm"); + try { + is = response.body().byteStream(); + fos = new FileOutputStream(file); + while ((len = is.read(buf)) != -1) { + fos.write(buf, 0, len); + } + fos.flush(); + sendEvent2Server("喊话.pcm文件下载成功", 1); + String opusFilePath = PCMTools.INSTANCE.convertToOpusFileSync(file.getAbsolutePath()); + + Movement.getInstance().setStep_key("download"); + Movement.getInstance().setTTS_status("in_progress"); + + FileInfo fileInfo = new FileInfo(UploadType.VOICE_FILE, + new File(opusFilePath), + null); + MegaphoneManager.getInstance().startPushingFileToMegaphone(fileInfo, + new CommonCallbacks.CompletionCallbackWithProgress() { + @Override + public void onProgressUpdate(Integer integer) { + + Movement.getInstance().setSpeakerpercent(integer); + + LogUtil.log(TAG, "喊话器内容上传进度:" + integer + "%"); + } + + @Override + public void onSuccess() { + 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); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendEvent2Server("喊话器播放音频失败:" + getIDJIErrorMsg(error), 2); + } + }); + } + }, 1000); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendFailMsg2Server(message, "喊话器内容上传失败:" + getIDJIErrorMsg(error)); + } + }); + + + } catch (Exception e) { + sendEvent2Server("喊话.pcm文件下载失败:" + e.toString(), 2); + } + } + } + }); + } + + + public void speakerReply(MessageDown message) { + MegaphoneManager.getInstance().startPlay(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendFailMsg2Server(message, "喊话器播放音频失败:" + getIDJIErrorMsg(error)); + } + }); + } + + public void speakerStop(MessageDown message) { + MegaphoneManager.getInstance().stopPlay(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + SpeakerProgressReporter.getInstance().stopAudioReport(); + SpeakerProgressReporter.getInstance().stopTTSReport(); + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + SpeakerProgressReporter.getInstance().stopAudioReport(); + SpeakerProgressReporter.getInstance().stopTTSReport(); + sendMsg2Server(message); + sendFailMsg2Server(message, "喊话器停止播放失败:" + getIDJIErrorMsg(error)); + } + }); + } + + public void speakerPlayModeSet(MessageDown message) { + MegaphoneManager.getInstance().setPlayMode(message.getData().getPlay_mode() == 0 ? + PlayMode.SINGLE : PlayMode.LOOP, new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + Movement.getInstance().setStep_key("change_work_mode"); + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendFailMsg2Server(message, "喊话器播放模式设置失败:" + getIDJIErrorMsg(error)); + } + }); + } + + public void speakerPlayVolumeSet(MessageDown message) { + MegaphoneManager.getInstance().setVolume(message.getData().getPlay_volume(), new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + sendMsg2Server(message); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendFailMsg2Server(message, "喊话器音量设置失败:" + getIDJIErrorMsg(error)); + } + }); + } + + private String text; + + public void speakerTTSPlayStart(MessageDown message, int type) { + message.getData().getTts().getMd5(); + XTTSParams params = new XTTSParams(); + if (type == 0) { + text = message.getData().getTts().getText(); + } else { + if (megaphoneStatus == 2) { + MegaphoneManager.getInstance().stopPlay(new CommonCallbacks.CompletionCallback() { + @Override + public void onSuccess() { + + LogUtil.log(TAG, "终止喊话"); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + LogUtil.log(TAG, "终止喊话失败:" + getIDJIErrorMsg(error)); + + } + }); + } + params.vcn = message.getData().getType() == 0 ? "xiaofeng" : "xiaoyan"; + params.language = message.getData().getLanguage() == 0 ? 1 : 2; + params.speed = message.getData().getSpeed(); + params.volume = message.getData().getVolume(); + } + if (TextUtils.isEmpty(text)) { + sendFailMsg2Server(message, "喊话失败:喊话内容为空"); + return; + } + XTtsPcmGenerator tts = new XTtsPcmGenerator(); + AiListener listener = tts.buildListener(); + tts.init(listener); + + File pcmFile = new File(Utils.getSDCardPath() + "/pcm", "tts_output_local.pcm"); + + int synthToPcm = tts.synthToPcm( + text, + params, + pcmFile + ); + if (synthToPcm == 0) { + sendMsg2Server(message); + String opusFilePath = PCMTools.INSTANCE.convertToOpusFileSync(pcmFile.getAbsolutePath()); + + FileInfo fileInfo = new FileInfo(UploadType.VOICE_FILE, + new File(opusFilePath), + null); + MegaphoneManager.getInstance().startPushingFileToMegaphone(fileInfo, + new CommonCallbacks.CompletionCallbackWithProgress() { + @Override + public void onProgressUpdate(Integer integer) { + Movement.getInstance().setTTS_status("in_progress"); + Movement.getInstance().setSpeakerpercent(integer); + Movement.getInstance().setStep_key("upload"); + LogUtil.log(TAG, "喊话器内容上传进度:" + integer + "%"); + } + + @Override + public void onSuccess() { + 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"); + Movement.getInstance().setStep_key("play"); + sendEvent2Server("喊话器播放TTS音频成功", 1); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendEvent2Server("喊话器播放TTS音频失败:" + getIDJIErrorMsg(error), 2); + } + }); + } + }, 1000); + } + + @Override + public void onFailure(@NonNull IDJIError error) { + sendFailMsg2Server(message, "喊话器内容上传失败:" + getIDJIErrorMsg(error)); + } + }); + } else { + sendFailMsg2Server(message, "tts合成失败"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/manager/UdpStreamManager.java b/app/src/main/java/com/aros/apron/manager/UdpStreamManager.java new file mode 100644 index 00000000..1b7242a2 --- /dev/null +++ b/app/src/main/java/com/aros/apron/manager/UdpStreamManager.java @@ -0,0 +1,331 @@ +package com.aros.apron.manager; + +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; + +import com.aros.apron.tools.LogUtil; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import dji.sdk.keyvalue.value.common.ComponentIndexType; +import dji.v5.manager.datacenter.MediaDataCenter; +import dji.v5.manager.datacenter.camera.StreamInfo; +import dji.v5.manager.interfaces.ICameraStreamManager; + +/** + * UDP视频流推送管理类 + * 负责从视频监听器获取码流数据并通过UDP协议推送 + */ +public class UdpStreamManager { + private static final String TAG = "UdpStreamManager"; + + // 线程池和主线程Handler + private final ExecutorService udpExecutor = Executors.newSingleThreadExecutor(); + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + // 单例模式 + private static class UdpStreamHolder { + private static final UdpStreamManager INSTANCE = new UdpStreamManager(); + } + + public static UdpStreamManager getInstance() { + return UdpStreamHolder.INSTANCE; + } + + // UDP配置 + private static final String DEFAULT_HOST = "127.0.0.1"; + private static final int DEFAULT_PORT_1_PORT = 8080; + private static final int DEFAULT_FPV_PORT = 8081; + private String targetHost = DEFAULT_HOST; + private int port1Port = DEFAULT_PORT_1_PORT; + private int fpvPort = DEFAULT_FPV_PORT; + + // 状态管理 + private final AtomicBoolean isStreaming = new AtomicBoolean(false); + private final AtomicBoolean isInitialized = new AtomicBoolean(false); + + // UDP相关 + private DatagramSocket port1UdpSocket; + private DatagramSocket fpvUdpSocket; + private InetAddress targetAddress; + + // 视频码流监听器 + private ICameraStreamManager.ReceiveStreamListener port1StreamListener; + private ICameraStreamManager.ReceiveStreamListener fpvStreamListener; + + private UdpStreamManager() { + } + + /** + * 初始化UDP流管理器 + * @param host 目标主机地址 + * @param port1Port PORT_1相机的目标端口 + * @param fpvPort FPV相机的目标端口 + */ + public void init(String host, int port1Port, int fpvPort) { + if (isInitialized.get()) { + LogUtil.log(TAG, "UDP流管理器已初始化"); + return; + } + + targetHost = host != null ? host : DEFAULT_HOST; + this.port1Port = port1Port > 0 ? port1Port : DEFAULT_PORT_1_PORT; + this.fpvPort = fpvPort > 0 ? fpvPort : DEFAULT_FPV_PORT; + + udpExecutor.execute(() -> { + try { + // 初始化UDP socket + port1UdpSocket = new DatagramSocket(); + fpvUdpSocket = new DatagramSocket(); + targetAddress = InetAddress.getByName(targetHost); + + // 初始化视频码流监听器 + initStreamListeners(); + + isInitialized.set(true); + LogUtil.log(TAG, "UDP流管理器初始化成功,目标地址: " + targetHost + ", PORT_1端口: " + this.port1Port + ", FPV端口: " + this.fpvPort); + } catch (SocketException e) { + LogUtil.log(TAG, "UDP socket初始化失败: " + e.getMessage()); + e.printStackTrace(); + } catch (UnknownHostException e) { + LogUtil.log(TAG, "无法解析目标地址: " + e.getMessage()); + e.printStackTrace(); + } + }); + } + + /** + * 初始化UDP流管理器(使用默认端口) + * @param host 目标主机地址 + */ + public void init(String host) { + init(host, DEFAULT_PORT_1_PORT, DEFAULT_FPV_PORT); + } + + /** + * 初始化UDP流管理器(使用默认地址和端口) + */ + public void init() { + init(DEFAULT_HOST, DEFAULT_PORT_1_PORT, DEFAULT_FPV_PORT); + } + + /** + * 初始化视频码流监听器 + */ + private void initStreamListeners() { + ICameraStreamManager cameraManager = MediaDataCenter.getInstance().getCameraStreamManager(); + if (cameraManager == null) { + LogUtil.log(TAG, "CameraStreamManager获取失败"); + return; + } + + port1StreamListener=new ICameraStreamManager.ReceiveStreamListener() { + @Override + public void onReceiveStream(@NonNull byte[] data, int offset, int length, @NonNull StreamInfo info) { + if (isStreaming.get()) { + // 截取有效数据 + byte[] streamData = new byte[length]; + System.arraycopy(data, offset, streamData, 0, length); + sendStreamData(streamData, ComponentIndexType.PORT_1); + } + } + }; + + // FPV相机码流监听器 + fpvStreamListener=new ICameraStreamManager.ReceiveStreamListener() { + @Override + public void onReceiveStream(@NonNull byte[] data, int offset, int length, @NonNull StreamInfo info) { + if (isStreaming.get()) { + // 截取有效数据 + byte[] streamData = new byte[length]; + System.arraycopy(data, offset, streamData, 0, length); + sendStreamData(streamData, ComponentIndexType.FPV); + } + } + }; + + + // 注册监听器 + cameraManager.addReceiveStreamListener(ComponentIndexType.PORT_1, port1StreamListener); + cameraManager.addReceiveStreamListener(ComponentIndexType.FPV, fpvStreamListener); + + LogUtil.log(TAG, "视频码流监听器注册成功"); + } + + /** + * 发送码流数据 + * @param streamData 码流数据 + * @param componentIndex 相机组件索引 + */ + private void sendStreamData(byte[] streamData, ComponentIndexType componentIndex) { + if (targetAddress == null) { + return; + } + + try { + // 选择对应的UDP socket和端口 + DatagramSocket socket = null; + int port = 0; + + if (componentIndex == ComponentIndexType.PORT_1) { + socket = port1UdpSocket; + port = port1Port; + } else if (componentIndex == ComponentIndexType.FPV) { + socket = fpvUdpSocket; + port = fpvPort; + } + + if (socket == null) { + return; + } + + // 构造UDP数据包 + // 前4字节表示相机索引(1: PORT_1, 2: FPV) + // 接下来4字节表示数据长度 + // 剩余部分为码流数据 + int headerSize = 8; + byte[] packetData = new byte[headerSize + streamData.length]; + + // 写入相机索引 + int cameraIndex = componentIndex == ComponentIndexType.PORT_1 ? 1 : 2; + packetData[0] = (byte) (cameraIndex >> 24); + packetData[1] = (byte) (cameraIndex >> 16); + packetData[2] = (byte) (cameraIndex >> 8); + packetData[3] = (byte) cameraIndex; + + // 写入数据长度 + packetData[4] = (byte) (streamData.length >> 24); + packetData[5] = (byte) (streamData.length >> 16); + packetData[6] = (byte) (streamData.length >> 8); + packetData[7] = (byte) streamData.length; + + // 写入码流数据 + System.arraycopy(streamData, 0, packetData, headerSize, streamData.length); + + // 发送UDP数据包 + DatagramPacket packet = new DatagramPacket( + packetData, + packetData.length, + targetAddress, + port + ); + + socket.send(packet); + + } catch (IOException e) { + LogUtil.log(TAG, "发送UDP数据包失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 开始UDP流推送 + */ + public void startStreaming() { + if (!isInitialized.get()) { + LogUtil.log(TAG, "UDP流管理器未初始化,请先调用init方法"); + return; + } + + isStreaming.set(true); + LogUtil.log(TAG, "UDP流推送已启动"); + } + + /** + * 停止UDP流推送 + */ + public void stopStreaming() { + isStreaming.set(false); + LogUtil.log(TAG, "UDP流推送已停止"); + } + + /** + * 关闭UDP流管理器 + */ + public void close() { + stopStreaming(); + + // 移除视频码流监听器 + ICameraStreamManager cameraManager = MediaDataCenter.getInstance().getCameraStreamManager(); + if (cameraManager != null) { + if (port1StreamListener != null) { + cameraManager.removeReceiveStreamListener(port1StreamListener); + } + if (fpvStreamListener != null) { + cameraManager.removeReceiveStreamListener(fpvStreamListener); + } + } + + // 关闭UDP socket + if (port1UdpSocket != null) { + port1UdpSocket.close(); + port1UdpSocket = null; + } + if (fpvUdpSocket != null) { + fpvUdpSocket.close(); + fpvUdpSocket = null; + } + + isInitialized.set(false); + LogUtil.log(TAG, "UDP流管理器已关闭"); + } + + /** + * 设置目标地址 + * @param host 目标主机地址 + * @param port1Port PORT_1相机的目标端口 + * @param fpvPort FPV相机的目标端口 + */ + public void setTargetAddress(String host, int port1Port, int fpvPort) { + if (host != null) { + targetHost = host; + } + if (port1Port > 0) { + this.port1Port = port1Port; + } + if (fpvPort > 0) { + this.fpvPort = fpvPort; + } + + udpExecutor.execute(() -> { + try { + targetAddress = InetAddress.getByName(targetHost); + LogUtil.log(TAG, "目标地址已更新: " + targetHost + ", PORT_1端口: " + this.port1Port + ", FPV端口: " + this.fpvPort); + } catch (UnknownHostException e) { + LogUtil.log(TAG, "无法解析新目标地址: " + e.getMessage()); + e.printStackTrace(); + } + }); + } + + /** + * 获取UDP流推送状态 + */ + public boolean isStreaming() { + return isStreaming.get(); + } + + /** + * 获取UDP流管理器初始化状态 + */ + public boolean isInitialized() { + return isInitialized.get(); + } + + /** + * 获取当前目标地址 + */ + public String getTargetAddress() { + return targetHost + ", PORT_1端口: " + port1Port + ", FPV端口: " + fpvPort; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/manager/VlcRtspManager.java b/app/src/main/java/com/aros/apron/manager/VlcRtspManager.java new file mode 100644 index 00000000..1e8586d2 --- /dev/null +++ b/app/src/main/java/com/aros/apron/manager/VlcRtspManager.java @@ -0,0 +1,133 @@ +package com.aros.apron.manager; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.aros.apron.tools.LogUtil; + +import org.videolan.libvlc.LibVLC; +import org.videolan.libvlc.Media; +import org.videolan.libvlc.MediaPlayer; + +import java.util.ArrayList; +import java.util.List; + +/** + * RTSP流检测工具(严格匹配LibVLC 3.6.0源码) + * 核心:单例、初始化、拉流检测、成功/失败回调(无任何冗余) + */ +public class VlcRtspManager { + private static volatile VlcRtspManager instance; + private static final long CHECK_TIMEOUT = 5000L; // 5秒超时 + + private LibVLC libVLC; + private MediaPlayer mediaPlayer; + private Handler handler = new Handler(Looper.getMainLooper()); + private OnRtspCheckListener checkListener; + private boolean isChecking = false; + + // 仅保留成功/失败回调 + public interface OnRtspCheckListener { + void onSuccess(); // 拉流成功 + void onFailed(); // 拉流失败 + } + + // 单例 + private VlcRtspManager() {} + public static VlcRtspManager getInstance() { + if (instance == null) { + synchronized (VlcRtspManager.class) { + if (instance == null) { + instance = new VlcRtspManager(); + } + } + } + return instance; + } + + // 初始化(3.6.0 源码级配置) + public void init(Context context) { + if (libVLC != null) return; + + List options = new ArrayList<>(); + options.add("--rtsp-tcp"); + options.add("--network-caching=500"); + options.add("--vout=none"); + options.add("--aout=none"); + options.add("--quiet"); + + libVLC = new LibVLC(context.getApplicationContext(), options); + mediaPlayer = new MediaPlayer(libVLC); + + // ====================== 核心修正:3.6.0 事件判定(源码级) ====================== + // 参考3.6.0源码:MediaPlayer.Event的type是int型,对应EventType枚举的ordinal() + mediaPlayer.setEventListener(new MediaPlayer.EventListener() { + @Override + public void onEvent(MediaPlayer.Event event) { + // 3.6.0源码中: + // EventType.Playing.ordinal() = 0 + // EventType.Error.ordinal() = 1 + // 直接用整型判定,避开枚举引用错误 + if (event.type == 0) { // Playing事件 + if (checkListener != null) checkListener.onSuccess(); + stopCheck(); + } else if (event.type == 1) { // Error事件 + if (checkListener != null) checkListener.onFailed(); + stopCheck(); + } + } + }); + } + + // 检测RTSP流(拉流测试) + public void checkRtspStream(String rtspUrl, OnRtspCheckListener listener) { + stopCheck(); + + this.checkListener = listener; + this.isChecking = true; + + // 超时检测 + handler.postDelayed(() -> { + if (isChecking) { + if (checkListener != null) checkListener.onFailed(); + stopCheck(); + } + }, CHECK_TIMEOUT); + + // 拉流(3.6.0 源码级写法) + try { + Media media = new Media(libVLC, rtspUrl); + mediaPlayer.setMedia(media); + media.release(); + mediaPlayer.play(); + } catch (Exception e) { + LogUtil.log("VlcRtspManager", "拉流异常:" + e.getMessage()); + if (checkListener != null) checkListener.onFailed(); + stopCheck(); + } + } + + // 停止检测(私有) + private void stopCheck() { + isChecking = false; + handler.removeCallbacksAndMessages(null); + if (mediaPlayer != null && mediaPlayer.isPlaying()) { + mediaPlayer.stop(); + } + } + + // 释放资源(可选) + public void release() { + stopCheck(); + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + if (libVLC != null) { + libVLC.release(); + libVLC = null; + } + instance = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java b/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java new file mode 100644 index 00000000..dc7fa2f4 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/ApronArucoDetectPort.java @@ -0,0 +1,827 @@ +package com.aros.apron.tools; + +import android.os.Handler; +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.manager.AlternateLandingManager; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.Point; +import org.opencv.imgproc.Imgproc; +import org.opencv.objdetect.ArucoDetector; +import org.opencv.objdetect.DetectorParameters; +import org.opencv.objdetect.Dictionary; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 多二维码融合版本(V2.0): + * 1. 【关键修改】先用6号二维码进行机头旋转对准,转到位(isYawAligned=true)才进入后续阶段 + * 2. 旋转阶段(vz=0)不下降,丢失时悬停等待 + * 3. 【禁用】5号二维码逻辑已注释掉 + * 4. 旋转完成后:识别1-4号/小码进行水平对准+下降 + */ +public class ApronArucoDetectPort { + + private static final String TAG = "ApronArucoDetect"; + + // ========== 原有参数(参数别改) ========== + private static double LENS_OFFSET_X = 0; + private static double LENS_OFFSET_Y = 0; + private static final int CENTER_ERR_MAX = 50; + 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; + private boolean isStartAruco = false; + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + Future lastFuture = null; + private String TAG_LOG = getClass().getSimpleName(); + private boolean triggerToAlternateLandingPoint; + long startTime; + long endTime; + private int sigleMarkerDetectFailsTimes; + private boolean dropTimesTag; + private int dropTimes = 0; + private boolean isDoublePayload; + private int trycount = 0; + + public PIDControl pidControlX = null; + public PIDControl pidControlY = null; + public int checkThrowingErrorsTimes; + private int consecutiveTriggerCount = 0; + private int frameCounter = 0; + + // ========== 【新增】阶段控制标志 ========== + // 偏航是否已对准(6号旋转阶段完成标志) + private volatile boolean isYawAligned = false; + // 当前激活的降落模式:0=旋转阶段(6号), 1=小码, 2=1-4大码, 3=6号纯下降 + private int currentLandingMode = 0; + + // ========== 【新增】连续降落条件判断 ========== + // 记录上一帧满足的降落条件类型:0=无, 1=单码精准降落(15/17号), 2=几何中心降落 + private int lastLandingCondition = 0; + + // ========== 标准方法 ========== + public int getCheckThrowingErrorsTimes() { return checkThrowingErrorsTimes; } + public void setCheckThrowingErrorsTimes(int v) { checkThrowingErrorsTimes = v; } + public boolean isTriggerSuccess() { return isTriggerSuccess; } + public void setTriggerSuccess(boolean v) { isTriggerSuccess = v; } + public boolean isDoublePayload() { return isDoublePayload; } + public void setDoublePayload(boolean v) { isDoublePayload = v; } + public boolean isStartFastStick() { return startFastStick; } + public void setStartFastStick(boolean v) { startFastStick = v; } + + private boolean setMF=false; + public boolean isCanLanding() { return canLanding; } + public boolean isStartAruco() { + return isStartAruco; + } + + public void setStartAruco(boolean startAruco) { + isStartAruco = startAruco; + } + public void setCanLanding(boolean v) { + startTime = 0; + endTime = 0; + canLanding = v; + } + + private ApronArucoDetectPort() {} + private static class OpenCVHelperHolder { + private static final ApronArucoDetectPort INSTANCE = new ApronArucoDetectPort(); + static { INSTANCE.init(); } + } + public static ApronArucoDetectPort getInstance() { return OpenCVHelperHolder.INSTANCE; } + + public void init() { + // 参数别改:PID初始值 + pidControlX = new PIDControl(0.7f, 0.02f, 0.22f, 0.05f, 2.5f, 0.1f); + pidControlY = new PIDControl(0.7f, 0.02f, 0.22f, 0.05f, 2.5f, 0.1f); + pidControlX.reset(); + pidControlY.reset(); + } + + public boolean startFastStick; + public boolean canLanding; + + // ========== 【新增】计算二维码像素宽度 ========== + private double calculatePixelWidth(Mat corner) { + try { + Point[] pts = new Point[4]; + for (int i = 0; i < 4; i++) { + double[] p = corner.get(0, i); + pts[i] = new Point(p[0], p[1]); + } + // 计算上下两边长度的平均值作为像素宽度 + double top = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) + Math.pow(pts[1].y - pts[0].y, 2)); + double bottom = Math.sqrt(Math.pow(pts[2].x - pts[3].x, 2) + Math.pow(pts[2].y - pts[3].y, 2)); + return (top + bottom) / 2.0; + } catch (Exception e) { + return 0.0; + } + } + // ========== 【核心】主入口:3帧一处理 ========== + public void detectArucoTags(int height, int width, byte[] data, Dictionary dictionary) { + isTriggerSuccess = true; + Movement.getInstance().setVirtualStickEnableReason(2); + + if (isStartAruco || startFastStick) { + LogUtil.log(TAG_LOG, "过滤:" + isStartAruco + startFastStick); + if (!isStartAruco && startFastStick) { + checkThrowingErrorsTimes++; + } + return; + } + // 改为 3 帧一处理(10Hz,接近 DJI 建议上限) + int currentFrame = frameCounter++; + if (currentFrame % 3 != 0) { // 0处理,1、2跳过 + return; + } + if (currentFrame % 30 != 0) { // 0处理,1、2跳过 + //DroneHelper.getInstance().setGimbalPitchdown(); + } + + isStartAruco = true; + if (lastFuture != null && !lastFuture.isDone()) { + lastFuture.cancel(true); + } + int ultHeight = Movement.getInstance().getUltrasonicHeight(); + + if(ultHeight <=10 && setMF==false){ + //切换mf对焦模式 + setMF=true; + } + if(isDoublePayload){ + if (ultHeight <=5) + { + LENS_OFFSET_X=-125; + LENS_OFFSET_Y=120; + }else if(ultHeight<10){ + LENS_OFFSET_X=-85; + LENS_OFFSET_Y=80; + } else if (ultHeight<20) { + LENS_OFFSET_X=-70; + LENS_OFFSET_Y=60; + } + }else{ + if (ultHeight <=5) + { + LENS_OFFSET_X=120; + LENS_OFFSET_Y = 100; + }else if(ultHeight<10){ + LENS_OFFSET_X=80; + LENS_OFFSET_Y = 80; + } else if (ultHeight<20) { + LENS_OFFSET_X=60; + LENS_OFFSET_Y = 60; + } + } + + lastFuture = executor.schedule(new Runnable() { + @Override + public void run() { + 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); + + MatOfInt ids = new MatOfInt(); + List corners = new ArrayList<>(); + DetectorParameters parameters = new DetectorParameters(); + ArucoDetector detector = new ArucoDetector(dictionary, parameters); + + detector.detectMarkers(grayImgMat, corners, ids); + + int ultHeight=Movement.getInstance().getUltrasonicHeight(); + + if (!ids.empty()) { + trycount = 0; + arucoNotFoundTag = false; + + List smallMarkers = new ArrayList<>(); + List bigMarkers1_4 = new ArrayList<>(); + // 【禁用】5号逻辑注释掉,但保留变量避免编译错误 + // List bigMarker5 = new ArrayList<>(); + List bigMarker6 = new ArrayList<>(); + + int[] idArray = ids.toArray(); + List excludeIds = isDoublePayload ? + Arrays.asList(12, 11, 21, 14, 13, 22) : + Arrays.asList(12, 11, 21, 20, 19, 25); + + // 【新增】记录所有检测到的码的像素信息 + StringBuilder detectInfo = new StringBuilder(); + + for (int i = 0; i < idArray.length; i++) { + int id = idArray[i]; + Mat corner = corners.get(i); + double pixelW = calculatePixelWidth(corner); + + if (id >= 11 && id <= 25 && !excludeIds.contains(id)) { + smallMarkers.add(new ArucoMarker(id, corner, 0.03f)); + detectInfo.append(String.format("小码%d(%.0fpx) ", id, pixelW)); + } else if (id >= 1 && id <= 4) { + bigMarkers1_4.add(new ArucoMarker(id, corner, 0.18f)); + detectInfo.append(String.format("大码%d(%.0fpx) ", id, pixelW)); + } + // 【禁用】5号二维码逻辑已注释 + /* + else if (id == 5) { + bigMarker5.add(new ArucoMarker(5, corner, 0.15f)); + detectInfo.append(String.format("5号(%.0fpx) ", pixelW)); + } + */ + else if (id == 6) { + bigMarker6.add(new ArucoMarker(6, corner, 0.24f)); + detectInfo.append(String.format("6号(%.0fpx) ", pixelW)); + } + } + // 输出检测到的所有码及其像素宽度 + if (detectInfo.length() > 0) { + LogUtil.log(TAG_LOG, "【检测到】" + detectInfo.toString()); + } + + // ========== 【关键修改】阶段0:6号旋转阶段(最高优先级) ========== + // 只要看到6号且还没对准,就只用6号旋转,不进入其他任何阶段 + if (!bigMarker6.isEmpty() && !isYawAligned) { + currentLandingMode = 0; + processMarker6YawOnly(bigMarker6.get(0), rgbMat.width(), rgbMat.height(), ultHeight); + } + // ========== 【关键】只有旋转完成后(isYawAligned=true),才识别其他码 ========== + else if (isYawAligned) { + // 优先级1:小码模式(高度<25dm且≥3个码) + if (!smallMarkers.isEmpty() && ultHeight < 25) { + currentLandingMode = 1; + processSmallMarkers(smallMarkers, rgbMat.width(), rgbMat.height(), ultHeight); + } + // 优先级2:1-4号几何中心下降 + else if (!bigMarkers1_4.isEmpty()) { + currentLandingMode = 2; + processBigMarkersCenter(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight); + } + // 优先级3:6号纯下降(旋转完成后,没看到1-4和小码时) + else if (!bigMarker6.isEmpty()) { + currentLandingMode = 3; + processLandingOnly(bigMarker6.get(0), rgbMat.width(), rgbMat.height(), ultHeight); + } + // 旋转完成后丢失其他码,但还有6号:继续用6号下降 + else { + handleLostMarker(ultHeight); + } + } else { + LogUtil.log(TAG_LOG, "【等待6号】未检测到6号二维码,无法开始旋转对准"); + handleLostMarker(ultHeight); + } + dropTimesTag = true; + } else { + // 丢失处理(包含旋转阶段保护) + handleLostMarker(ultHeight); + } + grayImgMat.release(); + rgbMat.release(); + yuvMat.release(); + ids.release(); + if (!corners.isEmpty()) { + for (Mat c : corners) if (c != null) c.release(); + } + isStartAruco = false; + + } catch (Exception e) { + LogUtil.log(TAG_LOG, "Exception: " + e); + isStartAruco = false; + } + } + }, 0, TimeUnit.MILLISECONDS); + } + + // ========== 【新增】6号专用旋转阶段(纯旋转,不下降) ========== + private void processMarker6YawOnly(ArucoMarker marker, int imgWidth, int imgHeight, int ultHeight) { + Mat corner = marker.getConner(); + Point[] pts = new Point[4]; + for (int i = 0; i < 4; i++) { + double[] p = corner.get(0, i); + pts[i] = new Point(p[0], p[1]); + } + + // 使用6号的角点计算yaw误差 + double yawError = calculateYawErrorFromCorners(pts); + double absYaw = Math.abs(yawError); + + // 计算6号的像素宽度用于日志 + double pixelWidth = calculatePixelWidth(corner); + + if (absYaw < 5.0) { + // 旋转到位,设置标志,下一帧将进入正常下降流程 + isYawAligned = true; + LogUtil.log(TAG_LOG, String.format("【6号旋转到位】误差=%.1f° pixel=%.0f 进入下降阶段", yawError, pixelWidth)); + // 发送悬停指令(停止旋转) + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 0f); + } else { + // 未对准:计算转速(分段) + float yawRate; + if (absYaw > 100.0) yawRate = 50.0f; + else if (absYaw > 50.0) yawRate = 30.0f; + else if (absYaw > 30.0) yawRate = 20.0f; + else if (absYaw > 20.0) yawRate = 10.0f; + else if (absYaw > 10.0) yawRate = 5.0f; + else if (absYaw > 5.0) yawRate = 3.0f; + else yawRate = 0.0f; + + // 【注意】如果实际测试还是单向转,把下一行改成:yawRate = yawError > 0 ? -yawRate : yawRate; + yawRate = yawError > 0 ? yawRate : -yawRate; + + LogUtil.log(TAG_LOG, String.format("【6号旋转中】误差=%.1f° 转速=%.1f pixel=%.0f ult=%d", + yawError, yawRate, pixelWidth, ultHeight)); + + // 【关键】只旋转,不水平移动,不下降(vz=0) + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, yawRate, 0f); + } + } + + private void processSmallMarkers(List markers, int imgWidth, int imgHeight, int ultHeight) { + // 【关键修改】必须检测到3个及以上小码才处理,否则进入丢失保护 + if (markers == null || markers.size() < 3) { + LogUtil.log(TAG_LOG, "【小码数量不足】检测到" + (markers == null ? 0 : markers.size()) + "个,需要≥3个"); + // 数量不足时,如果有6号就用6号下降,否则悬停 + return; + } + + // 1. 计算几何中心和统计信息 + double sumX = 0, sumY = 0; + int validCount = 0; + double totalPixelWidth = 0; + + ArucoMarker target15 = null; // 单挂目标 + ArucoMarker target17 = null; // 双挂目标 + + for (ArucoMarker marker : markers) { + Mat corner = marker.getConner(); + totalPixelWidth += calculatePixelWidth(corner); + + 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; + sumX += cx; + sumY += cy; + validCount++; + + int id = marker.getId(); + if (id == 15) target15 = marker; + if (id == 17) target17 = marker; + } + + if (validCount == 0) return; + + double avgPixelWidth = totalPixelWidth / validCount; + double geoCenterX = sumX / validCount; + double geoCenterY = sumY / validCount; + double offsetX = geoCenterX - imgWidth / 2.0 - LENS_OFFSET_X; + double offsetY = geoCenterY - imgHeight / 2.0 + LENS_OFFSET_Y; + + double absX = Math.abs(offsetX); + double absY = Math.abs(offsetY); + double z = ultHeight / 10.0; + + // 降落条件判断(连续帧) + 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]; + } + 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); + } + } + + // 双挂模式:检查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]; + } + 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); + } + } + + // 连续帧判断 + 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) { + startFastStick = true; + consecutiveTriggerCount = 0; + lastLandingCondition = 0; + + LogUtil.log(TAG_LOG, currentCondition == 1 ? + "【降落触发】单双挂精准条件满足" : + "【降落触发】几何中心对准,码数量=" + validCount); + + handler.postDelayed(() -> handler.post(runnable), 300); + return; + } + } + + double outX = 0, outY = 0, outZ = 0; + + // 分段PID控制(原有逻辑保持不变) + if(z <= 0.4){ + pidControlX.setInputFilterAll((float)offsetX/1750); + pidControlY.setInputFilterAll(-(float)offsetY/1750); + if (pidControlX.get_pid()<0){ + if (pidControlX.get_pid()<-0.125){ + outX=absX<120?0:-0.125; + }else{ + outX=absX<120?0:pidControlX.get_pid(); + } + }else{ + if (pidControlX.get_pid()>0.125){ + outX=absX<120?0:0.125; + }else{ + outX=absX<120?0:pidControlX.get_pid(); + } + } + if (pidControlY.get_pid()<0){ + if (pidControlY.get_pid()<-0.125){ + outY=absY<120?0:-0.125; + }else{ + outY=absY<120?0:pidControlY.get_pid(); + } + }else{ + if (pidControlY.get_pid()>0.125){ + outY=absY<120?0:0.125; + }else{ + outY=absY<120?0:pidControlY.get_pid(); + } + } + }else if(z <= 0.7){ + pidControlX.setInputFilterAll((float)offsetX/1200); + pidControlY.setInputFilterAll(-(float)offsetY/1200); + double pidX = pidControlX.get_pid(); + double pidY = pidControlY.get_pid(); + + if (pidX < 0){ + outX = (pidX < -0.135) ? (absX < 120 ? 0 : -0.135) : (absX < 120 ? 0 : pidX); + }else{ + outX = (pidX > 0.135) ? (absX < 120 ? 0 : 0.135) : (absX < 120 ? 0 : pidX); + } + + if (pidY < 0){ + outY = (pidY < -0.135) ? (absY < 120 ? 0 : -0.135) : (absY < 120 ? 0 : pidY); + }else{ + outY = (pidY > 0.135) ? (absY < 120 ? 0 : 0.135) : (absY < 120 ? 0 : pidY); + } + outZ = (absX < 250 && absY < 250) ? -0.1 : 0; + + }else if(z <= 1.0){ + pidControlX.setInputFilterAll((float)offsetX/1200); + pidControlY.setInputFilterAll(-(float)offsetY/1200); + double pidX = pidControlX.get_pid(); + double pidY = pidControlY.get_pid(); + + if (pidX < 0){ + outX = (pidX < -0.145) ? (absX < 120 ? 0 : -0.145) : (absX < 120 ? 0 : pidX); + }else{ + outX = (pidX > 0.145) ? (absX < 120 ? 0 : 0.145) : (absX < 120 ? 0 : pidX); + } + + if (pidY < 0){ + outY = (pidY < -0.175) ? (absY < 120 ? 0 : -0.175) : (absY < 120 ? 0 : pidY); + }else{ + outY = (pidY > 0.175) ? (absY < 120 ? 0 : 0.175) : (absY < 120 ? 0 : pidY); + } + outZ = (absX < 200 && absY < 200) ? -0.2 : 0; + + }else if(z <= 1.5){ + pidControlX.setInputFilterAll((float)offsetX/1450); + pidControlY.setInputFilterAll(-(float)offsetY/1450); + double pidX = pidControlX.get_pid(); + double pidY = pidControlY.get_pid(); + + if (pidX < 0){ + outX = (pidX < -0.185) ? (absX < 120 ? 0 : -0.185) : (absX < 120 ? 0 : pidX); + }else{ + outX = (pidX > 0.185) ? (absX < 120 ? 0 : 0.185) : (absX < 120 ? 0 : pidX); + } + if (pidY < 0){ + outY = (pidY < -0.185) ? (absY < 120 ? 0 : -0.185) : (absY < 120 ? 0 : pidY); + }else{ + outY = (pidY > 0.185) ? (absY < 120 ? 0 : 0.185) : (absY < 120 ? 0 : pidY); + } + outZ = (absX < 180 && absY < 180) ? -0.4 : 0; + + }else if(z <= 2.0){ + pidControlX.setInputFilterAll((float)offsetX/1350); + pidControlY.setInputFilterAll(-(float)offsetY/1350); + outX = absX < 120 ? 0 : pidControlX.get_pid(); + outY = absY < 120 ? 0 : pidControlY.get_pid(); + outZ = (absX < 200 && absY < 200) ? -0.4 : 0; + + }else if(z <= 3.0){ + pidControlX.setInputFilterAll((float)offsetX/1250); + pidControlY.setInputFilterAll(-(float)offsetY/1250); + outX = absX < 120 ? 0 : pidControlX.get_pid(); + outY = absY < 120 ? 0 : pidControlY.get_pid(); + outZ = (absX < 200 && absY < 200) ? -0.4 : 0; + + }else { + pidControlX.setInputFilterAll((float)offsetX/850); + pidControlY.setInputFilterAll(-(float)offsetY/850); + outX = absX < 80 ? 0 : pidControlX.get_pid(); + outY = absY < 80 ? 0 : pidControlY.get_pid(); + outZ = (absX < 130 && absY < 130) ? -0.4 : 0; + } + + LogUtil.log(TAG_LOG, String.format("【执行移动】小码 vx=%.3f vy=%.3f vz=%.3f pixel=%.0f ult=%d errX=%.0f errY=%.0f", + outX, outY, outZ, avgPixelWidth, ultHeight, absX, absY)); + DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0, (float)outZ); + } + + // ========== 【修改】1-4号几何中心处理(旋转完成后才进入) ========== + private void processBigMarkersCenter(List markers, int imgWidth, int imgHeight, int ultHeight) { + double sumX = 0, sumY = 0; + StringBuilder markerInfo = new StringBuilder(); + + for (ArucoMarker marker : markers) { + Mat corner = marker.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]; + } + sumX += cx / 4.0; + sumY += cy / 4.0; + + double pixelW = calculatePixelWidth(corner); + markerInfo.append(String.format("[ID%d:%.0fpx]", marker.getId(), pixelW)); + } + + double geoCenterX = sumX / markers.size(); + double geoCenterY = sumY / markers.size(); + double offsetX = geoCenterX - imgWidth / 2.0; + double offsetY = geoCenterY - imgHeight / 2.0; + double absX = Math.abs(offsetX); + double absY = Math.abs(offsetY); + + double z = ultHeight / 10.0; + double outX = 0, outY = 0, outZ = 0; + + if (z >= 5.0) { + pidControlX.setInputFilterAll((float)offsetX/300); + pidControlY.setInputFilterAll(-(float)offsetY/300); + outX = absX < 50 ? 0 : pidControlX.get_pid(); + 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; + }else if(z>=4.0){ + pidControlX.setInputFilterAll((float)offsetX/600); + pidControlY.setInputFilterAll(-(float)offsetY/600); + outX = absX < 50 ? 0 : pidControlX.get_pid(); + 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 < 120 && absY < 120) ? -0.4 : 0; + }else if(z>=2.0){ + pidControlX.setInputFilterAll((float)offsetX/900); + pidControlY.setInputFilterAll(-(float)offsetY/900); + outX = absX < 50 ? 0 : pidControlX.get_pid(); + 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 < 140 && absY < 140) ? -0.4 : 0; + }else if(z>=1.0){ + pidControlX.setInputFilterAll((float)offsetX/1000); + pidControlY.setInputFilterAll(-(float)offsetY/1000); + outX = absX < 50 ? 0 : pidControlX.get_pid(); + 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 < 150 && absY < 150) ? -0.1 : 0; + }else if(z>=0.5){ + outZ = (absX < 150 && absY < 150) ? -0.1 : 0; + } + + LogUtil.log(TAG_LOG, String.format("【执行移动】大码几何中心 vx=%.3f vy=%.3f vz=%.3f ult=%d %s errX=%.0f errY=%.0f", + outX, outY, outZ, ultHeight, markerInfo.toString(), absX, absY)); + DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0f, (float)outZ); + } + + // ========== 【修改】6号纯下降模式(旋转完成后使用) ========== + private void processLandingOnly(ArucoMarker marker, int imgWidth, int imgHeight, int ultHeight) { + Mat corner = marker.getConner(); + Point[] pts = new Point[4]; + for (int i = 0; i < 4; i++) { + double[] p = corner.get(0, i); + pts[i] = new Point(p[0], p[1]); + } + + double pixelWidth = calculatePixelWidth(corner); + double cx = 0, cy = 0; + for (int i = 0; i < 4; i++) { + cx += pts[i].x; + cy += pts[i].y; + } + cx /= 4.0; + cy /= 4.0; + double offsetX = cx - imgWidth / 2.0; + double offsetY = cy - imgHeight / 2.0; + + float vz; + + if (ultHeight >= 50) { + vz = -0.7f; + } else if(ultHeight<=20){ + vz = 0.0f; + }else{ + vz=-0.3f; + } + + LogUtil.log(TAG_LOG, String.format("【执行移动】6号纯下降 vz=%.3f pixel=%.0f ult=%d errX=%.0f errY=%.0f", + vz, pixelWidth, ultHeight, Math.abs(offsetX), Math.abs(offsetY))); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, vz); + } + + private double calculateYawErrorFromCorners(Point[] pts) { + double dxTop = pts[1].x - pts[0].x; + double dyTop = pts[1].y - pts[0].y; + double angleTop = Math.toDegrees(Math.atan2(dyTop, dxTop)); + double dxBottom = pts[2].x - pts[3].x; + double dyBottom = pts[2].y - pts[3].y; + double angleBottom = Math.toDegrees(Math.atan2(dyBottom, dxBottom)); + double yawError = (angleTop + angleBottom) / 2.0; + while (yawError > 180) yawError -= 360; + while (yawError < -180) yawError += 360; + return yawError; + } + + // ========== 【修改】handleLostMarker 增加旋转阶段保护 ========== + private void handleLostMarker(int ultHeight) { + if (!arucoNotFoundTag) { + startTime = System.currentTimeMillis(); + arucoNotFoundTag = true; + } + endTime = System.currentTimeMillis(); + long lostDuration = endTime - startTime; + + // 【关键新增】旋转阶段保护:还没对准(isYawAligned=false)时,悬停等待,不下降 + if (!isYawAligned) { + LogUtil.log(TAG_LOG, "【旋转阶段丢失】未对准6号,悬停等待 vz=0 lostDuration=" + lostDuration); + // 悬停(不上升也不下降) + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 0f); + + // 10秒还没看到6号,去备降点 + if (lostDuration > 10000) { + LogUtil.log(TAG_LOG, "【旋转超时】10秒未检测到6号,飞往备降点"); + AlternateLandingManager.getInstance().startTaskProcess(null); + Movement.getInstance().setAlternate(true); + } + return; + } + + if(isYawAligned){ + // 原有逻辑(旋转完成后的丢失处理) + if (lostDuration > 1000 && lostDuration <= 12000) { + if (ultHeight <= 20) { + // 低空丢失:上升 + LogUtil.log(TAG_LOG, "【执行移动】丢失上升 vz=3.0"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f); + if (dropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) { + LogUtil.log(TAG_LOG, "超过复降限制,去备降点"); + AlternateLandingManager.getInstance().startTaskProcess(null); + Movement.getInstance().setAlternate(true); + return; + } + if (dropTimesTag) { + dropTimesTag = false; + dropTimes++; + LogUtil.log(TAG_LOG, "复降第:" + dropTimes + "次"); + } + } else { + // 高空丢失:下降寻找 + LogUtil.log(TAG_LOG, "【执行移动】丢失下降 vz=-0.3"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); + } + } else if (lostDuration > 8000) { + LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点"); + AlternateLandingManager.getInstance().startTaskProcess(null); + Movement.getInstance().setAlternate(true); + } + } +// if(lostDuration > 8000) { +// LogUtil.log(TAG_LOG, "判定未识别到二维码,飞往备降点"); +// AlternateLandingManager.getInstance().startTaskProcess(null); +// Movement.getInstance().setAlternate(true); +// } + } + + private double calculateDistance(Point p1, Point p2) { + double dx = p2.x - p1.x; + double dy = p2.y - p1.y; + return Math.sqrt(dx * dx + dy * dy); + } + + public void setDetectedBigMarkers() { + startFastStick = false; + isStartAruco = false; + consecutiveTriggerCount = 0; + frameCounter = 0; + // 【关键】重置旋转标志,下次降落重新用6号旋转 + isYawAligned = false; + currentLandingMode = 0; + lastLandingCondition = 0; + } + + private int handlerCallbackCount = 0; + private Handler handler = new Handler(Looper.getMainLooper()); + private Runnable runnable = new Runnable() { + @Override + public void run() { + performOperation(); + if (handlerCallbackCount < 20) { + handler.postDelayed(this, 50); + } else { + performNextStep(); + } + } + }; + + private void performOperation() { + LogUtil.log(TAG_LOG, String.format("【执行移动】速降 vz=-4 count=%d/20", handlerCallbackCount)); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -5); + handlerCallbackCount++; + } + + private void performNextStep() { + canLanding = true; + handlerCallbackCount = 0; + dropTimes = 0; + handler.removeCallbacks(runnable); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/ApronArucoStatus.java b/app/src/main/java/com/aros/apron/tools/ApronArucoStatus.java new file mode 100644 index 00000000..d22d4c07 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/ApronArucoStatus.java @@ -0,0 +1,83 @@ +package com.aros.apron.tools; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * ArUco 双路相机状态管理 + * volatile 保证内存可见性,防止双路同时工作 + */ +public class ApronArucoStatus { + + private static final ApronArucoStatus INSTANCE = new ApronArucoStatus(); + + /** + * 当前激活的相机源 + * volatile 保证多线程可见性 + */ + private volatile CameraSource activeSource = CameraSource.DOWNWARD; + + /** + * 复降次数 - AtomicInteger 保证原子性 + */ + private final AtomicInteger tryTimes = new AtomicInteger(0); + + public enum CameraSource { + DOWNWARD, // 下视觉 FPV + GIMBAL // 云台 PORT_1 + } + + private ApronArucoStatus() {} + + public static ApronArucoStatus getInstance() { + return INSTANCE; + } + + /** + * 检查是否允许处理(核心互斥) + */ + public boolean canProcess(CameraSource source) { + return activeSource == source; + } + + /** + * 切换源(立即生效,内存可见) + */ + public void switchTo(CameraSource source) { + this.activeSource = source; + } + + /** + * 获取当前激活源 + */ + public CameraSource getActiveSource() { + return activeSource; + } + + // ========== 复降次数(线程安全) ========== + + public int getTryTimes() { + return tryTimes.get(); + } + + public void setTryTimes(int value) { + this.tryTimes.set(value); + } + + /** + * 原子性 +1 + */ + public void incrementTryTimes() { + tryTimes.incrementAndGet(); + } + + /** + * 原子性 -1 + */ + public void decrementTryTimes() { + tryTimes.decrementAndGet(); + } + + public void resetTryTimes() { + tryTimes.set(0); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/DualCaptureHelper.java b/app/src/main/java/com/aros/apron/tools/DualCaptureHelper.java new file mode 100644 index 00000000..a4aed1b8 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/DualCaptureHelper.java @@ -0,0 +1,270 @@ +package com.aros.apron.tools; + +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; + +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.Size; +import org.opencv.imgcodecs.Imgcodecs; +import org.opencv.imgproc.Imgproc; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.os.Handler; +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.manager.AlternateLandingManager; + +import org.opencv.calib3d.Calib3d; +import org.opencv.core.Core; +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.MatOfPoint2f; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.imgproc.Imgproc; +import org.opencv.objdetect.ArucoDetector; +import org.opencv.objdetect.DetectorParameters; +import org.opencv.objdetect.Dictionary; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 双图保存工具 - 单例版 + * 持续接收帧,按钮触发保存 + * 替换为【极致容错】图像增强逻辑,保留增强后图像的保存 + */ +public class DualCaptureHelper { + + private static final String TAG = "DualCapture"; + private static final String TAG_LOG = "EnhanceError"; // 增强失败日志TAG + private static DualCaptureHelper instance; + + private int count = 0; + private volatile boolean shouldCapture = false; // 按钮触发标志 + private Handler mainHandler = new Handler(Looper.getMainLooper()); + + // 帧数据缓存(最新一帧) + private int lastHeight = 0; + private int lastWidth = 0; + private byte[] lastFrameData = null; + + private DualCaptureHelper() {} + + public static synchronized DualCaptureHelper getInstance() { + if (instance == null) { + instance = new DualCaptureHelper(); + } + return instance; + } + + /** + * 持续接收帧(在帧回调里调用,每次都有) + * 参数别改:和你 detectArucoTags 一致 + */ + public void onFrame(int height, int width, byte[] yuvData) { + // 缓存最新帧 + this.lastHeight = height; + this.lastWidth = width; + this.lastFrameData = yuvData; + + // 如果按钮触发了,保存这一帧 + if (shouldCapture && lastFrameData != null) { + shouldCapture = false; // 重置,只保存一次 + + final int num = ++count; + final String time = new SimpleDateFormat("HHmmss", Locale.CHINA).format(new Date()); + + // 子线程保存 + new Thread(() -> { + saveRaw(lastHeight, lastWidth, lastFrameData, time, num); + saveEnhanced(lastHeight, lastWidth, lastFrameData, time, num); + + mainHandler.post(() -> { + LogUtil.log(TAG, "【完成】第" + num + "组"); + }); + }).start(); + } + } + + /** + * 按钮调用:触发下一帧保存 + * 绑定到你的按钮点击 + */ + public void captureNextFrame() { + shouldCapture = true; + LogUtil.log(TAG, "【等待】下一帧保存..."); + } + + /** + * 原始图:YUV直接转BGR,无任何处理 + */ + private void saveRaw(int height, int width, byte[] yuvData, String time, int num) { + try { + Mat yuv = new Mat(height + height / 2, width, CvType.CV_8UC1); + yuv.put(0, 0, yuvData); + + Mat bgr = new Mat(); + Imgproc.cvtColor(yuv, bgr, Imgproc.COLOR_YUV2BGR_I420); + yuv.release(); + + File dir = getSaveDir(); + String name = String.format("raw_%s_%03d.jpg", time, num); + String path = new File(dir, name).getAbsolutePath(); + + Imgcodecs.imwrite(path, bgr, new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 95)); + bgr.release(); + + LogUtil.log(TAG, "【原始】" + path); + + } catch (Exception e) { + LogUtil.log(TAG, "【原始失败】" + e.getMessage()); + } + } + + /** + * 增强图:应用【极致容错】图像增强流程 + */ + private void saveEnhanced(int height, int width, byte[] yuvData, String time, int num) { + try { + Mat yuv = new Mat(height + height / 2, width, CvType.CV_8UC1); + yuv.put(0, 0, yuvData); + + Mat gray = new Mat(); + Imgproc.cvtColor(yuv, gray, Imgproc.COLOR_YUV2GRAY_I420); + yuv.release(); + + // 替换为极致容错的增强逻辑 + Mat enhanced = createEnhancedImage(gray); + gray.release(); + + Mat bgr = new Mat(); + Imgproc.cvtColor(enhanced, bgr, Imgproc.COLOR_GRAY2BGR); + enhanced.release(); + + File dir = getSaveDir(); + String name = String.format("enhanced_%s_%03d.jpg", time, num); + String path = new File(dir, name).getAbsolutePath(); + + Imgcodecs.imwrite(path, bgr, new MatOfInt(Imgcodecs.IMWRITE_JPEG_QUALITY, 95)); + bgr.release(); + + LogUtil.log(TAG, "【增强】" + path); + + } catch (Exception e) { + LogUtil.log(TAG, "【增强失败】" + e.getMessage()); + } + } + + // ========== 【极致容错】图像增强:全高度段通用 ========== + private Mat createEnhancedImage(Mat src) { + Mat result = new Mat(); + try { + // 1. 多尺度CLAHE(适应不同亮度) + Mat claheMat = new Mat(); + Imgproc.createCLAHE(2.0, new Size(8, 8)).apply(src, claheMat); + + // 2. 中值滤波去噪(比双边快,保边足够) + Mat filtered = new Mat(); + Imgproc.medianBlur(claheMat, filtered, 5); + claheMat.release(); + + // 3. 自适应阈值(块大小动态,适应全高度) + Mat binary = new Mat(); + Imgproc.adaptiveThreshold(filtered, binary, 255, + Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C, + Imgproc.THRESH_BINARY, 41, 3); // 块41更大,常数3更敏感 + filtered.release(); + + // 4. 形态学操作(连接断裂边框) + Mat morph = new Mat(); + Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(5, 5)); + Imgproc.morphologyEx(binary, morph, Imgproc.MORPH_CLOSE, kernel); + kernel.release(); + binary.release(); + + // 5. 轻度锐化(突出边缘,不过度) + Mat sharpened = new Mat(); + Mat blurred = new Mat(); + Imgproc.GaussianBlur(morph, blurred, new Size(0, 0), 3.0); + Core.addWeighted(morph, 1.3, blurred, -0.3, 0, sharpened); + morph.release(); + blurred.release(); + + return sharpened; + + } catch (Exception e) { + LogUtil.log(TAG_LOG, "增强失败: " + e.getMessage()); + src.copyTo(result); + return result; + } + } + + // ========== 【超高容错】ArUco检测参数配置(保留方法,按需调用) ========== + private DetectorParameters createUltraTolerantParams() { + DetectorParameters params = new DetectorParameters(); + + // 全高度段:4dm=375px, 9dm=190px, 50dm=19px + params.set_minMarkerPerimeterRate(0.003f); // 降到0.003,19像素也能检 + + // 畸变/反光/模糊宽容 + params.set_polygonalApproxAccuracyRate(0.12f); // 更松 + + params.set_cornerRefinementMethod(1); // SUBPIX + params.set_cornerRefinementWinSize(3); // 降到3更快 + params.set_cornerRefinementMaxIterations(30); + params.set_cornerRefinementMinAccuracy(0.12f); // 放宽收敛 + + // 阈值范围更大,适应全光照 + params.set_adaptiveThreshWinSizeMin(3); + params.set_adaptiveThreshWinSizeMax(63); // 更大 + params.set_adaptiveThreshWinSizeStep(10); + params.set_adaptiveThreshConstant(3); // 配合预处理 + + params.set_minCornerDistanceRate(0.02f); + params.set_minMarkerLengthRatioOriginalImg(0.03f); // 更宽容 + params.set_minDistanceToBorder(1); // 边缘也检 + + params.set_perspectiveRemovePixelPerCell(2); // 降到2,小像素精细 + params.set_perspectiveRemoveIgnoredMarginPerCell(0.2f); + + params.set_maxErroneousBitsInBorderRate(0.6f); // 60%容错 + + params.set_detectInvertedMarker(false); + + return params; + } + + private File getSaveDir() { + File dir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), + "DJIDemo/DualCapture"); + if (!dir.exists()) dir.mkdirs(); + return dir; + } + + public void reset() { + count = 0; + LogUtil.log(TAG, "【重置】"); + } + + public int getCount() { + return count; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/Generakmzaltheratools.java b/app/src/main/java/com/aros/apron/tools/Generakmzaltheratools.java new file mode 100644 index 00000000..9aea9fc6 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/Generakmzaltheratools.java @@ -0,0 +1,230 @@ +package com.aros.apron.tools; + +import android.util.Log; + +import com.aros.apron.base.BaseManager; +import com.aros.apron.entity.MissionDataBean; +import com.aros.apron.entity.Movement; +import com.aros.apron.manager.TakeOffToPointManager; +import com.aros.apron.manager.TaskFailManager; +import com.dji.wpmzsdk.common.data.Template; +import com.dji.wpmzsdk.common.utils.kml.GeoidManager; +import com.dji.wpmzsdk.common.utils.kml.GpsUtils; +import com.dji.wpmzsdk.manager.WPMZManager; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import dji.sdk.keyvalue.key.FlightControllerKey; +import dji.sdk.keyvalue.key.KeyTools; +import dji.sdk.keyvalue.value.common.LocationCoordinate3D; +import dji.sdk.wpmz.value.mission.*; +import dji.v5.manager.KeyManager; + +/** + * 单航点起飞→目标点→返航 + * 纯 Java、无 Builder、无 WaypointHeadingParam + */ +public class Generakmzaltheratools extends BaseManager { + private Generakmzaltheratools() {} + private static class Holder { private static final Generakmzaltheratools INSTANCE = new Generakmzaltheratools(); } + public static Generakmzaltheratools getInstance() { return Holder.INSTANCE; } + + public Boolean generateKmz() { + LogUtil.log(TAG, "备降点经纬度:" + PreferenceUtils.getInstance().getAlternatePointLat() + + "/" + PreferenceUtils.getInstance().getAlternatePointLon()); + LogUtil.log("qwq","开始生成备降点航线"); + sendEvent2Server("开始生成备降点航线", 1); + LocationCoordinate3D lat = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey. + KeyAircraftLocation3D)); + + int retryCount = 0; + final int MAX_RETRY = 60; + + while (retryCount < MAX_RETRY) { + lat = KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D)); + if (lat != null) { + break; + } + retryCount++; + sendEvent2Server("生成失败:未获取到飞机当前位置(重试" + retryCount + "次)", 1); + if (retryCount >= MAX_RETRY) { + sendEvent2Server("生成失败:未获取到飞机当前位置(重试" + MAX_RETRY + "次超时)", 1); + TaskFailManager.getInstance().sendTaskFailMsg2Server(-1); + return false; + } + + try { + Thread.sleep(1000); // 休眠1秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LogUtil.log("qwq", "获取位置线程被中断"); + sendEvent2Server("生成失败:获取位置被中断", 1); + TaskFailManager.getInstance().sendTaskFailMsg2Server(-1); + return false; + } + } + + + if (lat == null) { + LogUtil.log("qwq", "当前位置为空,无法生成航线"); + sendEvent2Server("生成失败:未获取到飞机当前位置", 1); + return false; + } + if (lat.getLatitude() == null || lat.getLongitude() == null|| lat.getAltitude() == null) { + LogUtil.log("qwq", "当前位置坐标无效"); + sendEvent2Server("生成失败:位置坐标无效", 1); + return false; + } + + File root = new File("/storage/self/primary/DJIDemo/cache/kmz"); + if (!root.exists()) root.mkdirs(); + File kmz = new File(root, "alternate.kmz"); + LogUtil.log(TAG,"高度:"+lat.getAltitude()); + + /* 1. 基本信息 */ + WaylineMission mission = new WaylineMission(); + mission.setAuthor("aros"); + double now = System.currentTimeMillis(); + mission.setCreateTime(now); + mission.setUpdateTime(now); + + /* 2. 全局策略 */ + WaylineMissionConfig config = new WaylineMissionConfig(); + //飞向目标点的模式 + config.setFlyToWaylineMode(WaylineFlyToWaylineMode.SAFELY); + //航线结束后的动作 + config.setFinishAction(WaylineFinishedAction.AUTO_LAND); + config.setExitOnRCLostBehavior(WaylineExitOnRCLostBehavior.GO_ON); + config.setExitOnRCLostType(WaylineExitOnRCLostAction.LANDING); + + //全局过度速度 + config.setGlobalTransitionalSpeed(7.0); + //全局返航高度 + config.setGlobalRTHHeight(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointSecurityHeight())); + config.setIsGlobalRTHHeightSet(true); + //安全起飞高度 + config.setSecurityTakeOffHeight(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointSecurityHeight())); + config.setIsSecurityTakeOffHeightSet(true); + + + + //为400准备的 + WaylineDroneInfo droneInfo = new WaylineDroneInfo(); + droneInfo.setDroneType(WaylineDroneType.PM440); + config.setDroneInfo(droneInfo); + + + //这个航点列表 + WaylineWaypoint wp1 = new WaylineWaypoint(); + WaylineWaypoint wp = new WaylineWaypoint(); + + //初始点 + double wp1heighet= GpsUtils.egm96Altitude(lat.getAltitude(),lat.getLatitude(),lat.getLongitude()); + //double wpheight=GpsUtils.egm96Altitude(data.getTargetHeight(),data.getTargetLatitude(),data.getTargetLongitude()); + + //double wp1ellheighet=GpsUtils.wgs84Altitude(); + double wpellheight=GpsUtils.wgs84Altitude(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointSecurityHeight()),Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointLat()),Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointLon())); + + wp1.setWaypointIndex(0); + wp1.setLocation(new WaylineLocationCoordinate2D(lat.getLatitude(), lat.getLongitude())); + //wp1.setLocation(new WaylineLocationCoordinate2D(data.getTargetLatitude(), data.getTargetLongitude())); + //wp1.setHeight((double)data.getCommanderFlightHeight()); + wp1.setHeight(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointSecurityHeight())); + //wp.setUseGlobalFlightHeight(true); + wp1.setEllipsoidHeight(wpellheight); + + wp1.setUseStraightLine(true); + wp1.setUseGlobalTurnParam(true); + wp1.setUseGlobalYawParam(true); + wp1.setUseGlobalGimbalHeadingParam(true); + wp1.setUseGlobalAutoFlightSpeed(true); + wp1.setIsRisky(false); + + + + //基础设置 + wp.setWaypointIndex(1); + wp.setLocation(new WaylineLocationCoordinate2D(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointLat()), Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointLon()))); + //wp.setHeight((double)data.getCommanderFlightHeight()); + wp.setHeight(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointSecurityHeight())); + //wp.setUseGlobalFlightHeight(true); + //椭球高 + wp.setEllipsoidHeight(wpellheight); + + + + wp.setUseStraightLine(true); + wp.setUseGlobalTurnParam(true); + wp.setUseGlobalYawParam(true); + wp.setUseGlobalGimbalHeadingParam(true); + wp.setUseGlobalAutoFlightSpeed(true); + wp.setIsRisky(false); + + + List wpList = new ArrayList<>(); + wpList.add(wp1); + wpList.add(wp); + + /* 4. */ + WaylineTemplateWaypointInfo waypointInfo = new WaylineTemplateWaypointInfo(); + + waypointInfo.setGlobalFlightHeight(Double.parseDouble(PreferenceUtils.getInstance().getAlternatePointSecurityHeight())); + waypointInfo.setIsGlobalFlightHeightSet(true); + + WaylineWaypointYawParam waypointYawParam=new WaylineWaypointYawParam(); + waypointYawParam.setYawMode(WaylineWaypointYawMode.FOLLOW_WAYLINE); + waypointInfo.setGlobalYawParam(waypointYawParam); + waypointInfo.setIsTemplateGlobalYawParamSet(true); + + WaylineWaypointGimbalHeadingParam waypointGimbalHeadingParam=new WaylineWaypointGimbalHeadingParam(); + waypointGimbalHeadingParam.setHeadingMode(WaylineWaypointGimbalHeadingMode.FOLLOW_WAYLINE); + waypointGimbalHeadingParam.setYawAngle(0.0); + waypointGimbalHeadingParam.setPitchAngle(0.0); + waypointInfo.setGlobalGimbalHeadingParam(waypointGimbalHeadingParam); + + waypointInfo.setGlobalTurnMode(WaylineWaypointTurnMode.COORDINATE_TURN); + waypointInfo.setIsTemplateGlobalTurnModeSet(true); + waypointInfo.setWaypoints(wpList); + + waypointInfo.setUseStraightLine(true); + + + + /* 5. */ + WaylineCoordinateParam coordParam = new WaylineCoordinateParam(); + coordParam.setCoordinateMode(WaylineCoordinateMode.WGS84); + + coordParam.setAltitudeMode(WaylineAltitudeMode.EGM96); + coordParam.setPositioningType(WaylinePositioningType.GPS); + + /* 6. */ + Template template = new Template(); + template.setTemplateId(0); + template.setCoordinateParam(coordParam); + template.setTransitionalSpeed(10.0); + template.setWaypointInfo(waypointInfo); + template.setUseGlobalTransitionalSpeed(true); + template.setAutoFlightSpeed(10.0); + + //不写就报错 + template.setPayloadParam(new ArrayList<>()); + /* 7. 生成文件 */ + WPMZManager.getInstance().generateKMZFile(kmz.getAbsolutePath(), mission, config, template); + //验证这个kmz + + WaylineCheckErrorMsg result= WPMZManager.getInstance().checkValidation(kmz.getAbsolutePath()); + LogUtil.log("qwq",result.toString()); + if (result.getValue().isEmpty()) { + LogUtil.log("qwq", "KMZ 校验通过"); + sendEvent2Server("备降点航线生成成功", 1); + return true; + } else { + // 其他错误码照旧报错 + LogUtil.log("qwq", "校验失败,错误码:" + result.getValue()); + sendEvent2Server("备降点航线文件生成失败错误码:"+result.getValue(), 1); + return false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/Gpsdistance.java b/app/src/main/java/com/aros/apron/tools/Gpsdistance.java new file mode 100644 index 00000000..c242611a --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/Gpsdistance.java @@ -0,0 +1,45 @@ +package com.aros.apron.tools; + +public class Gpsdistance { + private static final double EARTH_RADIUS = 6371000; + + /** + * 水平距离(Haversine) + */ + public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return EARTH_RADIUS * c; + } + + /** + * 三维直线距离(水平 + 高度差) + * 参数:经纬度(度),高度(米) + */ + public static double calculate3DDistance(double lat1, double lon1, double alt1, + double lat2, double lon2, double alt2) { + // 1. 算水平距离 + double horizontal = calculateDistance(lat1, lon1, lat2, lon2); + + // 2. 算垂直距离(高度差) + double vertical = Math.abs(alt2 - alt1); + + // 3. 勾股定理算斜距 + return Math.sqrt(horizontal * horizontal + vertical * vertical); + } + + /** + * 重载:直接用 DJI 的 LocationCoordinate3D + */ + public static double calculate3DDistance(dji.sdk.keyvalue.value.common.LocationCoordinate3D current, + dji.sdk.keyvalue.value.common.LocationCoordinate3D target) { + return calculate3DDistance( + current.getLatitude(), current.getLongitude(), current.getAltitude(), + target.getLatitude(), target.getLongitude(), target.getAltitude() + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/MixedVisionLanding.java b/app/src/main/java/com/aros/apron/tools/MixedVisionLanding.java new file mode 100644 index 00000000..4f53b273 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/MixedVisionLanding.java @@ -0,0 +1,1045 @@ +package com.aros.apron.tools; + +import android.os.Handler; +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.manager.AlternateLandingManager; + +import org.opencv.core.CvType; +import org.opencv.core.Mat; +import org.opencv.core.MatOfInt; +import org.opencv.core.Point; +import org.opencv.imgproc.Imgproc; +import org.opencv.objdetect.ArucoDetector; +import org.opencv.objdetect.DetectorParameters; +import org.opencv.objdetect.Dictionary; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 融合视觉降落识别器(独立版本) + * 集成云台相机和下视相机的识别功能,实现相机切换逻辑 + * + * 降落策略: + * 1. 先使用云台相机识别 + * 2. 云台相机识别失败,切换到下视相机 + * 3. 下视相机识别失败,切换回云台相机 + * 4. 最多切换2次,超过则触发备降 + */ +public class MixedVisionLanding { + + private static final String TAG_LOG = "MixedVisionLanding"; + + // 单例模式 + private static class SingletonHolder { + private static final MixedVisionLanding INSTANCE = new MixedVisionLanding(); + static { INSTANCE.init(); } + } + + public static MixedVisionLanding getInstance() { + return SingletonHolder.INSTANCE; + } + + // ========== 降落模式 ========== + public enum LandingMode { + GIMBAL_CAMERA, // 云台相机 + DOWNWARD_CAMERA // 下视相机 + } + + // ========== 状态变量 ========== + private LandingMode currentMode = LandingMode.GIMBAL_CAMERA; + private int switchCount = 0; + private static final int MAX_SWITCH_COUNT = 2; + private boolean isLanding = false; + private boolean isDoublePayload = false; + + // ========== 云台相机参数 ========== + private static double GIMBAL_LENS_OFFSET_X = 0; + private static double GIMBAL_LENS_OFFSET_Y = 0; + private static final int GIMBAL_CENTER_ERR_MAX = 50; + private static final int GIMBAL_PIXEL_TRIGGER = 360; + private static final int GIMBAL_TRIGGER_FRAME_THRESHOLD = 2; + + // ========== 下视相机参数 ========== + private static double DOWNWARD_LENS_OFFSET_X = 0; + private static double DOWNWARD_LENS_OFFSET_Y = 0; + private static final int DOWNWARD_CENTER_ERR_MAX = 30; + private static final int DOWNWARD_PIXEL_TRIGGER = 360; + private static final int DOWNWARD_TRIGGER_FRAME_THRESHOLD = 2; + + // ========== 云台相机状态 ========== + private boolean gimbalIsTriggerSuccess = false; + private boolean gimbalArucoNotFoundTag = false; + private boolean gimbalIsStartAruco = false; + private boolean gimbalStartFastStick = false; + private boolean gimbalCanLanding = false; + private int gimbalConsecutiveTriggerCount = 0; + private int gimbalFrameCounter = 0; + private int gimbalDropTimes = 0; + private boolean gimbalDropTimesTag = false; + private long gimbalStartTime = 0; + private long gimbalEndTime = 0; + private boolean gimbalIsYawAligned = false; + private int gimbalCurrentLandingMode = 0; + private int gimbalLastLandingCondition = 0; + private boolean gimbalSetMF = false; + + // ========== 下视相机状态 ========== + private boolean downwardIsTriggerSuccess = false; + private boolean downwardArucoNotFoundTag = false; + private boolean downwardIsStartAruco = false; + private boolean downwardStartFastStick = false; + private boolean downwardCanLanding = false; + private int downwardConsecutiveTriggerCount = 0; + private int downwardFrameCounter = 0; + private int downwardDropTimes = 0; + private boolean downwardDropTimesTag = false; + private long downwardStartTime = 0; + private long downwardEndTime = 0; + + // ========== 执行器 ========== + private ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + private Future lastGimbalFuture = null; + private Future lastDownwardFuture = null; + + // ========== PID控制器 ========== + public PIDControl pidControlX = null; + public PIDControl pidControlY = null; + + // ========== Handler ========== + private Handler handler = new Handler(Looper.getMainLooper()); + private int handlerCallbackCount = 0; + + // ========== 初始化 ========== + public void init() { + LogUtil.log(TAG_LOG, "初始化融合视觉降落识别器"); + pidControlX = new PIDControl(0.7f, 0.02f, 0.2f, 0.05f, 2.5f, 0.1f); + pidControlY = new PIDControl(0.7f, 0.02f, 0.2f, 0.05f, 2.5f, 0.1f); + pidControlX.reset(); + pidControlY.reset(); + } + + // ========== 公共方法 ========== + public void setDoublePayload(boolean doublePayload) { + isDoublePayload = doublePayload; + } + + public void startLanding() { + LogUtil.log(TAG_LOG, "开始融合视觉降落"); + isLanding = true; + currentMode = LandingMode.GIMBAL_CAMERA; + switchCount = 0; + resetGimbalState(); + resetDownwardState(); + } + + public void stopLanding() { + LogUtil.log(TAG_LOG, "停止融合视觉降落"); + isLanding = false; + resetGimbalState(); + resetDownwardState(); + } + + public LandingMode getCurrentMode() { + return currentMode; + } + + public int getSwitchCount() { + return switchCount; + } + + public boolean isLanding() { + return isLanding; + } + + // ========== 云台相机帧处理 ========== + public void processGimbalFrame(int height, int width, byte[] data, Dictionary dictionary) { + if (!isLanding || currentMode != LandingMode.GIMBAL_CAMERA) { + return; + } + + gimbalIsTriggerSuccess = true; + Movement.getInstance().setVirtualStickEnableReason(2); + + if (gimbalIsStartAruco || gimbalStartFastStick) { + return; + } + + int currentFrame = gimbalFrameCounter++; + if (currentFrame % 3 != 0) { + return; + } + + if (currentFrame % 30 != 0) { + DroneHelper.getInstance().setGimbalPitchdown(); + } + + gimbalIsStartAruco = true; + if (lastGimbalFuture != null && !lastGimbalFuture.isDone()) { + lastGimbalFuture.cancel(true); + } + + int ultHeight = Movement.getInstance().getUltrasonicHeight(); + updateGimbalLensOffset(ultHeight); + + final int finalUltHeight = ultHeight; + lastGimbalFuture = executor.schedule(() -> { + try { + processGimbalFrameInternal(height, width, data, dictionary, finalUltHeight); + } catch (Exception e) { + LogUtil.log(TAG_LOG, "云台相机处理异常: " + e); + gimbalIsStartAruco = false; + handleLandingFailure(); + } + }, 0, TimeUnit.MILLISECONDS); + } + + // ========== 下视相机帧处理 ========== + public void processDownwardFrame(int height, int width, byte[] data, Dictionary dictionary) { + if (!isLanding || currentMode != LandingMode.DOWNWARD_CAMERA) { + return; + } + + downwardIsTriggerSuccess = true; + Movement.getInstance().setVirtualStickEnableReason(2); + + if (downwardIsStartAruco || downwardStartFastStick) { + return; + } + + int currentFrame = downwardFrameCounter++; + if (currentFrame % 2 != 0) { + return; + } + + downwardIsStartAruco = true; + if (lastDownwardFuture != null && !lastDownwardFuture.isDone()) { + lastDownwardFuture.cancel(true); + } + + int ultHeight = Movement.getInstance().getUltrasonicHeight(); + updateDownwardLensOffset(ultHeight); + + final int finalUltHeight = ultHeight; + lastDownwardFuture = executor.schedule(() -> { + try { + processDownwardFrameInternal(height, width, data, dictionary, finalUltHeight); + } catch (Exception e) { + LogUtil.log(TAG_LOG, "下视相机处理异常: " + e); + downwardIsStartAruco = false; + handleLandingFailure(); + } + }, 0, TimeUnit.MILLISECONDS); + } + + // ========== 云台相机内部处理 ========== + private void processGimbalFrameInternal(int height, int width, byte[] data, Dictionary dictionary, int ultHeight) { + 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); + + 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()) { + gimbalArucoNotFoundTag = false; + gimbalDropTimesTag = true; + + List smallMarkers = new ArrayList<>(); + List bigMarkers1_4 = new ArrayList<>(); + List bigMarker5 = new ArrayList<>(); + List bigMarker6 = new ArrayList<>(); + + int[] idArray = ids.toArray(); + List excludeIds = isDoublePayload ? + Arrays.asList(12, 11, 21, 14, 13, 22) : + Arrays.asList(12, 11, 21, 20, 19, 25); + + StringBuilder detectInfo = new StringBuilder(); + for (int i = 0; i < idArray.length; i++) { + int id = idArray[i]; + Mat corner = corners.get(i); + double pixelW = calculatePixelWidth(corner); + + if (id >= 11 && id <= 25 && !excludeIds.contains(id)) { + smallMarkers.add(new ArucoMarker(id, corner, 0.03f)); + detectInfo.append(String.format("小码%d(%.0fpx) ", id, pixelW)); + } else if (id >= 1 && id <= 4) { + bigMarkers1_4.add(new ArucoMarker(id, corner, 0.18f)); + detectInfo.append(String.format("大码%d(%.0fpx) ", id, pixelW)); + } else if (id == 5) { + bigMarker5.add(new ArucoMarker(5, corner, 0.15f)); + detectInfo.append(String.format("5号(%.0fpx) ", pixelW)); + } else if (id == 6) { + bigMarker6.add(new ArucoMarker(6, corner, 0.24f)); + detectInfo.append(String.format("6号(%.0fpx) ", pixelW)); + } + } + + if (detectInfo.length() > 0) { + LogUtil.log(TAG_LOG, "【云台检测到】" + detectInfo.toString()); + } + + // 优先级1:小码模式 + if (!smallMarkers.isEmpty() && ultHeight < 25) { + gimbalCurrentLandingMode = 1; + processGimbalSmallMarkers(smallMarkers, rgbMat.width(), rgbMat.height(), ultHeight); + } + // 优先级2:1-4号几何中心 + else if (!bigMarkers1_4.isEmpty()) { + gimbalCurrentLandingMode = 2; + if (ultHeight > 30 && !gimbalIsYawAligned) { + processGimbalBigMarkersYawOnly(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight); + } else { + processGimbalBigMarkersCenter(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight); + } + } + // 优先级3:5/6号纯下降 + else if ((!bigMarker5.isEmpty() || !bigMarker6.isEmpty())) { + gimbalCurrentLandingMode = 3; + ArucoMarker target = !bigMarker5.isEmpty() ? bigMarker5.get(0) : bigMarker6.get(0); + processGimbalLandingOnly(target, rgbMat.width(), rgbMat.height(), ultHeight); + } else { + handleGimbalLostMarker(ultHeight); + } + } else { + handleGimbalLostMarker(ultHeight); + } + + grayImgMat.release(); + rgbMat.release(); + yuvMat.release(); + ids.release(); + if (!corners.isEmpty()) { + for (Mat c : corners) if (c != null) c.release(); + } + gimbalIsStartAruco = false; + } + + // ========== 下视相机内部处理 ========== + private void processDownwardFrameInternal(int height, int width, byte[] data, Dictionary dictionary, int ultHeight) { + 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); + + MatOfInt ids = new MatOfInt(); + List corners = new ArrayList<>(); + DetectorParameters parameters = new DetectorParameters(); + ArucoDetector detector = new ArucoDetector(dictionary, parameters); + + detector.detectMarkers(grayImgMat, corners, ids); + + // 只保留6号码 + ids = keepOnly6(ids, corners); + + if (!ids.empty()) { + downwardArucoNotFoundTag = false; + downwardDropTimesTag = true; + + int[] idArray = ids.toArray(); + if (idArray.length == 1 && idArray[0] == 6) { + Mat corner6 = corners.get(0); + Point[] points = new Point[4]; + for (int i = 0; i < 4; i++) { + double[] p = corner6.get(0, i); + points[i] = new Point(p[0], p[1]); + } + + double pixelWidth = calculateDistance(points[0], points[1]); + double centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4.0 + DOWNWARD_LENS_OFFSET_X; + double errX = Math.abs(centerX - rgbMat.width() / 2.0); + double centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4.0 + DOWNWARD_LENS_OFFSET_Y; + double errY = Math.abs(centerY - rgbMat.height() / 2.0); + + LogUtil.log(TAG_LOG, "【下视检测到】6号 像素" + (int) pixelWidth + " errX=" + (int) errX + " errY=" + (int) errY); + + // 速降判断 + if (!downwardStartFastStick) { + if (pixelWidth >= DOWNWARD_PIXEL_TRIGGER && + ultHeight <= 4 && + errX < DOWNWARD_CENTER_ERR_MAX && + errY < DOWNWARD_CENTER_ERR_MAX) { + + downwardConsecutiveTriggerCount++; + LogUtil.log(TAG_LOG, "下视速降条件满足,累计帧数: " + downwardConsecutiveTriggerCount); + + if (downwardConsecutiveTriggerCount >= DOWNWARD_TRIGGER_FRAME_THRESHOLD) { + downwardStartFastStick = true; + downwardConsecutiveTriggerCount = 0; + LogUtil.log(TAG_LOG, "【下视触发速降】"); + handler.postDelayed(() -> handler.post(downwardRunnable), 300); + return; + } + } else { + if (downwardConsecutiveTriggerCount > 0) { + LogUtil.log(TAG_LOG, "下视速降条件中断,重置累计帧数"); + downwardConsecutiveTriggerCount = 0; + } + } + } + + // 正常修正 + moveOnArucoDetected(new ArucoMarker(1, corner6, 0.24f), rgbMat.width(), rgbMat.height()); + } + } else { + handleDownwardLostMarker(ultHeight); + } + + grayImgMat.release(); + rgbMat.release(); + yuvMat.release(); + ids.release(); + if (!corners.isEmpty()) { + for (Mat c : corners) if (c != null) c.release(); + } + downwardIsStartAruco = false; + } + + // ========== 云台相机小码处理 ========== + private void processGimbalSmallMarkers(List markers, int imgWidth, int imgHeight, int ultHeight) { + if (markers == null || markers.size() < 3) { + LogUtil.log(TAG_LOG, "【云台小码数量不足】检测到" + (markers == null ? 0 : markers.size()) + "个"); + return; + } + + double sumX = 0, sumY = 0; + int validCount = 0; + double totalPixelWidth = 0; + ArucoMarker target15 = null; + ArucoMarker target17 = null; + + for (ArucoMarker marker : markers) { + Mat corner = marker.getConner(); + totalPixelWidth += calculatePixelWidth(corner); + + 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; + sumX += cx; + sumY += cy; + validCount++; + + int id = marker.getId(); + if (id == 15) target15 = marker; + if (id == 17) target17 = marker; + } + + if (validCount == 0) return; + + double avgPixelWidth = totalPixelWidth / validCount; + double geoCenterX = sumX / validCount; + double geoCenterY = sumY / validCount; + double offsetX = geoCenterX - imgWidth / 2.0 - GIMBAL_LENS_OFFSET_X; + double offsetY = geoCenterY - imgHeight / 2.0 + GIMBAL_LENS_OFFSET_Y; + double absX = Math.abs(offsetX); + double absY = Math.abs(offsetY); + double z = ultHeight / 10.0; + + // 特殊降落判断 + boolean isSpecialLanding = false; + 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]; + } + cx /= 4.0; cy /= 4.0; + double singleErrX = cx - imgWidth / 2.0 - GIMBAL_LENS_OFFSET_X; + double singleErrY = cy - imgHeight / 2.0 + GIMBAL_LENS_OFFSET_Y; + + if (Math.abs(singleErrX) < 60 && Math.abs(singleErrY) < 60 && ultHeight <= 4) { + isSpecialLanding = true; + offsetX = singleErrX; offsetY = singleErrY; + absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); + } + } + + 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]; + } + cx /= 4.0; cy /= 4.0; + double singleErrX = cx - imgWidth / 2.0 - GIMBAL_LENS_OFFSET_X; + double singleErrY = cy - imgHeight / 2.0 + GIMBAL_LENS_OFFSET_Y; + + if (Math.abs(singleErrX) < 60 && Math.abs(singleErrY) < 60 && ultHeight <= 4) { + isSpecialLanding = true; + offsetX = singleErrX; offsetY = singleErrY; + absX = Math.abs(singleErrX); absY = Math.abs(singleErrY); + } + } + + // 连续帧判断 + int currentCondition = 0; + if (isSpecialLanding) { + currentCondition = 1; + } else if (ultHeight <= 4 && absX < 60 && absY < 60 && validCount >= 8) { + currentCondition = 2; + } + + if (currentCondition == 0 || currentCondition != gimbalLastLandingCondition) { + if (gimbalConsecutiveTriggerCount > 0) { + gimbalConsecutiveTriggerCount = 0; + LogUtil.log(TAG_LOG, "【云台计数器清零】条件变化"); + } + gimbalLastLandingCondition = currentCondition; + } + + if (currentCondition > 0) { + gimbalConsecutiveTriggerCount++; + LogUtil.log(TAG_LOG, "【云台计数器累加】count=" + gimbalConsecutiveTriggerCount); + + if (gimbalConsecutiveTriggerCount >= GIMBAL_TRIGGER_FRAME_THRESHOLD) { + gimbalStartFastStick = true; + gimbalConsecutiveTriggerCount = 0; + gimbalLastLandingCondition = 0; + LogUtil.log(TAG_LOG, "【云台触发速降】"); + handler.postDelayed(() -> handler.post(gimbalRunnable), 300); + return; + } + } + + // PID控制 + double outX = 0, outY = 0, outZ = 0; + if (z <= 0.4) { + pidControlX.setInputFilterAll((float)offsetX/1750); + pidControlY.setInputFilterAll(-(float)offsetY/1750); + outX = Math.max(-0.125, Math.min(0.125, pidControlX.get_pid())); + outY = Math.max(-0.125, Math.min(0.125, pidControlY.get_pid())); + } else if (z <= 0.7) { + pidControlX.setInputFilterAll((float)offsetX/1200); + pidControlY.setInputFilterAll(-(float)offsetY/1200); + outX = Math.max(-0.135, Math.min(0.135, pidControlX.get_pid())); + outY = Math.max(-0.135, Math.min(0.135, pidControlY.get_pid())); + outZ = (absX < 250 && absY < 250) ? -0.1 : 0; + } else if (z <= 1.0) { + pidControlX.setInputFilterAll((float)offsetX/1200); + pidControlY.setInputFilterAll(-(float)offsetY/1200); + outX = Math.max(-0.145, Math.min(0.145, pidControlX.get_pid())); + outY = Math.max(-0.175, Math.min(0.175, pidControlY.get_pid())); + outZ = (absX < 200 && absY < 200) ? -0.2 : 0; + } else if (z <= 1.5) { + pidControlX.setInputFilterAll((float)offsetX/1450); + pidControlY.setInputFilterAll(-(float)offsetY/1450); + outX = Math.max(-0.185, Math.min(0.185, pidControlX.get_pid())); + outY = Math.max(-0.185, Math.min(0.185, pidControlY.get_pid())); + outZ = (absX < 180 && absY < 180) ? -0.4 : 0; + } else if (z <= 2.0) { + pidControlX.setInputFilterAll((float)offsetX/1350); + pidControlY.setInputFilterAll(-(float)offsetY/1350); + outX = pidControlX.get_pid(); + outY = pidControlY.get_pid(); + outZ = (absX < 200 && absY < 200) ? -0.4 : 0; + } else if (z <= 3.0) { + pidControlX.setInputFilterAll((float)offsetX/1250); + pidControlY.setInputFilterAll(-(float)offsetY/1250); + outX = pidControlX.get_pid(); + outY = pidControlY.get_pid(); + outZ = (absX < 200 && absY < 200) ? -0.4 : 0; + } else { + pidControlX.setInputFilterAll((float)offsetX/850); + pidControlY.setInputFilterAll(-(float)offsetY/850); + outX = pidControlX.get_pid(); + outY = pidControlY.get_pid(); + outZ = (absX < 130 && absY < 130) ? -0.4 : 0; + } + + LogUtil.log(TAG_LOG, String.format("【云台执行】小码 vx=%.3f vy=%.3f vz=%.3f pixel=%.0f ult=%d", outX, outY, outZ, avgPixelWidth, ultHeight)); + DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0, (float)outZ); + } + + // ========== 云台相机大码旋转处理 ========== + private void processGimbalBigMarkersYawOnly(List markers, int imgWidth, int imgHeight, int ultHeight) { + double sumYaw = 0; + int count = 0; + + for (ArucoMarker marker : markers) { + Point[] pts = new Point[4]; + Mat corner = marker.getConner(); + for (int i = 0; i < 4; i++) { + double[] p = corner.get(0, i); + pts[i] = new Point(p[0], p[1]); + } + sumYaw += calculateYawErrorFromCorners(pts); + count++; + } + + double avgYaw = sumYaw / count; + double absYaw = Math.abs(avgYaw); + + if (absYaw < 10.0) { + gimbalIsYawAligned = true; + LogUtil.log(TAG_LOG, "【云台旋转到位】偏航已对准"); + return; + } + + float yawRate = calculateYawRate(avgYaw, ultHeight); + LogUtil.log(TAG_LOG, String.format("【云台执行】纯旋转 yawRate=%.1f avgYaw=%.1f", yawRate, avgYaw)); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, yawRate, 0f); + } + + // ========== 云台相机大码中心处理 ========== + private void processGimbalBigMarkersCenter(List markers, int imgWidth, int imgHeight, int ultHeight) { + double sumX = 0, sumY = 0; + StringBuilder markerInfo = new StringBuilder(); + + for (ArucoMarker marker : markers) { + Mat corner = marker.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]; + } + sumX += cx / 4.0; + sumY += cy / 4.0; + + double pixelW = calculatePixelWidth(corner); + markerInfo.append(String.format("[ID%d:%.0fpx]", marker.getId(), pixelW)); + } + + double geoCenterX = sumX / markers.size(); + double geoCenterY = sumY / markers.size(); + double offsetX = geoCenterX - imgWidth / 2.0; + double offsetY = geoCenterY - imgHeight / 2.0; + double absX = Math.abs(offsetX); + double absY = Math.abs(offsetY); + double z = ultHeight / 10.0; + + double outX = 0, outY = 0, outZ = 0; + + if (z >= 5.0) { + pidControlX.setInputFilterAll((float)offsetX/300); + pidControlY.setInputFilterAll(-(float)offsetY/300); + outX = Math.max(-0.20, Math.min(0.20, pidControlX.get_pid())); + outY = Math.max(-0.20, Math.min(0.20, pidControlY.get_pid())); + outZ = (absX < 100 && absY < 100) ? -0.4 : 0; + } else if (z >= 4) { + pidControlX.setInputFilterAll((float)offsetX/600); + pidControlY.setInputFilterAll(-(float)offsetY/600); + outX = Math.max(-0.20, Math.min(0.20, pidControlX.get_pid())); + outY = Math.max(-0.20, Math.min(0.20, pidControlY.get_pid())); + outZ = (absX < 120 && absY < 120) ? -0.4 : 0; + } else if (z >= 2) { + pidControlX.setInputFilterAll((float)offsetX/900); + pidControlY.setInputFilterAll(-(float)offsetY/900); + outX = Math.max(-0.20, Math.min(0.20, pidControlX.get_pid())); + outY = Math.max(-0.20, Math.min(0.20, pidControlY.get_pid())); + outZ = (absX < 140 && absY < 140) ? -0.4 : 0; + } + + LogUtil.log(TAG_LOG, String.format("【云台执行】大码几何中心 vx=%.3f vy=%.3f vz=%.3f ult=%d %s", outX, outY, outZ, ultHeight, markerInfo.toString())); + DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0f, (float)outZ); + } + + // ========== 云台相机纯下降处理 ========== + private void processGimbalLandingOnly(ArucoMarker marker, int imgWidth, int imgHeight, int ultHeight) { + Mat corner = marker.getConner(); + double pixelWidth = calculatePixelWidth(corner); + + float vz; + if (ultHeight >= 50) { + vz = -0.7f; + } else if (ultHeight <= 15) { + vz = 0.0f; + } else { + vz = 0.3f; + } + + LogUtil.log(TAG_LOG, String.format("【云台执行】5/6号纯下降 vz=%.3f pixel=%.0f ult=%d", vz, pixelWidth, ultHeight)); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, vz); + } + + // ========== 云台相机丢失处理 ========== + private void handleGimbalLostMarker(int ultHeight) { + if (!gimbalArucoNotFoundTag) { + gimbalStartTime = System.currentTimeMillis(); + gimbalArucoNotFoundTag = true; + } + gimbalEndTime = System.currentTimeMillis(); + long lostDuration = gimbalEndTime - gimbalStartTime; + + if (lostDuration > 1000 && lostDuration <= 12000) { + if (ultHeight <= 20) { + LogUtil.log(TAG_LOG, "【云台执行】丢失上升 vz=3.0"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f); + if (gimbalDropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) { + LogUtil.log(TAG_LOG, "云台超过复降限制,切换相机"); + handleLandingFailure(); + return; + } + if (gimbalDropTimesTag) { + gimbalDropTimesTag = false; + gimbalDropTimes++; + LogUtil.log(TAG_LOG, "云台复降第:" + gimbalDropTimes + "次"); + } + } else { + LogUtil.log(TAG_LOG, "【云台执行】丢失下降 vz=-0.3"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); + } + } else if (lostDuration > 8000) { + LogUtil.log(TAG_LOG, "云台判定未识别到二维码,切换相机"); + handleLandingFailure(); + } + } + + // ========== 下视相机丢失处理 ========== + private void handleDownwardLostMarker(int ultHeight) { + LogUtil.log(TAG_LOG, "下视找不到了二维码"); + if (!downwardArucoNotFoundTag) { + downwardStartTime = System.currentTimeMillis(); + downwardArucoNotFoundTag = true; + } + downwardEndTime = System.currentTimeMillis(); + long lostDuration = downwardEndTime - downwardStartTime; + + if (lostDuration > 1000 && lostDuration <= 12000) { + if (ultHeight <= 20) { + LogUtil.log(TAG_LOG, "【下视执行】丢失上升 vz=3.0"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, 3f); + if (downwardDropTimes > Integer.parseInt(AMSConfig.getInstance().getAlternateLandingTimes())) { + LogUtil.log(TAG_LOG, "下视超过复降限制,切换相机"); + handleLandingFailure(); + return; + } + if (downwardDropTimesTag) { + downwardDropTimesTag = false; + downwardDropTimes++; + LogUtil.log(TAG_LOG, "下视复降第:" + downwardDropTimes + "次"); + } + } else { + LogUtil.log(TAG_LOG, "【下视执行】丢失下降 vz=-0.3"); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -0.3f); + } + } else if (lostDuration > 8000) { + LogUtil.log(TAG_LOG, "下视判定未识别到二维码,切换相机"); + handleLandingFailure(); + } + } + + // ========== 下视相机移动控制 ========== + private void moveOnArucoDetected(ArucoMarker marker, int imgWidth, int imgHeight) { + Mat corner = marker.getConner(); + Point[] points = new Point[4]; + for (int i = 0; i < 4; i++) { + double[] p = corner.get(0, i); + points[i] = new Point(p[0], p[1]); + } + + double centerX = (points[0].x + points[1].x + points[2].x + points[3].x) / 4.0 + DOWNWARD_LENS_OFFSET_X; + double centerY = (points[0].y + points[1].y + points[2].y + points[3].y) / 4.0 + DOWNWARD_LENS_OFFSET_Y; + double errX = centerX - imgWidth / 2.0; + double errY = centerY - imgHeight / 2.0; + double absX = Math.abs(errX); + double absY = Math.abs(errY); + + int ultHeight = Movement.getInstance().getUltrasonicHeight(); + double z = ultHeight / 10.0; + + double outX = 0, outY = 0, outZ = 0; + + if (z <= 0.4) { + pidControlX.setInputFilterAll((float)errX/1750); + pidControlY.setInputFilterAll(-(float)errY/1750); + outX = Math.max(-0.125, Math.min(0.125, pidControlX.get_pid())); + outY = Math.max(-0.125, Math.min(0.125, pidControlY.get_pid())); + } else if (z <= 0.7) { + pidControlX.setInputFilterAll((float)errX/1200); + pidControlY.setInputFilterAll(-(float)errY/1200); + outX = Math.max(-0.135, Math.min(0.135, pidControlX.get_pid())); + outY = Math.max(-0.135, Math.min(0.135, pidControlY.get_pid())); + outZ = (absX < 250 && absY < 250) ? -0.1 : 0; + } else if (z <= 1.0) { + pidControlX.setInputFilterAll((float)errX/1200); + pidControlY.setInputFilterAll(-(float)errY/1200); + outX = Math.max(-0.145, Math.min(0.145, pidControlX.get_pid())); + outY = Math.max(-0.175, Math.min(0.175, pidControlY.get_pid())); + outZ = (absX < 200 && absY < 200) ? -0.2 : 0; + } else if (z <= 1.5) { + pidControlX.setInputFilterAll((float)errX/1450); + pidControlY.setInputFilterAll(-(float)errY/1450); + outX = Math.max(-0.185, Math.min(0.185, pidControlX.get_pid())); + outY = Math.max(-0.185, Math.min(0.185, pidControlY.get_pid())); + outZ = (absX < 180 && absY < 180) ? -0.4 : 0; + } else if (z <= 2.0) { + pidControlX.setInputFilterAll((float)errX/1350); + pidControlY.setInputFilterAll(-(float)errY/1350); + outX = pidControlX.get_pid(); + outY = pidControlY.get_pid(); + outZ = (absX < 200 && absY < 200) ? -0.4 : 0; + } else if (z <= 3.0) { + pidControlX.setInputFilterAll((float)errX/1250); + pidControlY.setInputFilterAll(-(float)errY/1250); + outX = pidControlX.get_pid(); + outY = pidControlY.get_pid(); + outZ = (absX < 200 && absY < 200) ? -0.4 : 0; + } else { + pidControlX.setInputFilterAll((float)errX/850); + pidControlY.setInputFilterAll(-(float)errY/850); + outX = pidControlX.get_pid(); + outY = pidControlY.get_pid(); + outZ = (absX < 130 && absY < 130) ? -0.4 : 0; + } + + LogUtil.log(TAG_LOG, String.format("【下视执行】移动 vx=%.3f vy=%.3f vz=%.3f ult=%d errX=%.0f errY=%.0f", outX, outY, outZ, ultHeight, absX, absY)); + DroneHelper.getInstance().moveVxVyYawrateHeight((float)outX, (float)outY, 0f, (float)outZ); + } + + // ========== 降落失败处理(切换相机) ========== + private void handleLandingFailure() { + if (!isLanding) { + return; + } + + switchCount++; + LogUtil.log(TAG_LOG, "切换相机,当前切换次数: " + switchCount); + + // 切换到另一个相机 + if (currentMode == LandingMode.GIMBAL_CAMERA) { + currentMode = LandingMode.DOWNWARD_CAMERA; + LogUtil.log(TAG_LOG, "切换到下视相机"); + resetDownwardState(); + } else { + currentMode = LandingMode.GIMBAL_CAMERA; + LogUtil.log(TAG_LOG, "切换到云台相机"); + resetGimbalState(); + } + + // 如果达到最大切换次数,触发备降 + if (switchCount >= MAX_SWITCH_COUNT) { + LogUtil.log(TAG_LOG, "达到最大切换次数,触发备降"); + stopLanding(); + AlternateLandingManager.getInstance().startTaskProcess(null); + Movement.getInstance().setAlternate(true); + } + } + + // ========== 重置云台相机状态 ========== + private void resetGimbalState() { + gimbalIsTriggerSuccess = false; + gimbalArucoNotFoundTag = false; + gimbalIsStartAruco = false; + gimbalStartFastStick = false; + gimbalCanLanding = false; + gimbalConsecutiveTriggerCount = 0; + gimbalFrameCounter = 0; + gimbalDropTimes = 0; + gimbalDropTimesTag = false; + gimbalStartTime = 0; + gimbalEndTime = 0; + gimbalIsYawAligned = false; + gimbalCurrentLandingMode = 0; + gimbalLastLandingCondition = 0; + gimbalSetMF = false; + } + + // ========== 重置下视相机状态 ========== + private void resetDownwardState() { + downwardIsTriggerSuccess = false; + downwardArucoNotFoundTag = false; + downwardIsStartAruco = false; + downwardStartFastStick = false; + downwardCanLanding = false; + downwardConsecutiveTriggerCount = 0; + downwardFrameCounter = 0; + downwardDropTimes = 0; + downwardDropTimesTag = false; + downwardStartTime = 0; + downwardEndTime = 0; + } + + // ========== 更新云台相机镜头偏移 ========== + private void updateGimbalLensOffset(int ultHeight) { + if (isDoublePayload) { + if (ultHeight <= 5) { + GIMBAL_LENS_OFFSET_X = -120; + GIMBAL_LENS_OFFSET_Y = 120; + } else if (ultHeight < 10) { + GIMBAL_LENS_OFFSET_X = -80; + GIMBAL_LENS_OFFSET_Y = 80; + } else if (ultHeight < 20) { + GIMBAL_LENS_OFFSET_X = -60; + GIMBAL_LENS_OFFSET_Y = 60; + } + } else { + if (ultHeight <= 5) { + GIMBAL_LENS_OFFSET_X = 120; + GIMBAL_LENS_OFFSET_Y = 100; + } else if (ultHeight < 10) { + GIMBAL_LENS_OFFSET_X = 80; + GIMBAL_LENS_OFFSET_Y = 80; + } else if (ultHeight < 20) { + GIMBAL_LENS_OFFSET_X = 60; + GIMBAL_LENS_OFFSET_Y = 60; + } + } + } + + // ========== 更新下视相机镜头偏移 ========== + private void updateDownwardLensOffset(int ultHeight) { + if (ultHeight >= 30) { + DOWNWARD_LENS_OFFSET_X = 0; + DOWNWARD_LENS_OFFSET_Y = 0; + } else if (ultHeight >= 20) { + DOWNWARD_LENS_OFFSET_X = 0; + DOWNWARD_LENS_OFFSET_Y = 0; + } else if (ultHeight >= 10) { + DOWNWARD_LENS_OFFSET_X = 20; + DOWNWARD_LENS_OFFSET_Y = 10; + } else if (ultHeight >= 5) { + DOWNWARD_LENS_OFFSET_X = 30; + DOWNWARD_LENS_OFFSET_Y = 20; + } else { + DOWNWARD_LENS_OFFSET_X = 55; + DOWNWARD_LENS_OFFSET_Y = 35; + } + } + + // ========== 工具方法 ========== + private double calculatePixelWidth(Mat corner) { + try { + Point[] pts = new Point[4]; + for (int i = 0; i < 4; i++) { + double[] p = corner.get(0, i); + pts[i] = new Point(p[0], p[1]); + } + double top = Math.sqrt(Math.pow(pts[1].x - pts[0].x, 2) + Math.pow(pts[1].y - pts[0].y, 2)); + double bottom = Math.sqrt(Math.pow(pts[2].x - pts[3].x, 2) + Math.pow(pts[2].y - pts[3].y, 2)); + return (top + bottom) / 2.0; + } catch (Exception e) { + return 0.0; + } + } + + private double calculateDistance(Point p1, Point p2) { + double dx = p2.x - p1.x; + double dy = p2.y - p1.y; + return Math.sqrt(dx * dx + dy * dy); + } + + private double calculateYawErrorFromCorners(Point[] pts) { + double dxTop = pts[1].x - pts[0].x; + double dyTop = pts[1].y - pts[0].y; + double angleTop = Math.toDegrees(Math.atan2(dyTop, dxTop)); + double dxBottom = pts[2].x - pts[3].x; + double dyBottom = pts[2].y - pts[3].y; + double angleBottom = Math.toDegrees(Math.atan2(dyBottom, dxBottom)); + double yawError = (angleTop + angleBottom) / 2.0; + while (yawError > 180) yawError -= 360; + while (yawError < -180) yawError += 360; + return yawError; + } + + private float calculateYawRate(double yawError, int ultHeight) { + if (ultHeight < 10 || ultHeight > 50) return 0f; + + double absError = Math.abs(yawError); + if (absError < 10.0) return 0f; + + float targetRate; + if (absError > 100.0) targetRate = 50.0f; + else if (absError > 50.0) targetRate = 30.0f; + else if (absError > 30.0) targetRate = 20.0f; + else targetRate = 10.0f; + + return yawError > 0 ? targetRate : -targetRate; + } + + private MatOfInt keepOnly6(MatOfInt ids, List corners) { + if (ids.empty()) return ids; + + int[] idArray = ids.toArray(); + List indicesToKeep = new ArrayList<>(); + + for (int i = 0; i < idArray.length; i++) { + if (idArray[i] == 6) { + indicesToKeep.add(i); + } + } + + if (indicesToKeep.isEmpty()) { + ids.release(); + return new MatOfInt(); + } + + int[] newIds = new int[indicesToKeep.size()]; + List newCorners = new ArrayList<>(); + + for (int i = 0; i < indicesToKeep.size(); i++) { + newIds[i] = 6; + newCorners.add(corners.get(indicesToKeep.get(i))); + } + + corners.clear(); + corners.addAll(newCorners); + + MatOfInt newIdsMat = new MatOfInt(); + newIdsMat.fromArray(newIds); + ids.release(); + return newIdsMat; + } + + // ========== 云台速降Runnable ========== + private Runnable gimbalRunnable = new Runnable() { + @Override + public void run() { + LogUtil.log(TAG_LOG, String.format("【云台执行】速降 vz=-5 count=%d/20", handlerCallbackCount)); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -5); + handlerCallbackCount++; + if (handlerCallbackCount < 20) { + handler.postDelayed(this, 50); + } else { + gimbalCanLanding = true; + handlerCallbackCount = 0; + gimbalDropTimes = 0; + } + } + }; + + // ========== 下视速降Runnable ========== + private Runnable downwardRunnable = new Runnable() { + @Override + public void run() { + LogUtil.log(TAG_LOG, String.format("【下视执行】速降 vz=-5 count=%d/20", handlerCallbackCount)); + DroneHelper.getInstance().moveVxVyYawrateHeight(0f, 0f, 0f, -5); + handlerCallbackCount++; + if (handlerCallbackCount < 20) { + handler.postDelayed(this, 50); + } else { + downwardCanLanding = true; + handlerCallbackCount = 0; + downwardDropTimes = 0; + } + } + }; +} diff --git a/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java b/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java new file mode 100644 index 00000000..162f7b42 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/SimplePortScanner.java @@ -0,0 +1,81 @@ +package com.aros.apron.tools; + +import android.os.Handler; +import android.os.Looper; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Timer; +import java.util.TimerTask; + +/** + * 极简版:每3秒检测一次本地8554端口是否开启 + * 参数别改:回调已强制切到主线程,可直接操作UI + */ +public class SimplePortScanner { + private static final int CHECK_PORT = 8554; + private static final String LOCAL_HOST = "127.0.0.1"; + private static final long SCAN_INTERVAL = 3000L; + private static final int CONNECT_TIMEOUT = 500; + + private Timer scanTimer; + private OnPortCheckListener checkListener; + // 主线程 Handler,用于切换回调到UI线程 + private final Handler mainHandler = new Handler(Looper.getMainLooper()); + + public interface OnPortCheckListener { + void onPortOpen(); + void onPortClosed(); + } + + private static class Holder { + private static final SimplePortScanner INSTANCE = new SimplePortScanner(); + } + + public static SimplePortScanner getInstance() { + return Holder.INSTANCE; + } + + private SimplePortScanner() {} + + public void setOnPortCheckListener(OnPortCheckListener listener) { + this.checkListener = listener; + } + + public void startScan() { + stopScan(); + scanTimer = new Timer(); + scanTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + final boolean isOpen = checkPort(LOCAL_HOST, CHECK_PORT); + // 关键修改:切到主线程回调 + mainHandler.post(() -> { + if (checkListener != null) { + if (isOpen) { + checkListener.onPortOpen(); + } else { + checkListener.onPortClosed(); + } + } + }); + } + }, 0, SCAN_INTERVAL); + } + + public void stopScan() { + if (scanTimer != null) { + scanTimer.cancel(); + scanTimer = null; + } + } + + private boolean checkPort(String host, int port) { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(host, port), CONNECT_TIMEOUT); + return true; + } catch (IOException e) { + return false; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java b/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java new file mode 100644 index 00000000..adbb4bda --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/SpeakerProgressReporter.java @@ -0,0 +1,203 @@ +package com.aros.apron.tools; + +import android.os.Handler; +import android.os.Looper; + +import com.aros.apron.constant.AMSConfig; +import com.aros.apron.entity.Movement; +import com.aros.apron.entity.SpeakerAudioPlayProgress; +import com.aros.apron.entity.SpeakerTTSPlayProgress; +import com.google.gson.Gson; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.UUID; + +/** + * 喊话器进度定时上报(使用实体类 SpeakerAudioPlayProgress / SpeakerTTSPlayProgress) + */ +public class SpeakerProgressReporter { + + private static final String TAG = "SpeakerProgressReporter"; + private static volatile SpeakerProgressReporter instance; + + private Handler handler; + private Runnable audioRunnable; + private Runnable ttsRunnable; + private static final long INTERVAL = 1000; // 1秒 + + private SpeakerProgressReporter() { + handler = new Handler(Looper.getMainLooper()); + } + + public static SpeakerProgressReporter getInstance() { + if (instance == null) { + synchronized (SpeakerProgressReporter.class) { + if (instance == null) { + instance = new SpeakerProgressReporter(); + } + } + } + return instance; + } + + // ==================== 音频文件播放上报 ==================== + + public void startAudioReport(int psdkIndex) { + stopAudioReport(); + LogUtil.log(TAG, "启动音频进度上报,MD5:" + Movement.getInstance().getMd5()); + + audioRunnable = new Runnable() { + @Override + public void run() { + try { + sendAudioProgress(psdkIndex); + } catch (Exception e) { + e.printStackTrace(); + } + + // 关键修复:用 audio_status 判断,不是 TTS_status + String status = Movement.getInstance().getTTS_status(); + if (!"ok".equals(status)) { + handler.postDelayed(this, INTERVAL); + } else { + handler.postDelayed(() -> stopAudioReport(), 1000); + } + } + }; + + handler.post(audioRunnable); + } + + public void stopAudioReport() { + if (audioRunnable != null) { + handler.removeCallbacks(audioRunnable); + } + LogUtil.log(TAG, "停止音频进度上报"); + } + + /** + * 发送音频进度(使用 SpeakerAudioPlayProgress 实体类) + */ + private void sendAudioProgress(int psdkIndex) { + try { + if (!MqttManager.getInstance().mqttAndroidClient.isConnected()) return; + + Movement m = Movement.getInstance(); + + // 构建 progress + SpeakerAudioPlayProgress.Progress progress = new SpeakerAudioPlayProgress.Progress(); + progress.setPercent(m.getSpeakerpercent()); + progress.setStepKey(m.getStep_key()); + + // 构建 output + SpeakerAudioPlayProgress.Output output = new SpeakerAudioPlayProgress.Output(); + output.setPsdkIndex(psdkIndex); + output.setStatus(m.getTTS_status()); + output.setMd5("bacee8ed225fa346f6da87f67c914728"); + output.setProgress(progress); + + // 构建 data + SpeakerAudioPlayProgress.Data data = new SpeakerAudioPlayProgress.Data(); + data.setResult(0); + data.setOutput(output); + + // 构建根对象 + SpeakerAudioPlayProgress event = new SpeakerAudioPlayProgress(); + event.setBid(UUID.randomUUID().toString()); + event.setTid(UUID.randomUUID().toString()); + event.setTimestamp(System.currentTimeMillis()); + event.setMethod("speaker_audio_play_start_progress"); + event.setNeedReply(1); + event.setData(data); + + String json = new Gson().toJson(event); + MqttMessage msg = new MqttMessage(json.getBytes("UTF-8")); + msg.setQos(0); + MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_EVENT, msg); + + + } catch (Exception e) { + e.printStackTrace(); + } + } + + // ==================== TTS播放上报 ==================== + + public void startTTSReport(int psdkIndex) { + stopTTSReport(); + LogUtil.log(TAG, "启动TTS进度上报,MD5:" + Movement.getInstance().getMd5()); + + ttsRunnable = new Runnable() { + @Override + public void run() { + try { + sendTTSProgress(psdkIndex); + } catch (Exception e) { + e.printStackTrace(); + } + + String status = Movement.getInstance().getTTS_status(); + if (!"ok".equals(status)) { + handler.postDelayed(this, INTERVAL); + } else { + handler.postDelayed(() -> stopTTSReport(), 1000); + } + } + }; + + handler.post(ttsRunnable); + } + + public void stopTTSReport() { + if (ttsRunnable != null) { + handler.removeCallbacks(ttsRunnable); + } + LogUtil.log(TAG, "停止TTS进度上报"); + } + + /** + * 发送TTS进度(使用 SpeakerTTSPlayProgress 实体类) + */ + private void sendTTSProgress(int psdkIndex) { + try { + if (!MqttManager.getInstance().mqttAndroidClient.isConnected()) return; + + Movement m = Movement.getInstance(); + + // 构建 progress + SpeakerTTSPlayProgress.Progress progress = new SpeakerTTSPlayProgress.Progress(); + progress.setPercent(m.getSpeakerpercent()); // 复用 percent 字段 + progress.setStepKey(m.getStep_key()); // TTS 用 step_key + + // 构建 output + SpeakerTTSPlayProgress.Output output = new SpeakerTTSPlayProgress.Output(); + output.setPsdkIndex(psdkIndex); + output.setStatus(m.getTTS_status()); + output.setMd5("bacee8ed225fa346f6da87f67c914728"); + output.setProgress(progress); + + // 构建 data + SpeakerTTSPlayProgress.Data data = new SpeakerTTSPlayProgress.Data(); + data.setResult(0); + data.setOutput(output); + + // 构建根对象 + SpeakerTTSPlayProgress event = new SpeakerTTSPlayProgress(); + event.setBid(UUID.randomUUID().toString()); + event.setTid(UUID.randomUUID().toString()); + event.setTimestamp(System.currentTimeMillis()); + event.setMethod("speaker_tts_play_start_progress"); + event.setNeedReply(1); + event.setData(data); + + String json = new Gson().toJson(event); + MqttMessage msg = new MqttMessage(json.getBytes("UTF-8")); + msg.setQos(0); + MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_EVENT, msg); + + + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/TakeoffProgressScheduler.java b/app/src/main/java/com/aros/apron/tools/TakeoffProgressScheduler.java new file mode 100644 index 00000000..283c41e6 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/TakeoffProgressScheduler.java @@ -0,0 +1,229 @@ +package com.aros.apron.tools; + +import android.os.Handler; +import android.os.Looper; + +import com.aros.apron.constant.AMSConfig; +import com.aros.apron.entity.Movement; +import com.aros.apron.entity.TakeoffToPointProgress; +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import dji.sdk.keyvalue.key.FlightControllerKey; +import dji.sdk.keyvalue.key.KeyTools; +import dji.sdk.keyvalue.value.common.LocationCoordinate2D; +import dji.sdk.keyvalue.value.common.LocationCoordinate3D; +import dji.v5.manager.KeyManager; + +/** + * 一键起飞进度定时上报器(Handler版,无红线) + */ +public class TakeoffProgressScheduler { + + private static final String TAG = "TakeoffProgressScheduler"; + private static volatile TakeoffProgressScheduler instance; + + // Handler 定时器(Android 主线程) + private Handler reportHandler; + private Runnable reportRunnable; + + // 控制开关 + private volatile boolean isRunning = false; + + // 轨迹点缓存 + private List pathPoints; + + // 采样间隔(ms) + private static final long REPORT_INTERVAL = 2000; + + // 最大轨迹点数 + private static final int MAX_POINTS = 3600; + + private TakeoffProgressScheduler() { + reportHandler = new Handler(Looper.getMainLooper()); + pathPoints = new ArrayList<>(); + } + + public static TakeoffProgressScheduler getInstance() { + if (instance == null) { + synchronized (TakeoffProgressScheduler.class) { + if (instance == null) { + instance = new TakeoffProgressScheduler(); + } + } + } + return instance; + } + + /** + * 开始定时上报(开关打开) + */ + public void startReporting() { + if (isRunning) { + LogUtil.log(TAG, "定时上报已在运行中"); + return; + } + + isRunning = true; + pathPoints.clear(); + LogUtil.log(TAG, "启动一键起飞进度定时上报"); + + // 定义定时任务(参数别改) + reportRunnable = new Runnable() { + @Override + public void run() { + if (!isRunning) return; + + try { + // 1. 采样飞机位置(在子线程执行,避免阻塞主线程) + new Thread(() -> sampleCurrentLocation()).start(); + + // 2. 发送上报 + sendProgressReport(); + + } catch (Exception e) { + e.printStackTrace(); + LogUtil.log(TAG, "定时任务异常:" + e.getMessage()); + } + + // 3. 2秒后再次执行(实现循环) + if (isRunning) { + reportHandler.postDelayed(this, REPORT_INTERVAL); + } + } + }; + + // 立即执行第一次 + reportHandler.post(reportRunnable); + } + + /** + * 停止定时上报(开关关闭) + */ + public void stopReporting() { + if (!isRunning) return; + + isRunning = false; + + // 最后发送一次 + sendProgressReport(); + + reportHandler.removeCallbacks(reportRunnable); + + pathPoints.clear(); + + LogUtil.log(TAG, "停止一键起飞进度定时上报"); + } + + /** + * 采样当前飞机位置(参数别改) + */ + private void sampleCurrentLocation() { + try { + LocationCoordinate3D locationCoordinate3D=KeyManager.getInstance().getValue(KeyTools.createKey(FlightControllerKey.KeyAircraftLocation3D)); + + double lat = locationCoordinate3D.getLatitude(); + double lng = locationCoordinate3D.getLongitude(); + double alt = locationCoordinate3D.getAltitude(); + + // 过滤无效坐标 + if (Math.abs(lat) < 0.0001 && Math.abs(lng) < 0.0001) return; + + TakeoffToPointProgress.PlannedPathPoint point = new TakeoffToPointProgress.PlannedPathPoint(); + point.setLatitude(lat); + point.setLongitude(lng); + point.setHeight(alt); + double remaining_distance=Gpsdistance.calculate3DDistance(lat,lng,alt,Movement.getInstance().getTakeofftargetlatitude(),Movement.getInstance().getTakeofftargetlongitude(),Movement.getInstance().getRtk_takeoff_altitude()); + double remaining_time=remaining_distance/Movement.getInstance().getSpeed(); + Movement.getInstance().setTakeoff_remaining_distance((float) remaining_distance); + Movement.getInstance().setTakeoff_remaining_time((float) remaining_time); + + pathPoints.add(point); + + // 限制最大点数 + if (pathPoints.size() > MAX_POINTS) { + pathPoints.remove(0); + } + + } catch (Exception e) { + LogUtil.log(TAG, "采样位置失败:" + e.getMessage()); + } + } + + /** + * 发送进度上报(参数别改) + */ + public void sendProgressReport() { + try { + if (!MqttManager.getInstance().mqttAndroidClient.isConnected()) { + LogUtil.log(TAG, "MQTT未连接,跳过上报"); + return; + } + TakeoffToPointProgress.Data data = new TakeoffToPointProgress.Data(); + data.setFlightId(PreferenceUtils.getInstance().getFlightId()); + data.setTrackId(PreferenceUtils.getInstance().getFlightId()); + data.setStatus(Movement.getInstance().getTakeoff_status()); + data.setResult(Movement.getInstance().getTakeoff_result()); + data.setWayPointIndex(Movement.getInstance().getCurrentWaypointIndex()); + + //计算一下这个剩余距离 + data.setRemainingDistance(Movement.getInstance().getTakeoff_remaining_distance()); + //计算一个这个剩余时间 + data.setRemainingTime(Movement.getInstance().getTakeoff_remaining_time()); + + data.setPlannedPathPoints(new ArrayList<>(pathPoints)); + + TakeoffToPointProgress progress = new TakeoffToPointProgress(); + progress.setBid(UUID.randomUUID().toString()); + progress.setTid(UUID.randomUUID().toString()); + progress.setTimestamp(System.currentTimeMillis()); + progress.setMethod("takeoff_to_point_progress"); + progress.setNeedReply(1); + progress.setData(data); + + String json = new Gson().toJson(progress); + MqttMessage mqttMessage = new MqttMessage(json.getBytes("UTF-8")); + mqttMessage.setQos(0); + MqttManager.getInstance().mqttAndroidClient.publish(AMSConfig.UP_UAV_EVENT, mqttMessage); + + LogUtil.log(TAG, "上报进度,轨迹点:" + pathPoints.size() + " 个"); + + } catch (Exception e) { + e.printStackTrace(); + LogUtil.log(TAG, "上报异常:" + e.getMessage()); + } + } + + /** + * 轨迹点实体(参数别改) + */ + public static class PathPoint { + @SerializedName("latitude") + private double latitude; + + @SerializedName("longitude") + private double longitude; + + @SerializedName("height") + private double height; + + public PathPoint(double latitude, double longitude, double height) { + this.latitude = latitude; + this.longitude = longitude; + this.height = height; + } + } + + /** + * 释放资源 + */ + public void release() { + stopReporting(); + instance = null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/Utils.java b/app/src/main/java/com/aros/apron/tools/Utils.java new file mode 100644 index 00000000..237785b1 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/Utils.java @@ -0,0 +1,143 @@ + +package com.aros.apron.tools; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; + +import com.google.gson.Gson; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.util.Locale; + +import dji.v5.common.error.IDJIError; + +public class Utils { + public static void printJson(String tag, String json) { + if (json == null || json.length() == 0) { + Log.e(tag, "JSON is empty"); + return; + } + + int maxLogSize = 4000; + for (int i = 0; i <= json.length() / maxLogSize; i++) { + int start = i * maxLogSize; + int end = (i + 1) * maxLogSize; + if (end >= json.length()) { + end = json.length(); + } + Log.e(tag, json.substring(start, end)); + } + } + /** + * //修改变焦数据为从前端拿2-200自己计算然后放入官方的sdk + * + * @param smallZoomFromWeb + * @return + */ + public static int getbigZoomValue(String smallZoomFromWeb) { + int zoomLength = Integer.parseInt(smallZoomFromWeb); + int bigZoom = (47549 - 317) / 199 * (zoomLength - 2) + 317; + return bigZoom; + } + + public static String sHA1(Context context){ + try { + PackageInfo info = null; + try { + info = context.getPackageManager().getPackageInfo( + context.getPackageName(), PackageManager.GET_SIGNATURES); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + byte[] cert = info.signatures[0].toByteArray(); + MessageDigest md = MessageDigest.getInstance("SHA1"); + byte[] publicKey = md.digest(cert); + StringBuffer hexString = new StringBuffer(); + for (int i = 0; i < publicKey.length; i++) { + String appendString = Integer.toHexString(0xFF & publicKey[i]) + .toUpperCase(Locale.US); + if (appendString.length() == 1) + hexString.append("0"); + hexString.append(appendString); + hexString.append(":"); + } + String result = hexString.toString(); + return result.substring(0, result.length()-1); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] getByte(String str){ + try { + byte[] bytes = str.getBytes("UTF-8"); + return bytes; + // 使用bytes + } catch (UnsupportedEncodingException e) { + // 处理异常,比如使用默认字符集 + byte[] bytes = str.getBytes(); + return null; + } + } + + public static double parseLatLon(String latLonStr) { + // 检查输入字符串是否为空或无效 + if (latLonStr == null || latLonStr.isEmpty()) { + throw new IllegalArgumentException("Input string is null or empty"); + } + + // 获取最后一个字符,即方向标识符 + char direction = latLonStr.charAt(latLonStr.length() - 1); + + // 检查方向标识符是否有效 + if (direction != 'N' && direction != 'S' && direction != 'E' && direction != 'W') { +return Double.parseDouble(latLonStr); } + + // 提取数字部分 + String numberPart = latLonStr.substring(0, latLonStr.length() - 1); + + // 将数字部分转换为Double + double number = Double.parseDouble(numberPart); + + // 根据方向标识符调整数值 + if (direction == 'S' || direction == 'W') { + number = -number; + } + + return number; + } + + public static String getIDJIErrorMsg(IDJIError idjiError){ + if (TextUtils.isEmpty(idjiError.description())){ + return new Gson().toJson(idjiError); + }else{ + return idjiError.description(); + } + } + + public static String getSDCardPath() { + String sdCardPathString = ""; + if (checkSDCard()) { + sdCardPathString = Environment.getExternalStorageDirectory().getPath(); + } else { + sdCardPathString = Environment.getExternalStorageDirectory() + .getParentFile() + .getPath(); + } + return sdCardPathString; + } + + public static boolean checkSDCard() { + return TextUtils.equals( + Environment.MEDIA_MOUNTED, + Environment.getExternalStorageState() + ); + } + +} diff --git a/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java b/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java new file mode 100644 index 00000000..d675cf6f --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/XTtsPcmGenerator.java @@ -0,0 +1,154 @@ +package com.aros.apron.tools; + +import com.aros.apron.entity.XTTSParams; +import com.iflytek.aikit.core.AeeEvent; +import com.iflytek.aikit.core.AiHandle; +import com.iflytek.aikit.core.AiHelper; +import com.iflytek.aikit.core.AiInput; +import com.iflytek.aikit.core.AiListener; +import com.iflytek.aikit.core.AiRequest; +import com.iflytek.aikit.core.AiResponse; +import com.iflytek.aikit.core.AiText; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.List; + +public class XTtsPcmGenerator { + + private static final String TAG = "XTTS"; + private static final String ABILITY_ID = "e2e44feff"; + + private AiHandle aiHandle; + private FileOutputStream pcmOut; + + + public void init(AiListener listener) { + AiHelper.getInst().registerListener(ABILITY_ID, listener); + } + + /** + * 合成文本为 PCM 文件 + */ + public int synthToPcm( + String text, + XTTSParams params, + File pcmFile + ) { + File parent = pcmFile.getParentFile(); + if (parent != null && !parent.exists()) { + parent.mkdirs(); + } + try { + pcmOut = new FileOutputStream(pcmFile, false); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + + AiInput.Builder paramBuilder = AiInput.builder(); + paramBuilder.param("vcn", params.vcn); + paramBuilder.param("language", params.language); + paramBuilder.param("textEncoding", "UTF-8"); + paramBuilder.param("pitch", params.pitch); + paramBuilder.param("speed", params.speed); + paramBuilder.param("volume", params.volume); + + aiHandle = AiHelper.getInst().start( + ABILITY_ID, + paramBuilder.build(), + null + ); + + if (aiHandle.getCode() != 0) { + LogUtil.log(TAG, "start failed: " + aiHandle.getCode()); + return aiHandle.getCode(); + } + + AiText aiText = AiText.get("text") + .data(text) + .valid(); + + AiRequest request = AiRequest.builder() + .payload(aiText) + .build(); + + int ret = AiHelper.getInst().write(request, aiHandle); + if (ret != 0) { + LogUtil.log(TAG, "write failed: " + ret); + return ret; + } + + return 0; + } + + /** + * 内部使用的监听器 + */ + public AiListener buildListener() { + return new AiListener() { + + @Override + public void onResult(int handleID, List list, Object usrContext) { + if (list == null){ + LogUtil.log(TAG,"list == null"); + return; + } + + for (AiResponse resp : list) { + if ("audio".equals(resp.getKey())) { + byte[] pcm = resp.getValue(); + if (pcm != null && pcm.length > 0) { + writePcm(pcm); + } + } + } + } + + @Override + public void onEvent(int handleID, int event, List eventData, Object usrContext) { + if (event == AeeEvent.AEE_EVENT_END.getValue()) { + close(); + } + } + + @Override + public void onError(int handleID, int err, String msg, Object usrContext) { + LogUtil.log(TAG, "XTTS error: " + err + ", " + msg); + close(); + } + }; + } + + private synchronized void writePcm(byte[] data) { + try { + if (pcmOut != null) { + pcmOut.write(data); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void close() { + try { + if (pcmOut != null) { + pcmOut.flush(); + pcmOut.close(); + pcmOut = null; + } + } catch (Exception ignored) {} + + if (aiHandle != null) { + AiHelper.getInst().end(aiHandle); + aiHandle = null; + } + } + + /** + * 释放引擎(App 退出时调用) + */ +// public void release() { +// AiHelper.getInst().engineUnInit(ABILITY_ID); +// } +} \ No newline at end of file diff --git a/app/src/main/java/com/aros/apron/tools/mixrongapron.java b/app/src/main/java/com/aros/apron/tools/mixrongapron.java new file mode 100644 index 00000000..8e5ca352 --- /dev/null +++ b/app/src/main/java/com/aros/apron/tools/mixrongapron.java @@ -0,0 +1,4 @@ +package com.aros.apron.tools; + +public class mixrongapron { +}