feat: 添加示波器前后端

This commit is contained in:
2025-07-18 21:49:37 +08:00
parent ba79a2093b
commit e4a1c34a6c
10 changed files with 1743 additions and 91 deletions

View File

@@ -0,0 +1,264 @@
import { createInjectionState } from "@vueuse/core";
import { shallowRef, reactive, ref, computed } from "vue";
import { Mutex } from "async-mutex";
import {
OscilloscopeFullConfig,
OscilloscopeDataResponse,
} from "@/APIClient";
import { AuthManager } from "@/utils/AuthManager";
import { useAlertStore } from "@/components/Alert";
import { useRequiredInjection } from "@/utils/Common";
export type OscilloscopeDataType = {
x: number[];
y: number[] | number[][];
xUnit: "s" | "ms" | "us" | "ns";
yUnit: "V" | "mV" | "uV";
adFrequency: number;
adVpp: number;
adMax: number;
adMin: number;
};
// 默认配置
const DEFAULT_CONFIG: OscilloscopeFullConfig = new OscilloscopeFullConfig({
captureEnabled: false,
triggerLevel: 128,
triggerRisingEdge: true,
horizontalShift: 0,
decimationRate: 0,
autoRefreshRAM: false,
});
// 采样频率常量(后端返回)
const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(() => {
const oscData = shallowRef<OscilloscopeDataType>();
const alert = useRequiredInjection(useAlertStore);
// 互斥锁
const operationMutex = new Mutex();
// 状态
const isApplying = ref(false);
const isCapturing = ref(false);
// 配置
const config = reactive<OscilloscopeFullConfig>(new OscilloscopeFullConfig({ ...DEFAULT_CONFIG }));
// 采样点数(由后端数据决定)
const sampleCount = ref(0);
// 采样周期ns由adFrequency计算
const samplePeriodNs = computed(() =>
oscData.value?.adFrequency ? 1_000_000_000 / oscData.value.adFrequency : 200
);
// 应用配置
const applyConfiguration = async () => {
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
const release = await operationMutex.acquire();
isApplying.value = true;
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const success = await client.initialize({ ...config });
if (success) {
alert.success("示波器配置已应用", 2000);
} else {
throw new Error("应用失败");
}
} catch (error) {
alert.error("应用配置失败", 3000);
} finally {
isApplying.value = false;
release();
}
};
// 重置配置
const resetConfiguration = () => {
Object.assign(config, { ...DEFAULT_CONFIG });
alert.info("配置已重置", 2000);
};
// 获取数据
const getOscilloscopeData = async () => {
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const resp: OscilloscopeDataResponse = await client.getData();
// 解析波形数据
const binaryString = atob(resp.waveformData);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
sampleCount.value = bytes.length;
// 构建时间轴
const x = Array.from(
{ length: bytes.length },
(_, i) => (i * samplePeriodNs.value) / 1000 // us
);
const y = Array.from(bytes);
oscData.value = {
x,
y,
xUnit: "us",
yUnit: "V",
adFrequency: resp.adFrequency,
adVpp: resp.adVpp,
adMax: resp.adMax,
adMin: resp.adMin,
};
} catch (error) {
alert.error("获取示波器数据失败", 3000);
}
};
// 启动捕获
const startCapture = async () => {
if (operationMutex.isLocked()) {
alert.warn("有其他操作正在进行中,请稍后再试", 3000);
return;
}
isCapturing.value = true;
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const started = await client.startCapture();
if (!started) throw new Error("无法启动捕获");
alert.info("开始捕获...", 2000);
// 简单轮询,直到捕获完成(可根据后端实际情况优化)
await new Promise((resolve) => setTimeout(resolve, 1000));
await getOscilloscopeData();
alert.success("捕获完成", 2000);
} catch (error) {
alert.error("捕获失败", 3000);
} finally {
isCapturing.value = false;
release();
}
};
// 停止捕获
const stopCapture = async () => {
if (!isCapturing.value) {
alert.warn("当前没有正在进行的捕获操作", 2000);
return;
}
isCapturing.value = false;
const release = await operationMutex.acquire();
try {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
const stopped = await client.stopCapture();
if (!stopped) throw new Error("无法停止捕获");
alert.info("捕获已停止", 2000);
} catch (error) {
alert.error("停止捕获失败", 3000);
} finally {
release();
}
};
// 更新触发参数
const updateTrigger = async (level: number, risingEdge: boolean) => {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
try {
const ok = await client.updateTrigger(level, risingEdge);
if (ok) {
config.triggerLevel = level;
config.triggerRisingEdge = risingEdge;
alert.success("触发参数已更新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("更新触发参数失败", 2000);
}
};
// 更新采样参数
const updateSampling = async (horizontalShift: number, decimationRate: number) => {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
try {
const ok = await client.updateSampling(horizontalShift, decimationRate);
if (ok) {
config.horizontalShift = horizontalShift;
config.decimationRate = decimationRate;
alert.success("采样参数已更新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("更新采样参数失败", 2000);
}
};
// 手动刷新RAM
const refreshRAM = async () => {
const client = AuthManager.createAuthenticatedOscilloscopeApiClient();
try {
const ok = await client.refreshRAM();
if (ok) {
alert.success("RAM已刷新", 2000);
} else {
throw new Error();
}
} catch {
alert.error("刷新RAM失败", 2000);
}
};
// 生成测试数据
const generateTestData = () => {
const freq = 5_000_000;
const duration = 0.001; // 1ms
const points = Math.floor(freq * duration);
const x = Array.from({ length: points }, (_, i) => (i * 1_000_000_000 / freq) / 1000);
const y = Array.from({ length: points }, (_, i) =>
Math.floor(Math.sin(i * 0.01) * 127 + 128)
);
oscData.value = {
x,
y,
xUnit: "us",
yUnit: "V",
adFrequency: freq,
adVpp: 2.0,
adMax: 255,
adMin: 0,
};
alert.success("测试数据生成成功", 2000);
};
const isOperationInProgress = computed(
() => isApplying.value || isCapturing.value || operationMutex.isLocked()
);
return {
oscData,
config,
isApplying,
isCapturing,
isOperationInProgress,
sampleCount,
samplePeriodNs,
applyConfiguration,
resetConfiguration,
getOscilloscopeData,
startCapture,
stopCapture,
updateTrigger,
updateSampling,
refreshRAM,
generateTestData,
};
});
export { useProvideOscilloscope, useOscilloscopeState, DEFAULT_CONFIG };

View File

@@ -1,6 +1,6 @@
<template>
<div class="w-full h-100">
<v-chart v-if="true" class="w-full h-full" :option="option" autoresize />
<v-chart v-if="hasData" class="w-full h-full" :option="option" autoresize />
<div
v-else
class="w-full h-full flex items-center justify-center text-gray-500"
@@ -11,16 +11,15 @@
</template>
<script setup lang="ts">
import { computed, withDefaults } from "vue";
import { computed } from "vue";
import { forEach } from "lodash";
import VChart from "vue-echarts";
import { type WaveformDataType } from "./index";
import { useOscilloscopeState } from "./OscilloscopeManager";
// Echarts
import { use } from "echarts/core";
import { LineChart } from "echarts/charts";
import {
TitleComponent,
TooltipComponent,
LegendComponent,
ToolboxComponent,
@@ -37,6 +36,7 @@ import type {
DataZoomComponentOption,
GridComponentOption,
} from "echarts/components";
import { useRequiredInjection } from "@/utils/Common";
use([
TooltipComponent,
@@ -57,44 +57,47 @@ type EChartsOption = ComposeOption<
| LineSeriesOption
>;
const props = withDefaults(
defineProps<{
data?: WaveformDataType;
}>(),
{
data: () => ({
x: [],
y: [],
xUnit: "s",
yUnit: "V",
}),
},
);
// 使 manager oscilloscope
const { oscData } = useRequiredInjection(useOscilloscopeState);
const hasData = computed(() => {
return (
props.data &&
props.data.x &&
props.data.y &&
props.data.x.length > 0 &&
props.data.y.length > 0 &&
props.data.y.some((channel) => channel.length > 0)
oscData.value &&
oscData.value.x &&
oscData.value.y &&
oscData.value.x.length > 0 &&
(
Array.isArray(oscData.value.y[0])
? oscData.value.y.some((channel: any) => channel.length > 0)
: oscData.value.y.length > 0
)
);
});
const option = computed((): EChartsOption => {
if (!oscData.value || !oscData.value.x || !oscData.value.y) {
return {};
}
const series: LineSeriesOption[] = [];
forEach(props.data.y, (yData, index) => {
// x y [x, y]
const seriesData = props.data.x.map((xValue, i) => [xValue, yData[i] || 0]);
// yChannels number[][]
const yChannels: number[][] = Array.isArray(oscData.value.y[0])
? (oscData.value.y as number[][])
: [oscData.value.y as number[]];
forEach(yChannels, (yData, index) => {
if (!oscData.value || !yData) return;
const seriesData = oscData.value.x.map((xValue, i) => [
xValue,
yData && yData[i] !== undefined ? yData[i] : 0,
]);
series.push({
type: "line",
name: `通道 ${index + 1}`,
data: seriesData,
smooth: false, //
symbol: "none", //
smooth: false,
symbol: "none",
lineStyle: {
width: 2,
},
@@ -111,9 +114,10 @@ const option = computed((): EChartsOption => {
tooltip: {
trigger: "axis",
formatter: (params: any) => {
let result = `时间: ${params[0].data[0].toFixed(2)} ${props.data.xUnit}<br/>`;
if (!oscData.value) return "";
let result = `时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}<br/>`;
params.forEach((param: any) => {
result += `${param.seriesName}: ${param.data[1].toFixed(3)} ${props.data.yUnit}<br/>`;
result += `${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}<br/>`;
});
return result;
},
@@ -141,7 +145,7 @@ const option = computed((): EChartsOption => {
],
xAxis: {
type: "value",
name: `时间 (${props.data.xUnit})`,
name: oscData.value ? `时间 (${oscData.value.xUnit})` : "时间",
nameLocation: "middle",
nameGap: 30,
axisLine: {
@@ -156,7 +160,7 @@ const option = computed((): EChartsOption => {
},
yAxis: {
type: "value",
name: `电压 (${props.data.yUnit})`,
name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
nameLocation: "middle",
nameGap: 40,
axisLine: {

View File

@@ -1,42 +1,3 @@
import WaveformDisplay from "./WaveformDisplay.vue";
import OscilloscopeWaveformDisplay from "./OscilloscopeWaveformDisplay.vue";
type WaveformDataType = {
x: number[];
y: number[][];
xUnit: "s" | "ms" | "us";
yUnit: "V" | "mV" | "uV";
};
// Test data generator
function generateTestData(): WaveformDataType {
const sampleRate = 1000; // 1kHz
const duration = 0.1; // 10ms
const points = Math.floor(sampleRate * duration);
const x = Array.from({ length: points }, (_, i) => (i / sampleRate) * 1000); // time in ms
// Generate multiple channels with different waveforms
const y = [
// Channel 1: Sine wave 50Hz
Array.from(
{ length: points },
(_, i) => Math.sin((2 * Math.PI * 50 * i) / sampleRate) * 3.3,
),
// Channel 2: Square wave 25Hz
Array.from(
{ length: points },
(_, i) => Math.sign(Math.sin((2 * Math.PI * 25 * i) / sampleRate)) * 5,
),
// Channel 3: Sawtooth wave 33Hz
Array.from(
{ length: points },
(_, i) => (2 * (((33 * i) / sampleRate) % 1) - 1) * 2.5,
),
// Channel 4: Noise + DC offset
Array.from({ length: points }, () => Math.random() * 0.5 + 1.5),
];
return { x, y, xUnit: "ms", yUnit: "V" };
}
export { WaveformDisplay, generateTestData , type WaveformDataType };
export { OscilloscopeWaveformDisplay };