This commit is contained in:
Developer
2026-05-18 17:58:11 +08:00
commit 194033514f
49 changed files with 3414 additions and 0 deletions

9
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

60
android/build.gradle Normal file
View File

@@ -0,0 +1,60 @@
group = "com.example.terra"
version = "1.0"
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
flatDir{
dirs project(':terra').file('libs')
}
}
}
apply plugin: "com.android.library"
android {
if (project.android.hasProperty("namespace")) {
namespace = "com.example.terra"
}
compileSdk = 34
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
defaultConfig {
minSdk = 29
}
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
implementation(name: 'terra',ext: 'aar')
implementation(name: 'RamanMatch',ext: 'aar')
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

BIN
android/libs/RamanMatch.aar Normal file

Binary file not shown.

BIN
android/libs/terra.aar Normal file

Binary file not shown.

1
android/settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'terra'

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.terra">
</manifest>

View File

@@ -0,0 +1,974 @@
package com.example.terra;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.optics.terra.Device;
import com.optics.terra.DeviceWrapper;
import android.content.Context;
import android.util.Log;
import com.example.terra.utils.ArrayUtils;
import com.ramanmatch.match.RamanMatch;
import com.ramanmatch.match.MatchResult;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* TerraPlugin - 拉曼光谱仪 Flutter 通信插件
*
* 提供 Flutter 与拉曼光谱仪硬件设备之间的 MethodChannel 通信桥梁,
* 支持设备管理、参数配置、光谱采集、数据处理和算法匹配等功能。
*/
public class TerraPlugin implements FlutterPlugin, MethodCallHandler {
private static final String TAG = "TerraPlugin";
private static final String CHANNEL_NAME = "terra";
/** 默认激光功率 */
private static final int DEFAULT_LASER_POWER = 500;
/** 默认积分时间 (ms) */
private static final int DEFAULT_INTEGRATION_TIME = 500;
/** 消荧光因子,数字越小削得越狠 */
private static final int FLUORESCENCE_REMOVAL_FACTOR = 15;
/** 光谱饱和阈值 */
private static final double SATURATION_THRESHOLD = 65535.0;
/** 波数起始截取值 */
private static final double WAVE_NUMBER_START = 200.0;
/** 设备 SN 码与算法激活码、激光系数的映射表 */
private static final Map<String, String[]> DEVICE_CONFIG_MAP;
//SN算法激活码激光系数
static {
Map<String, String[]> map = new HashMap<>();
map.put("B7BP340105", new String[]{"2bb7b4a2206a90bea48fe999cace47ca", "0080080016002500"});
map.put("B7BP340101", new String[]{"10a1cf036a325b3e747dccefcf57ce84", "0081075016002300"});
map.put("B7BP230208", new String[]{"8bec16f120a49cd9987cd7f4dab60510", "0100100030004800"});
map.put("B7BP230209", new String[]{"0eb603282ef9d0f73a47a548c9cb8c9e", "0020023006801400"});
DEVICE_CONFIG_MAP = Collections.unmodifiableMap(map);
}
private MethodChannel channel;
private Context context;
private Device device;
/** 200 波数对应的像素位置索引 */
private int startPxIndexForWaveNumber = 0;
/** 当前积分时间 (ms) */
private int integrationTime = DEFAULT_INTEGRATION_TIME;
/** 波数数组 */
private double[] waveNumber = null;
/** USB 操作单线程执行器,确保所有调用串行化 */
private volatile ExecutorService usbExecutor = Executors.newSingleThreadExecutor();
/** 普通 USB 调用超时5 秒) */
private static final int USB_CALL_TIMEOUT_MS = 5000;
/** 光谱采集超时60 秒,支持深度模式长时间采集) */
private static final int SPECTRUM_TIMEOUT_MS = 60000;
// ==================== 生命周期方法 ====================
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), CHANNEL_NAME);
channel.setMethodCallHandler(this);
context = flutterPluginBinding.getApplicationContext();
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
/** Flutter MethodChannel 方法调用分发 */
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
// --- 平台信息 ---
case "getPlatformVersion":
result.success("Android " + android.os.Build.VERSION.RELEASE);
break;
// --- 设备管理 ---
case "openAllDevices":
handleOpenAllDevices(result);
break;
case "closeAllDevices":
handleCloseAllDevices(result);
break;
case "close":
handleCloseDevice(result);
break;
case "getDeviceSn":
handleGetDeviceSn(result);
break;
case "getIndex":
handleGetIndex(result);
break;
case "isConnect":
handleIsConnect(result);
break;
case "openDebug":
handleOpenDebug();
result.success(true);
break;
case "checkUsbPermission":
handleCheckUsbPermission(result);
break;
// --- CCD 制冷控制 ---
case "setCCDTECOn":
safeBooleanCall(result, () -> device.setCCDTECOn());
break;
case "setCCDTECOff":
safeBooleanCall(result, () -> device.setCCDTECOff());
break;
case "setCCDTECTemperature":
safeBooleanCall(result, () -> device.setCCDTECTemperature(getIntArg(call, "temperature")));
break;
case "getCCDTECState":
result.success(device.getCCDTECState());
break;
// --- 参数配置 ---
case "setIntegrationTime":
handleSetIntegrationTime(call, result);
break;
case "setAverageTimes":
handleSetAverageTimes(call, result);
break;
case "setTriggerMode":
handleSetTriggerMode(call, result);
break;
// --- 光谱数据获取 ---
case "getSpectrum":
result.success(device.getSpectrum());
break;
case "getResetSpectrum":
result.success(device.getResetSpectrum());
break;
case "getWavelength":
result.success(device.getWavelength());
break;
case "getWaveNumber":
result.success(device.getWaveNumber());
break;
case "getXvalue":
result.success(ArrayUtils.InterpolX);
break;
// --- 脉冲参数控制 ---
case "setLampDelayTime":
safeBooleanCall(result, () -> device.setLampDelayTime(getDoubleArg(call, "delayTime")));
break;
case "setLampWidth":
safeBooleanCall(result, () -> device.setLampWidth(getDoubleArg(call, "width")));
break;
case "setLampInterval":
safeBooleanCall(result, () -> device.setLampInterval(getDoubleArg(call, "interval")));
break;
case "getLampDelayTime":
safeDoubleCall(result, () -> device.getLampDelayTime());
break;
case "getLampWidth":
safeDoubleCall(result, () -> device.getLampWidth());
break;
case "getLampInterval":
safeDoubleCall(result, () -> device.getLampInterval());
break;
// --- 激发波长与激光控制 ---
case "setExcitedWaveLength":
safeBooleanCall(result, () -> device.setExcitedWaveLength(getIntArg(call, "excitedWaveLength")));
break;
case "setLaserPower":
int power = getIntArg(call, "power");
Log.i(TAG, "setLaserPower 请求: power=" + power);
if (power < 0 || power > 2000) {
Log.e(TAG, "setLaserPower 参数超出范围 (0-2000): " + power + ",拒绝执行");
result.success(false);
break;
}
safeBooleanCall(result, "setLaserPower", () -> device.setLaserPower(power));
break;
case "setLaserOn":
Log.i(TAG, "收到 setLaserOn 请求");
safeBooleanCall(result, () -> {
boolean is_tec_open = device.isTECOpen();
Log.i(TAG, "TEC 是否打开:" + is_tec_open);
if (!is_tec_open) {
boolean set_tec_res = device.setTECOn();
Log.i(TAG, "TEC 打开结果:" + set_tec_res);
}
boolean res = device.setLaserOn();
Log.i(TAG, "device.setLaserOn() 返回:" + res);
return res;
});
break;
case "setLaserOff":
Log.i(TAG, "收到 setLaserOff 请求");
safeBooleanCall(result, () -> {
boolean res = device.setLaserOff();
Log.i(TAG, "device.setLaserOff() 返回:" + res);
return res;
});
break;
case "setLaserPowerOn":
safeBooleanCall(result, () -> device.setLaserPowerOn());
break;
case "setLaserPowerOff":
safeBooleanCall(result, () -> device.setLaserPowerOff());
break;
// --- 激光校正系数 ---
case "getLaserPowerCorrectCoefficient":
handleGetLaserPowerCorrectCoefficient(result);
break;
case "setLaserPowerCorrectCoefficient":
handleSetLaserPowerCorrectCoefficient(call, result);
break;
// --- 硬件开关控制 ---
case "setTECOn":
safeBooleanCall(result, () -> device.setTECOn());
break;
case "setTECOff":
safeBooleanCall(result, () -> device.setTECOff());
break;
case "setMainBoardOn":
safeBooleanCall(result, () -> device.setMainBoardOn());
break;
case "setMainBoardOff":
safeBooleanCall(result, () -> device.setMainBoardOff());
break;
case "isMainBoardOpen":
safeBooleanCall(result, () -> device.isMainBoardOpen());
break;
case "isTECOpen":
safeBooleanCall(result, () -> device.isTECOpen());
break;
// --- 光谱数据处理 ---
case "waveletSmooth":
handleWaveletSmooth(call, result);
break;
case "removeFluorescence":
handleRemoveFluorescence(call, result);
break;
case "lineInterpolation":
handleLineInterpolation(call, result);
break;
case "getRamanSpectrum":
handleGetRamanSpectrum(result);
break;
case "findPeaks":
handleFindPeaks(call, result);
break;
// --- 算法匹配与校准 ---
case "ramanMatchRegister":
handleRamanMatchRegister(result);
break;
case "ramanMatchCalcSimilarity":
handleRamanMatchCalcSimilarity(call, result);
break;
case "calibration":
handleCalibration(call, result);
break;
case "getYJCorrectCoefficient":
result.success(device.getYJCorrectCoefficient());
break;
case "setYJCorrectCoefficient":
handleSetYJCorrectCoefficient(call, result);
break;
// --- 其他校准信息查询 ---
case "getYAxisCorrectCoefficient":
result.success(device.getYAxisCorrectCoefficient());
break;
case "getCorrectForDetectorNonlinear":
result.success(device.getCorrectForDetectorNonlinear());
break;
case "getWavelengthCalibrationCoefficients":
result.success(device.getWavelengthCalibrationCoefficients());
break;
case "getFpgaVersion":
result.success(device.getFpgaVersion());
break;
case "getSlit":
result.success(device.getSlit());
break;
case "getBadPoints":
result.success(device.getBadPoints());
break;
case "setLampEnable":
safeBooleanCall(result, () -> device.setLampEnable((Boolean) ((Map<String, Object>) call.arguments()).get("enable")));
break;
// --- MD5 加密 ---
case "bytesToMD5":
String sn = (String) ((Map<String, Object>) call.arguments()).get("sn");
result.success(computeMD5(sn));
break;
default:
result.notImplemented();
break;
}
}
// ==================== 设备管理处理 ====================
/** 打开所有已连接的设备,并初始化第一个设备的默认参数 */
private void handleOpenAllDevices(Result result) {
try {
List<Device> deviceList = DeviceWrapper.openAllDevices(context);
device = deviceList.get(0);
device.setCallBack(state -> {
if (state) {
initDeviceDefaults();
}
});
result.success(deviceList.size());
} catch (Exception e) {
result.success(0);
}
}
/** 初始化设备默认参数(激光功率、积分时间、波数起始索引、激光校正系数) */
private void initDeviceDefaults() {
device.setLaserPower(DEFAULT_LASER_POWER);
device.setIntegrationTime(integrationTime);
waveNumber = device.getWaveNumber();
startPxIndexForWaveNumber = findWaveNumberStartIndex(waveNumber);
// 设置激光校正系数
String sn = device.getSerialNumber();
String laserCoefficient = getLaserPowerCoefficient(sn);
Log.i(TAG, "初始化设备参数SN=" + sn + ", 激光校正系数:" + laserCoefficient);
if (laserCoefficient != null && !laserCoefficient.isEmpty() && laserCoefficient.length() == 16) {
try {
device.setLaserPowerCorrectCoefficient(laserCoefficient);
Log.i(TAG, "激光校正系数设置成功:" + laserCoefficient);
} catch (Exception e) {
Log.e(TAG, "设置激光校正系数失败:" + e.getMessage(), e);
}
} else {
Log.w(TAG, "激光校正系数为空或格式错误,跳过设置");
}
}
/**
* 查找波数数组中 >= 200 波数的起始索引位置
*
* @param waveNumber 波数数组
* @return 起始索引(目标位置的前一个),未找到返回 0
*/
private int findWaveNumberStartIndex(double[] waveNumber) {
for (int i = 0; i < waveNumber.length; i++) {
if (waveNumber[i] >= WAVE_NUMBER_START) {
return i - 1;
}
}
return 0;
}
/** 关闭所有设备并释放引用 */
private void handleCloseAllDevices(Result result) {
try {
DeviceWrapper.closeAllDevices();
device = null;
result.success(true);
} catch (Exception e) {
result.success(false);
}
}
/** 获取设备序列号 */
private void handleGetDeviceSn(Result result) {
try {
result.success(device.getSerialNumber());
} catch (Exception e) {
result.success("");
}
}
/** 检查设备是否已连接 */
private void handleIsConnect(Result result) {
try {
result.success(device.isConnect());
} catch (Exception e) {
result.success(false);
}
}
/** 关闭单个设备 */
private void handleCloseDevice(Result result) {
try {
if (device != null) {
device.close();
device = null;
}
result.success(true);
} catch (Exception e) {
result.success(false);
}
}
/** 获取设备索引 */
private void handleGetIndex(Result result) {
try {
result.success(device != null ? device.getIndex() : -1);
} catch (Exception e) {
result.success(-1);
}
}
/** 开启 SDK 调试日志 */
private void handleOpenDebug() {
DeviceWrapper.openDebug();
}
/** 检查 USB 权限SDK 的 PermissionCallBack 接口在 AAR 中不可用,此处占位) */
private void handleCheckUsbPermission(Result result) {
Map<String, Object> response = new HashMap<>();
response.put("deviceIndex", 0);
response.put("state", device != null && device.isConnect());
result.success(response);
}
// ==================== 参数配置处理 ====================
/** 设置积分时间 */
private void handleSetIntegrationTime(MethodCall call, Result result) {
result.success(device.setIntegrationTime(getDoubleArg(call, "integrationTime")));
}
/** 设置平均次数(设备方法无返回值,固定返回 true */
private void handleSetAverageTimes(MethodCall call, Result result) {
device.setAverageTimes(getIntArg(call, "averageTimes"));
result.success(true);
}
/** 设置触发模式 */
private void handleSetTriggerMode(MethodCall call, Result result) {
result.success(device.setTriggerMode(getIntArg(call, "triggerMode")));
}
// ==================== 激光校正系数处理 ====================
/** 获取激光校正系数 */
private void handleGetLaserPowerCorrectCoefficient(Result result) {
try {
double[] coefficient = device.getLaserPowerCorrectCoefficient();
result.success(coefficient);
} catch (Exception e) {
Log.e(TAG, "获取激光校正系数失败:" + e.getMessage(), e);
result.success(new double[]{});
}
}
/**
* 设置激光校正系数
* @param call 调用参数,包含 calibrationCoefficient 字符串
* @param result 回调结果
*/
private void handleSetLaserPowerCorrectCoefficient(MethodCall call, Result result) {
try {
String calibrationCoefficient = (String) ((Map<String, Object>) call.arguments()).get("calibrationCoefficient");
if (calibrationCoefficient == null || calibrationCoefficient.length() != 16) {
Log.e(TAG, "激光校正系数格式错误,必须为 16 位字符串");
result.success(false);
return;
}
device.setLaserPowerCorrectCoefficient(calibrationCoefficient);
result.success(true);
} catch (Exception e) {
Log.e(TAG, "设置激光校正系数失败:" + e.getMessage(), e);
result.success(false);
}
}
// ==================== 光谱数据处理 ====================
/** 小波平滑处理 */
private void handleWaveletSmooth(MethodCall call, Result result) {
try {
double[] spectrum = getDoubleArrayArg(call, "spectrum");
result.success(device.waveletSmooth(spectrum));
} catch (Exception e) {
result.success(new double[]{});
}
}
/** 消荧光处理 */
private void handleRemoveFluorescence(MethodCall call, Result result) {
try {
Map<String, Object> arguments = call.arguments();
double[] spectrum = getDoubleArrayArg(call, "spectrum");
int side = FLUORESCENCE_REMOVAL_FACTOR;
if (arguments != null && arguments.containsKey("side")) {
side = ((Number) arguments.get("side")).intValue();
}
result.success(device.removeFluorescence(spectrum, side));
} catch (Exception e) {
result.success(new double[]{});
}
}
/** 线性插值处理 */
private void handleLineInterpolation(MethodCall call, Result result) {
try {
double[] xDataRaw = getDoubleArrayArg(call, "xDataRaw");
double[] yDataRaw = getDoubleArrayArg(call, "yDataRaw");
double[] xData = getDoubleArrayArg(call, "xData");
result.success(device.lineInterpolation(xDataRaw, yDataRaw, xData));
} catch (Exception e) {
result.success(new double[]{});
}
}
/**
* 获取经过完整处理流程的拉曼光谱
* 在独立线程中执行,带 30 秒超时保护
*/
private void handleGetRamanSpectrum(Result result) {
runWithTimeout(result, "getRamanSpectrum", this::acquireAndProcessSpectrum, SPECTRUM_TIMEOUT_MS, new double[]{});
}
/** 寻峰处理 */
private void handleFindPeaks(MethodCall call, Result result) {
Map<String, Object> arguments = call.arguments();
double[] spectrum = getDoubleArrayArg(call, "spectrum");
int minIndicesBetweenPeaks = (int) arguments.get("minIndicesBetweenPeaks");
double baseline = (double) arguments.get("baseline");
result.success(device.findPeaks(spectrum, minIndicesBetweenPeaks, baseline));
}
// ==================== 算法匹配与校准处理 ====================
/** 注册检测算法(根据设备 SN 码获取激活码) */
private void handleRamanMatchRegister(Result result) {
String sn = device.getSerialNumber();
String activationCode = getActivationCode(sn);
Log.i(TAG, "算法注册sn=" + sn + ", code=" + activationCode);
result.success(RamanMatch.register(device, activationCode));
}
/** 光谱相似度比对 */
@SuppressWarnings("unchecked")
private void handleRamanMatchCalcSimilarity(MethodCall call, Result result) {
Map<String, ArrayList<Double>> arguments = call.arguments();
double[] spectrum = arrayListToDoubleArray(arguments.get("spectrum"));
double[] libSpectrum = arrayListToDoubleArray(arguments.get("libSpectrumList"));
MatchResult matchResult = RamanMatch.getInstance().calcSimilarity(spectrum, libSpectrum);
result.success(matchResult.getSimilarity());
}
/** 光谱校准 */
@SuppressWarnings("unchecked")
private void handleCalibration(MethodCall call, Result result) {
try {
Map<String, Object> arguments = call.arguments();
double[] spectrum = (double[]) arguments.get("spectrum");
double[] yjCoeff = arrayListToDoubleArray((ArrayList<Double>) arguments.get("YJCoeff"));
double[] calibrationResult = RamanMatch.getInstance().calibration(spectrum, yjCoeff);
if (calibrationResult == null) {
Log.e(TAG, "校准失败RamanMatch.calibration 返回 null");
result.success(new double[]{});
} else {
result.success(calibrationResult);
}
} catch (Exception e) {
Log.e(TAG, "校准异常:" + e.getMessage(), e);
result.success(new double[]{});
}
}
/** 设置乙腈定标系数 */
private void handleSetYJCorrectCoefficient(MethodCall call, Result result) {
try {
Map<String, Object> arguments = call.arguments();
Object coffesObj = arguments.get("coffes");
double[] coffes;
if (coffesObj instanceof double[]) {
// 直接是 double 数组
coffes = (double[]) coffesObj;
} else if (coffesObj instanceof ArrayList) {
// ArrayList 转换为 double 数组
coffes = arrayListToDoubleArray((ArrayList<Double>) coffesObj);
} else {
Log.e(TAG, "coffes 参数类型错误:" + coffesObj.getClass().getName());
result.success(false);
return;
}
result.success(device.setYJCorrectCoefficient(coffes));
} catch (Exception e) {
Log.e(TAG, "设置乙腈定标系数失败:" + e.getMessage(), e);
result.success(false);
}
}
// ==================== 光谱采集核心流程 ====================
/**
* 执行完整的拉曼光谱采集与处理流程
* 流程:关激光 -> 采集背景 -> 开激光 -> 等待稳定 -> 采集样本 -> 关激光 -> 饱和检测 -> 扣除背景 -> 插值 -> Y 轴校正 -> 消荧光 -> 平滑 -> 取整
*
* @return 处理完成的光谱数据(取整后),饱和时返回空数组
*/
private double[] acquireAndProcessSpectrum() {
// 1. 确保激光关闭后采集背景光谱
device.setLaserOff();
double[] bgSpectrum = device.getResetSpectrum();
// 2. 开激光
boolean laserOnRes = device.setLaserOn();
Log.i(TAG, "开激光结果:" + laserOnRes);
// 3. 等待激光功率稳定2 秒)
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.w(TAG, "激光稳定等待被中断");
}
// 4. 采集样本光谱
double[] sampleSpectrum = device.getResetSpectrum();
// 5. 关激光
device.setLaserOff();
// 6. 饱和检测
if (isSpectrumSaturated(sampleSpectrum)) {
Log.w(TAG, "光谱饱和,数据无效");
return new double[]{};
}
// 7. 扣除背景
double[] spectrum = subtractBackground(sampleSpectrum, bgSpectrum);
// 8. 插值到波数 200~3200
spectrum = device.lineInterpolation(waveNumber, spectrum, ArrayUtils.InterpolX);
// 9. Y 轴校正
spectrum = device.yAxisCorrect(waveNumber, spectrum);
// 10. 消荧光
spectrum = device.removeFluorescence(spectrum, FLUORESCENCE_REMOVAL_FACTOR);
// 11. 平滑
spectrum = device.waveletSmooth(spectrum);
// 12. 取整
roundInPlace(spectrum);
return spectrum;
}
/**
* 扣除背景光谱(样本 - 背景)
*
* @param sample 样本光谱
* @param background 背景光谱
* @return 扣除背景后的光谱数据
*/
private double[] subtractBackground(double[] sample, double[] background) {
int length = Math.min(background.length, sample.length);
double[] result = new double[length];
for (int i = 0; i < length; i++) {
result[i] = sample[i] - background[i];
}
return result;
}
/** 对数组每个元素四舍五入取整(原地修改) */
private void roundInPlace(double[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = Math.round(array[i]);
}
}
/**
* 判断光谱是否饱和200 波数之后是否存在>=65535 的值)
*
* @param spectrumRaw 原始光谱数据
* @return true 表示已饱和
*/
private boolean isSpectrumSaturated(double[] spectrumRaw) {
if (startPxIndexForWaveNumber <= 0) {
return false;
}
for (int i = startPxIndexForWaveNumber; i < spectrumRaw.length; i++) {
if (spectrumRaw[i] >= SATURATION_THRESHOLD) {
return true;
}
}
return false;
}
// ==================== 安全调用包装(带超时保护) ====================
/**
* 超时后放弃当前 executor 并重建
* 因为 bulkTransfer 是 JNI native 调用Thread.interrupt() 无法中断它,
* 被阻塞的线程会永远持有 synchronized 锁。
* 唯一办法是抛弃 executor 创建新的,让后续操作在新线程中执行。
*/
private synchronized void abandonExecutor(String reason) {
Log.w(TAG, reason + ":放弃当前 executor 并重建");
try {
usbExecutor.shutdownNow();
} catch (Exception ignored) {}
usbExecutor = Executors.newSingleThreadExecutor();
}
/**
* 带超时保护的设备操作包装器
*
* @param result Flutter 回调
* @param action 设备操作
*/
private void safeBooleanCall(Result result, BooleanAction action) {
safeBooleanCall(result, "", action);
}
/**
* 带超时保护的设备操作包装器(带操作名称日志)
*
* @param result Flutter 回调
* @param actionName 操作名称(用于日志)
* @param action 设备操作
*/
private void safeBooleanCall(Result result, String actionName, BooleanAction action) {
final ExecutorService currentExecutor = usbExecutor;
Future<Boolean> future = currentExecutor.submit(() -> {
boolean success = action.execute();
if (!actionName.isEmpty()) {
Log.i(TAG, actionName + " 执行结果:" + success);
}
return success;
});
try {
boolean success = future.get(USB_CALL_TIMEOUT_MS, TimeUnit.MILLISECONDS);
result.success(success);
} catch (TimeoutException e) {
abandonExecutor(actionName + " 超时");
result.success(false);
} catch (Exception e) {
Log.e(TAG, (actionName.isEmpty() ? "safeBooleanCall" : actionName) + " 执行失败:" + e.getMessage(), e);
result.success(false);
}
}
/**
* 通用 double 结果的安全调用包装,异常时返回空数组
*
* @param result Flutter 回调
* @param action 设备操作
*/
private void safeDoubleCall(Result result, DoubleAction action) {
try {
result.success(action.execute());
} catch (Exception e) {
result.success(new double[]{});
}
}
/**
* 带超时保护的 double 数组结果调用包装器
*
* @param result Flutter 回调
* @param actionName 操作名称(用于日志)
* @param action 设备操作
*/
private void safeDoubleArrayCall(Result result, String actionName, DoubleArrayAction action) {
final ExecutorService currentExecutor = usbExecutor;
Future<double[]> future = currentExecutor.submit(() -> {
double[] arr = action.execute();
if (arr == null) return new double[]{};
return arr;
});
try {
result.success(future.get(USB_CALL_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
abandonExecutor(actionName + " 超时");
result.success(new double[]{});
} catch (Exception e) {
Log.e(TAG, actionName + " 执行失败:" + e.getMessage(), e);
result.success(new double[]{});
}
}
/**
* 执行一个带超时的操作(任意返回类型)
* 超时后放弃 executor 并重建,确保后续调用不会被阻塞线程影响
*/
private <T> void runWithTimeout(Result result, String actionName, Callable<T> action, int timeoutMs, T fallback) {
final ExecutorService currentExecutor = usbExecutor;
Future<T> future = currentExecutor.submit(action);
try {
T value = future.get(timeoutMs, TimeUnit.MILLISECONDS);
result.success(value);
} catch (TimeoutException e) {
abandonExecutor(actionName + " 超时");
result.success(fallback);
} catch (Exception e) {
Log.e(TAG, actionName + " 执行失败:" + e.getMessage(), e);
result.success(fallback);
}
}
/** 返回 boolean 的设备操作接口 */
@FunctionalInterface
private interface BooleanAction {
boolean execute();
}
/** 返回 double 的设备操作接口 */
@FunctionalInterface
private interface DoubleAction {
double execute();
}
/** 返回 double[] 的设备操作接口 */
@FunctionalInterface
private interface DoubleArrayAction {
double[] execute();
}
// ==================== 参数提取工具方法 ====================
/** 从 MethodCall 参数中提取 double 值 */
private double getDoubleArg(MethodCall call, String key) {
Map<String, Object> arguments = call.arguments();
return ((Number) arguments.get(key)).doubleValue();
}
/** 从 MethodCall 参数中提取 int 值 */
private int getIntArg(MethodCall call, String key) {
Map<String, Object> arguments = call.arguments();
return ((Number) arguments.get(key)).intValue();
}
/**
* 从 MethodCall 参数中提取 double 数组,兼容 ArrayList 和原生数组
*
* @param call MethodCall 对象
* @param key 参数名
* @return double 数组
*/
private double[] getDoubleArrayArg(MethodCall call, String key) {
Map<String, Object> arguments = call.arguments();
return toDoubleArray(arguments.get(key));
}
// ==================== 类型转换工具方法 ====================
/**
* 将 Object 转换为 double 数组,兼容 ArrayList 和原生数组两种类型
*
* @param obj 待转换对象
* @return double 数组
* @throws IllegalArgumentException 类型不支持或包含非 Number 元素时抛出
*/
private double[] toDoubleArray(Object obj) {
if (obj instanceof ArrayList) {
ArrayList<?> list = (ArrayList<?>) obj;
double[] result = new double[list.size()];
for (int i = 0; i < list.size(); i++) {
result[i] = ((Number) list.get(i)).doubleValue();
}
return result;
} else if (obj.getClass().isArray()) {
int length = java.lang.reflect.Array.getLength(obj);
double[] result = new double[length];
for (int i = 0; i < length; i++) {
result[i] = ((Number) java.lang.reflect.Array.get(obj, i)).doubleValue();
}
return result;
}
throw new IllegalArgumentException("Provided object is neither an ArrayList nor an array");
}
/** 将 ArrayList<Double>转换为 double 原生数组 */
private double[] arrayListToDoubleArray(ArrayList<Double> list) {
double[] result = new double[list.size()];
for (int i = 0; i < list.size(); i++) {
result[i] = list.get(i);
}
return result;
}
// ==================== 加密与激活码 ====================
/**
* 计算字符串的 MD5 哈希值
*
* @param input 输入字符串
* @return MD5 十六进制字符串,异常时返回 null
*/
public static String computeMD5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xFF));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
/**
* 根据设备 SN 码获取算法激活码
*
* @param sn 设备序列号
* @return 对应的激活码,未注册的 SN 返回空字符串
*/
public String getActivationCode(String sn) {
String[] config = DEVICE_CONFIG_MAP.getOrDefault(sn, new String[]{"", ""});
return config[0];
}
/**
* 根据设备 SN 码获取激光校正系数
*
* @param sn 设备序列号
* @return 对应的激光校正系数16 位字符串),未注册的 SN 返回空字符串
*/
public String getLaserPowerCoefficient(String sn) {
String[] config = DEVICE_CONFIG_MAP.getOrDefault(sn, new String[]{"", ""});
return config[1];
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
package com.example.terra;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import org.junit.Test;
/**
* This demonstrates a simple unit test of the Java portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
public class TerraPluginTest {
@Test
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
TerraPlugin plugin = new TerraPlugin();
final MethodCall call = new MethodCall("getPlatformVersion", null);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(call, mockResult);
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
}
}