镜头降落

This commit is contained in:
cxf 2026-04-08 13:43:50 +08:00
parent 4c2d20f1b4
commit 984dadb7bf
23 changed files with 6001 additions and 0 deletions

View File

@ -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> 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<Points> getPoints() {
return points;
}
public void setPoints(List<Points> 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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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<Boolean>() {
@Override
public void subscribe(ObservableEmitter<Boolean> 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<Boolean>() {
@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;
}
}

View File

@ -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<NavigationSatelliteSystem>() {
@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);
}
}
});
}
}
}

View File

@ -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_12代表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;
}
}

View File

@ -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<Integer>() {
@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<Integer>() {
@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合成失败");
}
}
}

View File

@ -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;
}
}

View File

@ -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<String> 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;
}
}

View File

@ -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处理12跳过
return;
}
if (currentFrame % 30 != 0) { // 0处理12跳过
//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<Mat> 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<ArucoMarker> smallMarkers = new ArrayList<>();
List<ArucoMarker> bigMarkers1_4 = new ArrayList<>();
// 禁用5号逻辑注释掉但保留变量避免编译错误
// List<ArucoMarker> bigMarker5 = new ArrayList<>();
List<ArucoMarker> bigMarker6 = new ArrayList<>();
int[] idArray = ids.toArray();
List<Integer> 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());
}
// ========== 关键修改阶段06号旋转阶段最高优先级 ==========
// 只要看到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);
}
// 优先级21-4号几何中心下降
else if (!bigMarkers1_4.isEmpty()) {
currentLandingMode = 2;
processBigMarkersCenter(bigMarkers1_4, rgbMat.width(), rgbMat.height(), ultHeight);
}
// 优先级36号纯下降旋转完成后没看到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<ArucoMarker> 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<ArucoMarker> 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);
}
}

View File

@ -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);
}
}

View File

@ -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.00319像素也能检
// 畸变/反光/模糊宽容
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;
}
}

View File

@ -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. <wpml:mission> 基本信息 */
WaylineMission mission = new WaylineMission();
mission.setAuthor("aros");
double now = System.currentTimeMillis();
mission.setCreateTime(now);
mission.setUpdateTime(now);
/* 2. <wpml:missionConfig> 全局策略 */
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<WaylineWaypoint> wpList = new ArrayList<>();
wpList.add(wp1);
wpList.add(wp);
/* 4. <wpml:waypointInfo> */
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. <wpml:coordinateParam> */
WaylineCoordinateParam coordParam = new WaylineCoordinateParam();
coordParam.setCoordinateMode(WaylineCoordinateMode.WGS84);
coordParam.setAltitudeMode(WaylineAltitudeMode.EGM96);
coordParam.setPositioningType(WaylinePositioningType.GPS);
/* 6. <wpml:template> */
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;
}
}
}

View File

@ -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()
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<TakeoffToPointProgress.PlannedPathPoint> 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;
}
}

View File

@ -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()
);
}
}

View File

@ -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<AiResponse> 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<AiResponse> 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);
// }
}

View File

@ -0,0 +1,4 @@
package com.aros.apron.tools;
public class mixrongapron {
}