init
This commit is contained in:
9
android/.gitignore
vendored
Normal file
9
android/.gitignore
vendored
Normal 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
60
android/build.gradle
Normal 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
BIN
android/libs/RamanMatch.aar
Normal file
Binary file not shown.
BIN
android/libs/terra.aar
Normal file
BIN
android/libs/terra.aar
Normal file
Binary file not shown.
1
android/settings.gradle
Normal file
1
android/settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'terra'
|
||||
3
android/src/main/AndroidManifest.xml
Normal file
3
android/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.terra">
|
||||
</manifest>
|
||||
974
android/src/main/java/com/example/terra/TerraPlugin.java
Normal file
974
android/src/main/java/com/example/terra/TerraPlugin.java
Normal 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];
|
||||
}
|
||||
}
|
||||
217
android/src/main/java/com/example/terra/utils/ArrayUtils.java
Normal file
217
android/src/main/java/com/example/terra/utils/ArrayUtils.java
Normal file
File diff suppressed because one or more lines are too long
29
android/src/test/java/com/example/terra/TerraPluginTest.java
Normal file
29
android/src/test/java/com/example/terra/TerraPluginTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user