feat: 增加示波器探测参数显示,增加旋转编码器按下的功能

This commit is contained in:
SikongJueluo 2025-08-22 04:05:00 +08:00
parent 7d3ef598de
commit 6302489f3a
14 changed files with 278 additions and 55 deletions

View File

@ -268,10 +268,10 @@ public class OscilloscopeApiController : ControllerBase
var response = new OscilloscopeDataResponse
{
ADFrequency = freqResult.Value,
ADVpp = vppResult.Value,
ADMax = maxResult.Value,
ADMin = minResult.Value,
AdFrequency = freqResult.Value,
AdVpp = vppResult.Value,
AdMax = maxResult.Value,
AdMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};

View File

@ -34,10 +34,10 @@ public interface IOscilloscopeReceiver
[TranspilationSource]
public class OscilloscopeDataResponse
{
public uint ADFrequency { get; set; }
public byte ADVpp { get; set; }
public byte ADMax { get; set; }
public byte ADMin { get; set; }
public uint AdFrequency { get; set; }
public byte AdVpp { get; set; }
public byte AdMax { get; set; }
public byte AdMin { get; set; }
public string WaveformData { get; set; } = "";
}
@ -275,19 +275,19 @@ public class OscilloscopeHub : Hub<IOscilloscopeReceiver>, IOscilloscopeHub
var response = new OscilloscopeDataResponse
{
ADFrequency = freqResult.Value,
ADVpp = vppResult.Value,
ADMax = maxResult.Value,
ADMin = minResult.Value,
AdFrequency = freqResult.Value,
AdVpp = vppResult.Value,
AdMax = maxResult.Value,
AdMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
return new OscilloscopeDataResponse
{
ADFrequency = freqResult.Value,
ADVpp = vppResult.Value,
ADMax = maxResult.Value,
ADMin = minResult.Value,
AdFrequency = freqResult.Value,
AdVpp = vppResult.Value,
AdMax = maxResult.Value,
AdMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
}

View File

@ -16,6 +16,7 @@ public interface IRotaryEncoderHub
{
Task<bool> SetEnable(bool enable);
Task<bool> RotateEncoderOnce(int num, RotaryEncoderDirection direction);
Task<bool> PressEncoderOnce(int num, RotaryEncoderPressStatus press);
Task<bool> EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq);
Task<bool> DisableCycleRotateEncoder();
}
@ -133,6 +134,30 @@ public class RotaryEncoderHub : Hub<IRotaryEncoderReceiver>, IRotaryEncoderHub
}
}
public async Task<bool> PressEncoderOnce(int num, RotaryEncoderPressStatus press)
{
try
{
if (num <= 0 || num > 4)
throw new ArgumentException($"RotaryEncoder num should be 1~3, instead of {num}");
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
var result = await encoderCtrl.PressEncoderOnce(num, press);
if (!result.IsSuccessful)
{
logger.Error(result.Error, $"RotateEncoderOnce({num}, {press}) failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to rotate encoder once");
return false;
}
}
public async Task<bool> EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq)
{
try

View File

@ -7,8 +7,10 @@ namespace Peripherals.RotaryEncoderClient;
class RotaryEncoderCtrlAddr
{
public const UInt32 BASE = 0xB0_00_00_30;
public const UInt32 PRESS_BASE = 0xB0_00_00_40;
public const UInt32 ENABLE = BASE;
public const UInt32 PRESS_ENABLE = PRESS_BASE;
}
[TranspilationSource]
@ -18,6 +20,13 @@ public enum RotaryEncoderDirection : uint
Clockwise = 1,
}
[TranspilationSource]
public enum RotaryEncoderPressStatus : uint
{
Press = 0,
Release = 1,
}
public class RotaryEncoderCtrl
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@ -45,10 +54,22 @@ public class RotaryEncoderCtrl
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value)
{
logger.Error($"Set Rotary Encoder Enable failed: {ret.Error}");
return false;
}
}
{
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.PRESS_ENABLE, enable ? 0x1U : 0x0U, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
}
}
public async ValueTask<Result<bool>> RotateEncoderOnce(int num, RotaryEncoderDirection direction)
@ -61,7 +82,23 @@ public class RotaryEncoderCtrl
this.ep, this.taskID, RotaryEncoderCtrlAddr.BASE + (UInt32)num, (UInt32)direction, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Set Rotary Encoder {num} {direction.ToString()} failed: {ret.Error}");
logger.Error($"Set Rotary Encoder Rotate {num} {direction.ToString()} failed: {ret.Error}");
return new(ret.Error);
}
return ret.Value;
}
public async ValueTask<Result<bool>> PressEncoderOnce(int num, RotaryEncoderPressStatus press)
{
if (MsgBus.IsRunning)
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
else return new(new Exception("Message Bus not work!"));
var ret = await UDPClientPool.WriteAddr(
this.ep, this.taskID, RotaryEncoderCtrlAddr.PRESS_BASE + (UInt32)num, (UInt32)press, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Set Rotary Encoder Set {num} {press.ToString()} failed: {ret.Error}");
return new(ret.Error);
}
return ret.Value;

View File

@ -199,10 +199,16 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
}
sampleCount.value = bytes.length;
const aDFrequency = resp.adFrequency;
// 计算采样周期ns
const samplePeriodNs =
aDFrequency > 0 ? 1_000_000_000 / aDFrequency : 200;
// 构建时间轴
const x = Array.from(
{ length: bytes.length },
(_, i) => (i * samplePeriodNs.value) / 1000, // us
(_, i) => (i * samplePeriodNs) / 1000, // us
);
const y = Array.from(bytes);
@ -211,11 +217,13 @@ const [useProvideOscilloscope, useOscilloscopeState] = createInjectionState(
y,
xUnit: "us",
yUnit: "V",
adFrequency: resp.aDFrequency,
adVpp: resp.aDVpp,
adMax: resp.aDMax,
adMin: resp.aDMin,
adFrequency: aDFrequency,
adVpp: resp.adVpp,
adMax: resp.adMax,
adMin: resp.adMin,
};
console.log("解析后的参数:", resp, oscData.value); // 添加调试日志
};
// 获取数据

View File

@ -88,6 +88,67 @@
<div class="w-2 h-2 bg-white rounded-full animate-pulse"></div>
采集中
</div>
<!-- 测量数据展示面板 -->
<div
v-if="hasData"
class="absolute top-4 left-4 bg-white/95 dark:bg-slate-800/95 backdrop-blur-sm rounded-lg shadow-lg border border-slate-200/50 dark:border-slate-700/50 p-3 min-w-[200px]"
>
<h4 class="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
<Activity class="w-4 h-4 text-blue-500" />
测量参数
</h4>
<div class="space-y-2 text-xs">
<!-- 采样频率 -->
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400">采样频率:</span>
<span class="font-mono font-semibold text-blue-600 dark:text-blue-400">
{{ formatFrequency(oscData?.adFrequency || 0) }}
</span>
</div>
<!-- 电压范围 -->
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400">Vpp:</span>
<span class="font-mono font-semibold text-emerald-600 dark:text-emerald-400">
{{ (oscData?.adVpp || 0).toFixed(2) }}V
</span>
</div>
<!-- 最大值 -->
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400">最大值:</span>
<span class="font-mono font-semibold text-orange-600 dark:text-orange-400">
{{ formatAdcValue(oscData?.adMax || 0) }}
</span>
</div>
<!-- 最小值 -->
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400">最小值:</span>
<span class="font-mono font-semibold text-purple-600 dark:text-purple-400">
{{ formatAdcValue(oscData?.adMin || 0) }}
</span>
</div>
<!-- 采样点数 -->
<div class="flex justify-between items-center pt-1 border-t border-slate-200 dark:border-slate-700">
<span class="text-slate-600 dark:text-slate-400">采样点:</span>
<span class="font-mono font-semibold text-slate-700 dark:text-slate-300">
{{ formatSampleCount(oscManager.sampleCount.value) }}
</span>
</div>
<!-- 采样周期 -->
<div class="flex justify-between items-center">
<span class="text-slate-600 dark:text-slate-400">周期:</span>
<span class="font-mono font-semibold text-slate-700 dark:text-slate-300">
{{ formatPeriod(oscManager.samplePeriodNs.value) }}
</span>
</div>
</div>
</div>
</div>
</template>
@ -156,6 +217,44 @@ const hasData = computed(() => {
);
});
//
const formatFrequency = (frequency: number): string => {
if (frequency >= 1_000_000) {
return `${(frequency / 1_000_000).toFixed(1)}MHz`;
} else if (frequency >= 1_000) {
return `${(frequency / 1_000).toFixed(1)}kHz`;
} else {
return `${frequency}Hz`;
}
};
// ADC
const formatAdcValue = (value: number): string => {
return `${value} (${((value / 255) * 3.3).toFixed(2)}V)`;
};
//
const formatSampleCount = (count: number): string => {
if (count >= 1_000_000) {
return `${(count / 1_000_000).toFixed(1)}M`;
} else if (count >= 1_000) {
return `${(count / 1_000).toFixed(1)}k`;
} else {
return `${count}`;
}
};
//
const formatPeriod = (periodNs: number): string => {
if (periodNs >= 1_000_000) {
return `${(periodNs / 1_000_000).toFixed(2)}ms`;
} else if (periodNs >= 1_000) {
return `${(periodNs / 1_000).toFixed(2)}μs`;
} else {
return `${periodNs.toFixed(2)}ns`;
}
};
const option = computed((): EChartsOption => {
if (!oscData.value || !oscData.value.x || !oscData.value.y) {
return {};
@ -235,7 +334,9 @@ const option = computed((): EChartsOption => {
if (!oscData.value) return "";
let result = `<div style="font-weight: 600; margin-bottom: 4px;">时间: ${params[0].data[0].toFixed(2)} ${oscData.value.xUnit}</div>`;
params.forEach((param: any) => {
result += `<div style="color: ${param.color};">● ${param.seriesName}: ${param.data[1].toFixed(3)} ${oscData.value?.yUnit ?? ""}</div>`;
const adcValue = param.data[1];
const voltage = ((adcValue / 255) * 3.3).toFixed(3);
result += `<div style="color: ${param.color};">● ${param.seriesName}: ${adcValue} (${voltage}V)</div>`;
});
return result;
},
@ -333,7 +434,7 @@ const option = computed((): EChartsOption => {
},
yAxis: {
type: "value",
name: oscData.value ? `电压 (${oscData.value.yUnit})` : "电压",
name: oscData.value ? `ADC值 (0-255)` : "ADC值",
nameLocation: "middle",
nameGap: 50,
nameTextStyle: {
@ -357,6 +458,9 @@ const option = computed((): EChartsOption => {
axisLabel: {
color: "#64748B",
fontSize: 11,
formatter: (value: number) => {
return `${value} (${((value / 255) * 3.3).toFixed(1)}V)`;
},
},
splitLine: {
show: true,
@ -536,6 +640,14 @@ button:active::after {
font-size: 12px;
padding: 4px 8px;
}
/* 移动端测量面板调整 */
.absolute.top-4.left-4 {
top: 8px;
left: 8px;
min-width: 180px;
font-size: 11px;
}
}
/* 平滑过渡效果 */
@ -548,4 +660,17 @@ button:focus-visible {
outline: 2px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
}
</style>
/* 测量面板样式增强 */
.absolute.top-4.left-4 {
backdrop-filter: blur(8px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease-in-out;
z-index: 10;
}
.absolute.top-4.left-4:hover {
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
</style>

View File

@ -176,7 +176,10 @@
<script lang="ts" setup>
import { useRotaryEncoder } from "@/stores/Peripherals/RotaryEncoder";
import { RotaryEncoderDirection } from "@/utils/signalR/Peripherals.RotaryEncoderClient";
import {
RotaryEncoderDirection,
RotaryEncoderPressStatus,
} from "@/utils/signalR/Peripherals.RotaryEncoderClient";
import { watch } from "vue";
import { watchEffect } from "vue";
import { ref, computed } from "vue";
@ -185,6 +188,7 @@ const rotataryEncoderStore = useRotaryEncoder();
interface Props {
size?: number;
componentId?: string;
enableDigitalTwin?: boolean;
encoderNumber?: number;
}
@ -236,8 +240,16 @@ function handleMouseUp() {
//
if (!drag.value.hasRotated) {
isPressed.value = true;
rotataryEncoderStore.pressOnce(
props.encoderNumber,
RotaryEncoderPressStatus.Press,
);
setTimeout(() => {
isPressed.value = false;
rotataryEncoderStore.pressOnce(
props.encoderNumber,
RotaryEncoderPressStatus.Release,
);
}, 100);
}
}
@ -252,7 +264,10 @@ function handlePress(pressed: boolean) {
}
watchEffect(() => {
rotataryEncoderStore.setEnable(props.enableDigitalTwin);
if (!props.enableDigitalTwin) return;
if (props.componentId)
rotataryEncoderStore.setEnable(props.enableDigitalTwin);
});
watch(

View File

@ -108,6 +108,7 @@ import { ref, computed, watch, onMounted } from "vue";
interface Props {
size?: number;
componentId?: string;
enableDigitalTwin?: boolean;
switchCount?: number;
initialValues?: string;
@ -191,13 +192,10 @@ function setBtnStatus(idx: number, isOn: boolean) {
}
// props
const isFirstEnableDigitalTwin = ref(true);
watch(
() => props.enableDigitalTwin,
(newVal) => {
if (isFirstEnableDigitalTwin.value) {
isFirstEnableDigitalTwin.value = false;
} else {
if (props.componentId) {
const client = getClient();
client.setEnable(newVal);
}
@ -205,16 +203,11 @@ watch(
{ immediate: true },
);
const isFirstUpdateStatus = ref(true);
watch(
() => [switchCount.value, props.initialValues],
() => {
btnStatus.value = parseInitialValues();
if (isFirstUpdateStatus.value) {
isFirstUpdateStatus.value = false;
} else {
updateStatus(btnStatus.value);
}
if (props.componentId) updateStatus(btnStatus.value);
},
);
</script>

View File

@ -1,5 +1,8 @@
import { AuthManager } from "@/utils/AuthManager";
import type { RotaryEncoderDirection } from "@/utils/signalR/Peripherals.RotaryEncoderClient";
import type {
RotaryEncoderDirection,
RotaryEncoderPressStatus,
} from "@/utils/signalR/Peripherals.RotaryEncoderClient";
import {
getHubProxyFactory,
getReceiverRegister,
@ -72,6 +75,11 @@ export const useRotaryEncoder = defineStore("RotaryEncoder", () => {
return await proxy.rotateEncoderOnce(num, direction);
}
async function pressOnce(num: number, pressStatus: RotaryEncoderPressStatus) {
const proxy = getHubProxy();
return await proxy.pressEncoderOnce(num, pressStatus);
}
async function enableCycleRotate(
num: number,
direction: RotaryEncoderDirection,
@ -89,6 +97,7 @@ export const useRotaryEncoder = defineStore("RotaryEncoder", () => {
return {
setEnable,
rotateOnce,
pressOnce,
enableCycleRotate,
disableCycleRotate,
};

View File

@ -8,3 +8,9 @@ export enum RotaryEncoderDirection {
Clockwise = 1,
}
/** Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderPressStatus */
export enum RotaryEncoderPressStatus {
Press = 0,
Release = 1,
}

View File

@ -5,7 +5,7 @@
import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr';
import type { IDigitalTubesHub, IJtagHub, IOscilloscopeHub, IProgressHub, IRotaryEncoderHub, IWS2812Hub, IDigitalTubesReceiver, IJtagReceiver, IOscilloscopeReceiver, IProgressReceiver, IRotaryEncoderReceiver, IWS2812Receiver } from './server.Hubs';
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
import type { RotaryEncoderDirection, RotaryEncoderPressStatus } from '../Peripherals.RotaryEncoderClient';
import type { RGBColor } from '../Peripherals.WS2812Client';
@ -270,6 +270,10 @@ class IRotaryEncoderHub_HubProxy implements IRotaryEncoderHub {
return await this.connection.invoke("RotateEncoderOnce", num, direction);
}
public readonly pressEncoderOnce = async (num: number, press: RotaryEncoderPressStatus): Promise<boolean> => {
return await this.connection.invoke("PressEncoderOnce", num, press);
}
public readonly enableCycleRotateEncoder = async (num: number, direction: RotaryEncoderDirection, freq: number): Promise<boolean> => {
return await this.connection.invoke("EnableCycleRotateEncoder", num, direction, freq);
}

View File

@ -4,7 +4,7 @@
// @ts-nocheck
import type { IStreamResult, Subject } from '@microsoft/signalr';
import type { DigitalTubeTaskStatus, OscilloscopeFullConfig, OscilloscopeDataResponse, ProgressInfo } from '../server.Hubs';
import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient';
import type { RotaryEncoderDirection, RotaryEncoderPressStatus } from '../Peripherals.RotaryEncoderClient';
import type { RGBColor } from '../Peripherals.WS2812Client';
export type IDigitalTubesHub = {
@ -116,6 +116,12 @@ export type IRotaryEncoderHub = {
rotateEncoderOnce(num: number, direction: RotaryEncoderDirection): Promise<boolean>;
/**
* @param num Transpiled from int
* @param press Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderPressStatus
* @returns Transpiled from System.Threading.Tasks.Task<bool>
*/
pressEncoderOnce(num: number, press: RotaryEncoderPressStatus): Promise<boolean>;
/**
* @param num Transpiled from int
* @param direction Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderDirection
* @param freq Transpiled from int
* @returns Transpiled from System.Threading.Tasks.Task<bool>

View File

@ -13,13 +13,13 @@ export type DigitalTubeTaskStatus = {
/** Transpiled from server.Hubs.OscilloscopeDataResponse */
export type OscilloscopeDataResponse = {
/** Transpiled from uint */
aDFrequency: number;
adFrequency: number;
/** Transpiled from byte */
aDVpp: number;
adVpp: number;
/** Transpiled from byte */
aDMax: number;
adMax: number;
/** Transpiled from byte */
aDMin: number;
adMin: number;
/** Transpiled from string */
waveformData: string;
}

View File

@ -182,7 +182,7 @@
<!-- MD文档 -->
<div class="space-y-2">
<label class="text-sm font-medium text-base-content"
>MD文档 (必需)</label
>MD文档 (可选)</label
>
<div
class="border-2 border-dashed border-base-300 rounded-lg p-6 text-center cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors aspect-square flex items-center justify-center"
@ -468,7 +468,7 @@ const canCreateExam = computed(() => {
editExamInfo.value.id.trim() !== "" &&
editExamInfo.value.name.trim() !== "" &&
editExamInfo.value.description.trim() !== "" &&
(uploadFiles.value.mdFile !== null || mode.value === "edit")
(mode.value === "edit")
);
});
@ -605,11 +605,6 @@ const submitCreateExam = async () => {
return;
}
if (!uploadFiles.value.mdFile) {
alert.error("请上传MD文档");
return;
}
isUpdating.value = true;
try {