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

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
- platform: android
create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
LICENSE Normal file
View File

@@ -0,0 +1 @@
TODO: Add your license here.

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# terra
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/developing-packages/),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
analysis_options.yaml Normal file
View File

@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

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

593
docs/sdk_apis.md Normal file
View File

@@ -0,0 +1,593 @@
# Terra SDK 接口文档
## Terra SDK
Terra 是设备的名称,包含了光谱仪、激光器、拉曼相关接口。
## 接口列表
| 类名称 | 描述 |
|--------|------|
| DeviceWrapper | 设备管理器 |
| Device | 设备 |
## 调用示例
**step1: 获取 USB 设备**
```java
List<Device> allDevices = DeviceWrapper.openAllDevices(mContext);
```
**step2: 设备初始化**,初始化完成后通过回调方式通知客户端
```java
device.setCallBack(new Device.CallBack() {
@Override
public void onDeviceStatue(boolean state) {
Log.d(TAG, "state = " + state );
String sn = "";
if(state){
sn = device.getSerialNumber();
}
//...
}
});
```
**step3: 调用设备**
```java
double[] spectrum = device.getSpectrum();
```
## DeviceWrapper
获取或关闭所有设备
### Methods
#### openAllDevices
```java
/**
* 打开所有设备
*
* @param context Application context
* @return 挂在的所有设备
*/
public static List<Device> openAllDevices(Context context);
```
#### closeAllDevices
```java
/**
* 关闭所有设备
*/
public static void closeAllDevices();
```
#### openDebug
```java
/**
* 开启日志输出
*/
public static void openDebug();
```
#### checkUsbPermission
```java
/**
* 检测是否有 USB 权限
*/
public static void checkUsbPermission(Context context, PermissionCallBack permissionCallBack) {
UsbConnect.checkUsbPermission(context,permissionCallBack);
}
```
#### PermissionCallBack
```java
/**
* USB 权限状态回调
*/
public interface PermissionCallBack{
/**
* USB 权限状态
* @param deviceIndex 设备索引
* @param state true有权限
*/
public void onPermissionStatue(int deviceIndex, boolean state);
}
```
## Device
Device 是封装了设备提供的接口
### Methods
`CallBack` `setCallBack` `close` `getIndex` `isConnect` `getSerialNumber` `setIntegrationTime` `setAverageTimes` `setTriggerMode` `getSpectrum` `getResetSpectrum` `getWavelength` `getWaveNumber` `setCCDTECOn` `setCCDTECOff` `setCCDTECTemperature` `setLampEnable` `setLampDelayTime` `setLampWidth` `setLampInterval` `getLampDelayTime` `getLampWidth` `getLampInterval` `setExcitedWaveLength` `setLaserPower` `setLaserOn` `setLaserOff` `setLaserPowerOn` `setLaserPowerOff` `setTECOn` `setTECOff` `setMainBoardOn` `setMainBoardOff` `isMainBoardOpen` `isTECOpen` `waveletSmooth` `removeFluorescence` `lineInterpolation` `findPeaks` `getSlit` `getBadPoints` `getYJCorrectCoefficient` `getYAxisCorrectCoefficient` `getCorrectForDetectorNonlinear` `getWavelengthCalibrationCoefficients` `getLaserPowerCorrectCoefficient` `getFpgaVersion` `setYJCorrectCoefficient` `setLaserPowerCorrectCoefficient`
#### CallBack
```java
/**
* 设备状态回调接口
*/
public interface CallBack{
/**
* 设备状态回调
* @param state 设备状态
* true连接成功
* false连接失败
*/
void onDeviceStatue(boolean state);
}
```
#### setCallBack
```java
/**
* 设置回调函数
* @param callBack 回调
*/
public void setCallBack(CallBack callBack);
```
#### close
```java
/**
* 关闭设备
*/
public void close();
```
#### getIndex
```java
/**
* 设备编号
* @return 编号
*/
public int getIndex();
```
#### isConnect
```java
/**
* 设备是否连接成功
*/
public boolean isConnect();
```
#### getSerialNumber
```java
/**
* 获取设备序列号
* @return 设备序列号
*/
public String getSerialNumber();
```
#### setIntegrationTime
```java
/**
* 设置设备积分时间
* @param integrationTime 积分时间
* @return 是否设置成功
*/
public boolean setIntegrationTime(double integrationTime);
```
#### setAverageTimes
```java
/**
* 设置平均次数
* @param averageTimes 平均次数
*/
public void setAverageTimes(int averageTimes);
```
#### setTriggerMode
```java
/**
* 设置触发模式
* @param triggerMode 触发模式
* 0: Free Running
* 1: Hardware Level
* 2: Synchronous
* 3: Hardware Edge
* 4: Edge And Free
* @return 是否设置成功
*/
public boolean setTriggerMode(int triggerMode);
```
#### getSpectrum
```java
/**
* 获取光谱(读取当前光谱,非平均过后的光谱)
* @return 光谱数据
*/
public double[] getSpectrum();
```
#### getResetSpectrum
```java
/**
* 获取光谱(读取当前光谱,受用户积分时间)
* @return 光谱数据
*/
public double[] getResetSpectrum();
```
#### getWavelength
```java
/**
* 获取波长
* @return 波长数据
*/
public double[] getWavelength();
```
#### getWaveNumber
```java
/**
* 获取波数
* @return 波数数据
*/
public double[] getWaveNumber();
```
#### setCCDTECOn
```java
/**
* 开 CCD 制冷
* @return 是否关闭成功
*/
public boolean setCCDTECOn();
```
#### setCCDTECOff
```java
/**
* 关 CCD 制冷
* @return 是否关闭成功
*/
public boolean setCCDTECOff();
```
#### setCCDTECTemperature
```java
/**
* 设置制冷的目标温度 (-20~40)
* @return 是否设置成功
*/
public boolean setCCDTECTemperature(int temperature);
```
#### getCCDTECState
```java
/**
* 获取探测器制冷状态,返回 16 个字节:
* 第一个字节是 TEC 当前温度符号位0零上1零下单位
* 第二个字节是 TEC 当前温度整数位
* 第三个字节是 TEC 当前温度小数位
* 第四个字节是 PID 参数 P 高八位
* 第五个字节是 PID 参数 P 低八位
* 第六个字节是 PID 参数 I
* 第七个字节是 PID 参数 D
* 第八个字节是 TEC 电流符号位0正电流1负电流单位 mA
* 第九个字节是 TEC 电流高八位
* 第十个字节是 TEC 电流低八位
* 第十一个字节是报警是否位0 表示没有1 表示报警2 表示 TEC 没有)
* 第十二个字节 TEC 目标温度符号位0零上1零下单位
* 第十三个字节 TEC 目标温度整数位
* 第十四个字节 LD 电流高八位(目前是 PD 管放大器的电压)
* 第十五个字节 LD 电流低八位
* 第十六个字节上电状态0 表示没上电1 表示上电)
* @return 当前状态信息
*/
public byte[] getCCDTECState();
```
#### setLampEnable
```java
/**
* Lamp 控制
* @param enable 控制 truefalse
* @return 是否设置成功
*/
public boolean setLampEnable(boolean enable);
```
#### setLampDelayTime
```java
/**
* 设置灯泡的延迟时间(微秒)
* @param delayTime 延迟时间
* @return 是否设置成功
*/
public boolean setLampDelayTime(double delayTime);
```
#### setLampWidth
```java
/**
* 设置灯泡脉宽(微秒)
* @param width 脉宽
* @return 是否设置成功
*/
public boolean setLampWidth(double width);
```
#### setLampInterval
```java
/**
* 设置灯泡的周期(微秒)
* @param interval 周期
* @return 是否设置成功
*/
public boolean setLampInterval(double interval);
```
#### getLampDelayTime
```java
/**
* 获取延迟时间(微秒)
* @return 延迟时间
*/
public double getLampDelayTime();
```
#### getLampWidth
```java
/**
* 获取灯泡脉宽(微秒)
* @return 灯泡脉宽
*/
public double getLampWidth();
```
#### getLampInterval
```java
/**
* 获取灯泡的周期(微秒)
* @return 灯泡周期
*/
public double getLampInterval();
```
#### setExcitedWaveLength
```java
/**
* 设置激发波长
* @param excitedWaveLength 激发波长
* @return 是否设置成功
*/
public boolean setExcitedWaveLength(int excitedWaveLength);
```
#### setLaserPower
```java
/**
* 设置激光功率
* @param power 激光功率
* @return 是否设置成功
*/
public boolean setLaserPower(int power);
```
#### setLaserOn
```java
/**
* 开激光
* @return 是否设置成功
*/
public boolean setLaserOn();
```
#### setLaserOff
```java
/**
* 关激光
* @return 是否设置成功
*/
public boolean setLaserOff();
```
#### setLaserPowerOn
```java
/**
* 开激光器电源
* @return 是否设置成功
*/
public boolean setLaserPowerOn();
```
#### setLaserPowerOff
```java
/**
* 关激光器电源
* @return 是否设置成功
*/
public boolean setLaserPowerOff();
```
#### setTECOn
```java
/**
* 开 TEC
* @return 是否设置成功
*/
public boolean setTECOn();
```
#### setTECOff
```java
/**
* 关 TEC
* @return 是否设置成功
*/
public boolean setTECOff();
```
#### setMainBoardOn
```java
/**
* 开主板
* @return 是否设置成功
*/
public double setMainBoardOn();
```
#### setMainBoardOff
```java
/**
* 关主板
* @return 是否设置成功
*/
public double setMainBoardOff();
```
#### isMainBoardOpen
```java
/**
* 主板是否开成功,在最后一次操作时判断
* @return 是否开成功
*/
public boolean isMainBoardOpen();
```
#### isTECOpen
```java
/**
* 获取 TEC 状态true已开
* @return TEC 状态
*/
public boolean isTECOpen();
```
#### waveletSmooth
```java
/**
* 小波平滑
* @param spectrum 光谱
* @return 平滑后的光谱
*/
public double[] waveletSmooth(double[] spectrum);
```
#### removeFluorescence
```java
/**
* 去荧光
* @param spectrum 光谱
* @return 去荧光后的光谱
*/
public double[] removeFluorescence(double[] spectrum, int side);
```
#### lineInterpolation
```java
/**
* 线性插值
* @param xDataRaw 原 X 轴数据
* @param yDataRaw 原 Y 轴数据
* @param xData 插值后的 X 轴数据
* @return yData 插值后的 Y 轴数据
*/
public double[] lineInterpolation(double[] xDataRaw, double[] yDataRaw, double[] xData);
```
#### findPeaks
```java
/**
* 寻峰
* @param spectrum 光谱
* @param minIndicesBetweenPeaks 最小峰间距(值为 2实际可能需要 5
* @param baseline 基线
* @return 返回找到的峰的位置
*/
public static int[] findPeaks(double[] spectrum, int minIndicesBetweenPeaks, double baseline);
```
#### getSlit
```java
/**
* 获取狭缝
* @return 狭缝大小
*/
public String getSlit();
```
#### getBadPoints
```java
/**
* 获取坏点
* @return 坏点
*/
public int[] getBadPoints();
```
#### getYJCorrectCoefficient
```java
/**
* 获取波长校准系数
* @return 波长校准系数
*/
public double[] getYJCorrectCoefficient();
```
#### getYAxisCorrectCoefficient
```java
/**
* 获取 Y 轴校准系数
* @return Y 轴系数
*/
public double[] getYAxisCorrectCoefficient();
```
#### getCorrectForDetectorNonlinear
```java
/**
* 获取探测器非线性校正系数
* @return 探测器非线性校正系数
*/
public double[] getCorrectForDetectorNonlinear();
```
#### getWavelengthCalibrationCoefficients
```java
/**
* 获取波长校准系数
* @return 波长校准系数
*/
public double[] getWavelengthCalibrationCoefficients();
```
#### getLaserPowerCorrectCoefficient
```java
/**
* 获取功率校准系数
* @return 功率校准系数
*/
public double[] getLaserPowerCorrectCoefficient();
```
#### getFpgaVersion
```java
/**
* 获取 FPGA 版本
* @return FPGA 版本
*/
public double[] getFpgaVersion();
```
#### setYJCorrectCoefficient
```java
/**
* 设置波长校准系数
* @param coffes 波长校准系数
* @return 是否设置成功
*/
public boolean setYJCorrectCoefficient(double[] coffes);
```
#### setLaserPowerCorrectCoefficient
```java
/**
* 设置功率校准系数
* @param calibrationCoefficient 功率校准系数格式16 位字符串4 个数据,每个数据 4 位,如 0070030008001400
*/
public void setLaserPowerCorrectCoefficient(String calibrationCoefficient);
```

43
example/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

16
example/README.md Normal file
View File

@@ -0,0 +1,16 @@
# terra_example
Demonstrates how to use the terra plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
example/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,58 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
if (flutterVersionCode == null) {
flutterVersionCode = "1"
}
def flutterVersionName = localProperties.getProperty("flutter.versionName")
if (flutterVersionName == null) {
flutterVersionName = "1.0"
}
android {
namespace = "com.example.terra_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.terra_example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdk = 29
targetSdk = flutter.targetSdkVersion
versionCode = flutterVersionCode.toInteger()
versionName = flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="terra_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,6 @@
package com.example.terra_example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"

View File

@@ -0,0 +1,25 @@
// This is a basic Flutter integration test.
//
// Since integration tests run in a full Flutter application, they can interact
// with the host side of a plugin implementation, unlike Dart unit tests.
//
// For more information about Flutter integration tests, please see
// https://docs.flutter.dev/cookbook/testing/integration/introduction
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:terra/terra.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
final Terra plugin = Terra();
final String? version = await plugin.getPlatformVersion();
// The version string depends on the host platform running the test, so
// just assert that some non-empty string is returned.
expect(version?.isNotEmpty, true);
});
}

130
example/lib/main.dart Normal file
View File

@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:terra/terra.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
final _terraPlugin = Terra();
@override
void initState() {
super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await _terraPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Column(
children: [
Text('Running on: $_platformVersion\n'),
ElevatedButton(
onPressed: () async {
var deviceCount = await Terra.openAllDevices();
print("设备数量:$deviceCount");
},
child: const Text("连接设备")
),
ElevatedButton(
onPressed: () async {
var closeResult = await Terra.closeAllDevices();
print("关闭设备结果:$closeResult");
},
child: const Text("关闭所有设备")
),
ElevatedButton(
onPressed: () async {
var sn = await Terra.getDeviceSn();
print("设备SN$sn");
},
child: const Text("获取设备SN")
),
ElevatedButton(
onPressed: () async {
var res = await Terra.setIntegrationTime(10.00);
print("设置积分时间结果:$res");
},
child: const Text("设置积分时间")
),
ElevatedButton(
onPressed: () async {
var res = await Terra.setAverageTimes(3);
print("设置平均次数结果:$res");
},
child: const Text("设置平均次数")
),
ElevatedButton(
onPressed: () async {
var res = await Terra.setTriggerMode(0);
print("设置触发模式:$res");
},
child: const Text("设置触发模式")
),
ElevatedButton(
onPressed: () async {
var res = await Terra.isConnect();
print("是否连接:$res");
},
child: const Text("是否连接")
),
ElevatedButton(
onPressed: () async {
var spectrum = await Terra.getRamanSpectrum();
print("光谱:$spectrum");
},
child: const Text("获取光谱(处理后的光谱)")
),
ElevatedButton(
onPressed: () async {
var registerRes = await Terra.ramanMatchRegister();
print("注册结果:$registerRes");
},
child: const Text("算法注册")
),
],
),
),
),
);
}
}

283
example/pubspec.lock Normal file
View File

@@ -0,0 +1,283 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.9"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_driver:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
integration_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
process:
dependency: transitive
description:
name: process
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.5"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
sync_http:
dependency: transitive
description:
name: sync_http
sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
terra:
dependency: "direct main"
description:
path: ".."
relative: true
source: path
version: "0.0.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.10"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
url: "https://pub.flutter-io.cn"
source: hosted
version: "15.1.0"
webdriver:
dependency: transitive
description:
name: webdriver
sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

85
example/pubspec.yaml Normal file
View File

@@ -0,0 +1,85 @@
name: terra_example
description: "Demonstrates how to use the terra plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: '>=3.4.3 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
terra:
# When depending on this package from a real application you should use:
# terra: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

View File

@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:terra_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}

390
lib/terra.dart Normal file
View File

@@ -0,0 +1,390 @@
import 'package:flutter/services.dart';
import 'terra_platform_interface.dart';
class Terra {
Future<String?> getPlatformVersion() {
return TerraPlatform.instance.getPlatformVersion();
}
static const MethodChannel _channel = MethodChannel('terra');
// ==================== 设备管理 ====================
/// 打开所有设备
static Future<int> openAllDevices() async {
var deviceCount = await _channel.invokeMethod("openAllDevices");
return deviceCount;
}
/// 关闭所有设备
static Future<bool> closeAllDevices() async {
return await _channel.invokeMethod("closeAllDevices");
}
/// 关闭当前设备
static Future<bool> closeDevice() async {
return await _channel.invokeMethod("close");
}
/// 获取设备SN
static Future<String> getDeviceSn() async {
return await _channel.invokeMethod("getDeviceSn");
}
/// 获取设备索引
static Future<int> getDeviceIndex() async {
return await _channel.invokeMethod("getIndex");
}
/// 设备是否连接成功
static Future<bool> isConnect() async {
return await _channel.invokeMethod("isConnect");
}
/// 开启 SDK 调试日志
static Future<bool> openDebug() async {
return await _channel.invokeMethod("openDebug");
}
/// 检查 USB 权限
static Future<Map<String, dynamic>> checkUsbPermission() async {
final result = await _channel.invokeMethod("checkUsbPermission");
return Map<String, dynamic>.from(result as Map);
}
// ==================== 参数配置 ====================
/// 设置积分时间
static Future<bool> setIntegrationTime(double integrationTime) async {
Map<String, dynamic> args = {"integrationTime": integrationTime};
return await _channel.invokeMethod("setIntegrationTime", args);
}
/// 设置平均次数
static Future<bool> setAverageTimes(int averageTimes) async {
Map<String, dynamic> args = {"averageTimes": averageTimes};
return await _channel.invokeMethod("setAverageTimes", args);
}
/// 设置触发模式
/// 0: Free Running
/// 1: Hardware Level
/// 2: Synchronous
/// 3: Hardware Edge
/// 4: Edge And Free
static Future<bool> setTriggerMode(int triggerMode) async {
Map<String, dynamic> args = {"triggerMode": triggerMode};
return await _channel.invokeMethod("setTriggerMode", args);
}
// ==================== 光谱数据获取 ====================
/// 获取光谱(获取当前光谱,可能拿到缓存光谱)
static Future<List<double>> getSpectrum() async {
return await _channel.invokeMethod("getSpectrum");
}
/// 获取光谱(获取光谱前会重新设置积分时间)
static Future<List<double>> getResetSpectrum() async {
return await _channel.invokeMethod("getResetSpectrum");
}
/// 获取波长
static Future<List<double>> getWavelength() async {
return await _channel.invokeMethod("getWavelength");
}
/// 获取波数
static Future<List<double>> getWaveNumber() async {
return await _channel.invokeMethod("getWaveNumber");
}
/// 获取处理完的光谱
static Future<List<double>> getRamanSpectrum() async {
List<double> spectrum = await _channel.invokeMethod("getRamanSpectrum");
return spectrum;
}
/// 获取插值 X 轴数据200~3200步长 2
static Future<List<double>> getXvalue() async {
return await _channel.invokeMethod("getXvalue");
}
// ==================== CCD 制冷控制 ====================
/// 开 CCD 制冷
static Future<bool> setCCDTECOn() async {
return await _channel.invokeMethod("setCCDTECOn");
}
/// 关 CCD 制冷
static Future<bool> setCCDTECOff() async {
return await _channel.invokeMethod("setCCDTECOff");
}
/// 设置 CCD 制冷目标温度(-20 ~ 40
static Future<bool> setCCDTECTemperature(int temperature) async {
Map<String, dynamic> args = {"temperature": temperature};
return await _channel.invokeMethod("setCCDTECTemperature", args);
}
/// 获取 CCD 制冷状态(返回 16 字节状态数组)
static Future<List<int>> getCCDTECState() async {
final result = await _channel.invokeMethod("getCCDTECState");
return List<int>.from(result as List);
}
// ==================== 灯泡/脉冲控制 ====================
/// 设置灯泡延迟时间(微秒)
static Future<bool> setLampDelayTime(double delayTime) async {
return await _channel.invokeMethod("setLampDelayTime", {"delayTime": delayTime});
}
/// 设置灯泡脉宽(微秒)
static Future<bool> setLampWidth(double width) async {
return await _channel.invokeMethod("setLampWidth", {"width": width});
}
/// 设置灯泡周期(微秒)
static Future<bool> setLampInterval(double interval) async {
return await _channel.invokeMethod("setLampInterval", {"interval": interval});
}
/// 获取灯泡延迟时间(微秒)
static Future<List<double>> getLampDelayTime() async {
final result = await _channel.invokeMethod("getLampDelayTime");
return List<double>.from(result as List);
}
/// 获取灯泡脉宽(微秒)
static Future<List<double>> getLampWidth() async {
final result = await _channel.invokeMethod("getLampWidth");
return List<double>.from(result as List);
}
/// 获取灯泡周期(微秒)
static Future<List<double>> getLampInterval() async {
final result = await _channel.invokeMethod("getLampInterval");
return List<double>.from(result as List);
}
/// 开关灯泡控制
static Future<bool> setLampEnable(bool enable) async {
return await _channel.invokeMethod("setLampEnable", {"enable": enable});
}
// ==================== 激光控制 ====================
/// 设置激发波长
static Future<bool> setExcitedWaveLength(int excitedWaveLength) async {
return await _channel.invokeMethod("setExcitedWaveLength", {"excitedWaveLength": excitedWaveLength});
}
/// 设置激光功率(范围 0-2000
static Future<bool> setLaserPower(double power) async {
if (power < 0 || power > 2000) {
print('[Terra] setLaserPower 参数超出范围 (0-2000): $power,拒绝执行');
return false;
}
Map<String, dynamic> args = {"power": power};
print('[Terra] setLaserPower 发送: power=$power');
return await _channel.invokeMethod("setLaserPower", args)
.timeout(const Duration(seconds: 5), onTimeout: () {
print('[Terra] setLaserPower 超时,设备可能未正确响应');
return false;
});
}
/// 开激光
static Future<bool> setLaserOn() async {
return await _channel.invokeMethod("setLaserOn");
}
/// 关激光
static Future<bool> setLaserOff() async {
return await _channel.invokeMethod("setLaserOff");
}
/// 开激光器电源
static Future<bool> setLaserPowerOn() async {
return await _channel.invokeMethod("setLaserPowerOn");
}
/// 关激光器电源
static Future<bool> setLaserPowerOff() async {
return await _channel.invokeMethod("setLaserPowerOff");
}
// ==================== 硬件开关 ====================
/// 开 TEC
static Future<bool> setTECOn() async {
return await _channel.invokeMethod("setTECOn");
}
/// 关 TEC
static Future<bool> setTECOff() async {
return await _channel.invokeMethod("setTECOff");
}
/// 开主板
static Future<bool> setMainBoardOn() async {
return await _channel.invokeMethod("setMainBoardOn");
}
/// 关主板
static Future<bool> setMainBoardOff() async {
return await _channel.invokeMethod("setMainBoardOff");
}
/// 主板是否已打开
static Future<bool> isMainBoardOpen() async {
return await _channel.invokeMethod("isMainBoardOpen");
}
/// TEC 是否已打开
static Future<bool> isTECOpen() async {
return await _channel.invokeMethod("isTECOpen");
}
// ==================== 光谱数据处理 ====================
/// 小波平滑
static Future<List<double>> waveletSmooth(List<double> spectrum) async {
return await _channel.invokeMethod("waveletSmooth", {"spectrum": spectrum});
}
/// 消荧光side 为消荧光因子,默认 15
static Future<List<double>> removeFluorescence(List<double> spectrum, {int side = 15}) async {
return await _channel.invokeMethod("removeFluorescence", {"spectrum": spectrum, "side": side});
}
/// 线性插值
static Future<List<double>> lineInterpolation(
List<double> xDataRaw,
List<double> yDataRaw,
List<double> xData,
) async {
return await _channel.invokeMethod("lineInterpolation", {
"xDataRaw": xDataRaw,
"yDataRaw": yDataRaw,
"xData": xData,
});
}
/// 寻峰
static Future<List<int>> findPeaks(
List<double> spectrum,
int minIndicesBetweenPeaks,
double baseline,
) async {
Map<String, dynamic> args = {
"spectrum": spectrum,
"minIndicesBetweenPeaks": minIndicesBetweenPeaks,
"baseline": baseline,
};
return await _channel.invokeMethod("findPeaks", args);
}
// ==================== 校准系数 ====================
/// 获取激光校正系数
static Future<List<double>> getLaserPowerCorrectCoefficient() async {
return await _channel.invokeMethod("getLaserPowerCorrectCoefficient");
}
/// 设置激光校正系数
/// calibrationCoefficient 格式16 位字符串4 个数据,每个数据 4 位
/// 如 "0070030008001400"
static Future<bool> setLaserPowerCorrectCoefficient(String calibrationCoefficient) async {
Map<String, String> args = {"calibrationCoefficient": calibrationCoefficient};
return await _channel.invokeMethod("setLaserPowerCorrectCoefficient", args);
}
/// 获取乙腈校准数据
static Future<List<double>> getYJCorrectCoefficient() async {
return await _channel.invokeMethod("getYJCorrectCoefficient");
}
/// 设置乙腈定标系数
static Future<bool> setYJCorrectCoefficient(List<double> coffes) async {
Map<String, List<double>> args = {"coffes": coffes};
return await _channel.invokeMethod("setYJCorrectCoefficient", args);
}
/// 获取 Y 轴校准系数
static Future<List<double>> getYAxisCorrectCoefficient() async {
final result = await _channel.invokeMethod("getYAxisCorrectCoefficient");
return List<double>.from(result as List);
}
/// 获取探测器非线性校正系数
static Future<List<double>> getCorrectForDetectorNonlinear() async {
final result = await _channel.invokeMethod("getCorrectForDetectorNonlinear");
return List<double>.from(result as List);
}
/// 获取波长校准系数
static Future<List<double>> getWavelengthCalibrationCoefficients() async {
final result = await _channel.invokeMethod("getWavelengthCalibrationCoefficients");
return List<double>.from(result as List);
}
/// 获取 FPGA 版本
static Future<List<double>> getFpgaVersion() async {
final result = await _channel.invokeMethod("getFpgaVersion");
return List<double>.from(result as List);
}
/// 获取狭缝大小
static Future<String> getSlit() async {
return await _channel.invokeMethod("getSlit");
}
/// 获取坏点
static Future<List<int>> getBadPoints() async {
final result = await _channel.invokeMethod("getBadPoints");
return List<int>.from(result as List);
}
// ==================== 算法匹配与校准 ====================
/// 算法注册
static Future<bool> ramanMatchRegister() async {
return await _channel.invokeMethod("ramanMatchRegister");
}
/// 光谱匹配,返回相似度
static Future<double> ramanMatchCalcSimilarity(
List<double> spectrum,
List<double> libSpectrumList,
) async {
Map<String, List<double>> args = {
"spectrum": spectrum,
"libSpectrumList": libSpectrumList,
};
return await _channel.invokeMethod("ramanMatchCalcSimilarity", args);
}
/// 校准
static Future<List<double>> calibration(
List<double> spectrum,
List<double> YJCoeff,
) async {
Map<String, List<double>> args = {
"spectrum": spectrum,
"YJCoeff": YJCoeff,
};
return await _channel.invokeMethod("calibration", args);
}
// ==================== 其他 ====================
/// SN 码 MD5 加密
static Future<String> bytesToMD5(String sn) async {
Map<String, String> args = {"sn": sn};
return await _channel.invokeMethod("bytesToMD5", args);
}
}

View File

@@ -0,0 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'terra_platform_interface.dart';
/// An implementation of [TerraPlatform] that uses method channels.
class MethodChannelTerra extends TerraPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('terra');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
}

View File

@@ -0,0 +1,29 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'terra_method_channel.dart';
abstract class TerraPlatform extends PlatformInterface {
/// Constructs a TerraPlatform.
TerraPlatform() : super(token: _token);
static final Object _token = Object();
static TerraPlatform _instance = MethodChannelTerra();
/// The default instance of [TerraPlatform] to use.
///
/// Defaults to [MethodChannelTerra].
static TerraPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [TerraPlatform] when
/// they register themselves.
static set instance(TerraPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}

70
pubspec.yaml Normal file
View File

@@ -0,0 +1,70 @@
name: terra
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: '>=3.4.3 <4.0.0'
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.terra
pluginClass: TerraPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

View File

@@ -0,0 +1,27 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:terra/terra_method_channel.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
MethodChannelTerra platform = MethodChannelTerra();
const MethodChannel channel = MethodChannel('terra');
setUp(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
channel,
(MethodCall methodCall) async {
return '42';
},
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
});
test('getPlatformVersion', () async {
expect(await platform.getPlatformVersion(), '42');
});
}

29
test/terra_test.dart Normal file
View File

@@ -0,0 +1,29 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:terra/terra.dart';
import 'package:terra/terra_platform_interface.dart';
import 'package:terra/terra_method_channel.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
class MockTerraPlatform
with MockPlatformInterfaceMixin
implements TerraPlatform {
@override
Future<String?> getPlatformVersion() => Future.value('42');
}
void main() {
final TerraPlatform initialPlatform = TerraPlatform.instance;
test('$MethodChannelTerra is the default instance', () {
expect(initialPlatform, isInstanceOf<MethodChannelTerra>());
});
test('getPlatformVersion', () async {
Terra terraPlugin = Terra();
MockTerraPlatform fakePlatform = MockTerraPlatform();
TerraPlatform.instance = fakePlatform;
expect(await terraPlugin.getPlatformVersion(), '42');
});
}