feat: 修改后端apiclient生成逻辑

fix: 修复debugger获取flag失败的问题
refactor: 重新编写debugger前后端逻辑
This commit is contained in:
SikongJueluo 2025-07-30 15:31:11 +08:00
parent 6dfd275091
commit 3257a68407
No known key found for this signature in database
11 changed files with 4194 additions and 1733 deletions

280
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@types/lodash": "^4.17.16",
"@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0",
"axios": "^1.11.0",
"echarts": "^5.6.0",
"highlight.js": "^11.11.1",
"konva": "^9.3.20",
@ -2357,6 +2358,12 @@
"tslib": "^2.4.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@ -2395,6 +2402,17 @@
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2496,6 +2514,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001715",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001715.tgz",
@ -2542,6 +2573,18 @@
"fsevents": "~2.3.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/complex.js": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
@ -2735,6 +2778,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@ -2755,6 +2807,20 @@
"node": ">=0.3.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
@ -2814,6 +2880,51 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
@ -2979,6 +3090,42 @@
"node": ">=8"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@ -3036,6 +3183,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -3046,6 +3202,43 @@
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
@ -3099,6 +3292,18 @@
"node": ">=4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -3106,6 +3311,45 @@
"dev": true,
"license": "ISC"
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@ -3723,6 +3967,15 @@
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mathjs": {
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
@ -3768,6 +4021,27 @@
"node": ">= 0.10.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@ -4195,6 +4469,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",

View File

@ -17,6 +17,7 @@
"@types/lodash": "^4.17.16",
"@vueuse/core": "^13.5.0",
"async-mutex": "^0.5.0",
"axios": "^1.11.0",
"echarts": "^5.6.0",
"highlight.js": "^11.11.1",
"konva": "^9.3.20",

View File

@ -279,10 +279,18 @@ async function postProcessApiClient(): Promise<void> {
async function generateApiClient(): Promise<void> {
console.log('Generating API client...');
try {
const npxCommand = getCommand('npx');
await execAsync(`${npxCommand} nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts`);
console.log('✓ API client generated successfully');
const url = 'http://127.0.0.1:5000/GetAPIClientCode';
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch API client code: ${response.status} ${response.statusText}`);
}
const code = await response.text();
// 写入 APIClient.ts
const filePath = 'src/APIClient.ts';
fs.writeFileSync(filePath, code, 'utf8');
console.log('✓ API client code fetched and written successfully');
// 添加后处理步骤
await postProcessApiClient();
} catch (error) {

View File

@ -5,8 +5,11 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using NJsonSchema.CodeGeneration.TypeScript;
using NLog;
using NLog.Web;
using NSwag;
using NSwag.CodeGeneration.TypeScript;
using NSwag.Generation.Processors.Security;
using server.Services;
@ -191,6 +194,34 @@ try
// Setup Program
MsgBus.Init();
// Generate API Client
app.MapGet("GetAPIClientCode", async (HttpContext context) =>
{
try
{
var document = await OpenApiDocument.FromUrlAsync($"http://{Global.localhost}:5000/swagger/v1/swagger.json");
var settings = new TypeScriptClientGeneratorSettings
{
ClassName = "{controller}Client",
UseAbortSignal = false,
Template = TypeScriptTemplate.Axios,
TypeScriptGeneratorSettings = {
},
};
var generator = new TypeScriptClientGenerator(document, settings);
var code = generator.GenerateFile();
return Results.Text(code, "text/plain; charset=utf-8", Encoding.UTF8);
}
catch (Exception err)
{
logger.Error(err);
return Results.Problem(err.ToString());
}
});
app.Run();
}
catch (Exception exception)

View File

@ -26,6 +26,7 @@
<PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
<PackageReference Include="NSwag.CodeGeneration.TypeScript" Version="14.4.0" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />

View File

@ -6,7 +6,7 @@ using Peripherals.DebuggerClient;
namespace server.Controllers;
/// <summary>
/// FPGA调试器控制器
/// FPGA调试器控制器提供信号捕获、触发、数据读取等调试相关API
/// </summary>
[ApiController]
[Route("api/[controller]")]
@ -15,8 +15,81 @@ public class DebuggerController : ControllerBase
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 表示单个信号通道的配置信息
/// </summary>
public class ChannelConfig
{
/// <summary>
/// 通道名称
/// </summary>
required public string name;
/// <summary>
/// 通道显示颜色(如前端波形显示用)
/// </summary>
required public string color;
/// <summary>
/// 通道信号线宽度(位数)
/// </summary>
required public UInt32 wireWidth;
/// <summary>
/// 信号线在父端口中的起始索引bit
/// </summary>
required public UInt32 wireStartIndex;
/// <summary>
/// 父端口编号
/// </summary>
required public UInt32 parentPort;
/// <summary>
/// 捕获模式(如上升沿、下降沿等)
/// </summary>
required public CaptureMode mode;
}
/// <summary>
/// 调试器整体配置信息
/// </summary>
public class DebuggerConfig
{
/// <summary>
/// 时钟频率
/// </summary>
required public UInt32 clkFreq;
/// <summary>
/// 总端口数量
/// </summary>
required public UInt32 totalPortNum;
/// <summary>
/// 捕获深度(采样点数)
/// </summary>
required public UInt32 captureDepth;
/// <summary>
/// 触发器数量
/// </summary>
required public UInt32 triggerNum;
/// <summary>
/// 所有信号通道的配置信息
/// </summary>
required public ChannelConfig[] channelConfigs;
}
/// <summary>
/// 单个通道的捕获数据
/// </summary>
public class ChannelCaptureData
{
/// <summary>
/// 通道名称
/// </summary>
required public string name;
/// <summary>
/// 通道捕获到的数据Base64编码的UInt32数组
/// </summary>
required public string data;
}
/// <summary>
/// 获取调试器实例
/// 获取当前用户绑定的调试器实例
/// </summary>
private DebuggerClient? GetDebugger()
{
@ -50,19 +123,21 @@ public class DebuggerController : ControllerBase
}
/// <summary>
/// 设置捕获模式
/// 设置指定信号线的捕获模式
/// </summary>
/// <param name="wireNum">信号线编号0~511</param>
/// <param name="mode">捕获模式</param>
[HttpPost("SetMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetMode(int channelNum, CaptureMode mode)
public async Task<IActionResult> SetMode(UInt32 wireNum, CaptureMode mode)
{
if (channelNum > 0x0F)
if (wireNum > 512)
{
return BadRequest($"最多只能建立16个通道");
return BadRequest($"最多只能建立512位信号线");
}
try
@ -71,7 +146,7 @@ public class DebuggerController : ControllerBase
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.SetMode((byte)channelNum, mode);
var result = await debugger.SetMode(wireNum, mode);
if (!result.IsSuccessful)
{
logger.Error($"设置捕获模式失败: {result.Error}");
@ -88,7 +163,58 @@ public class DebuggerController : ControllerBase
}
/// <summary>
/// 启动触发器
/// 为每个通道中的每根线设置捕获模式
/// </summary>
/// <param name="config">调试器配置信息,包含所有通道的捕获模式设置</param>
[HttpPost("SetChannelsMode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> SetChannelsMode([FromBody] DebuggerConfig config)
{
if (config == null || config.channelConfigs == null)
return BadRequest("配置无效");
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
foreach (var channel in config.channelConfigs)
{
// 检查每个通道的配置
if (channel.wireWidth > 32 ||
channel.wireStartIndex > 32 ||
channel.wireStartIndex + channel.wireWidth > 32)
{
return BadRequest($"通道 {channel.name} 配置错误");
}
for (uint i = 0; i < channel.wireWidth; i++)
{
var result = await debugger.SetMode(channel.wireStartIndex * (channel.parentPort * 32) + i, channel.mode);
if (!result.IsSuccessful)
{
logger.Error($"设置通道 {channel.name} 第 {i} 根线捕获模式失败: {result.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, $"设置通道 {channel.name} 第 {i} 根线捕获模式失败");
}
}
}
return Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, "为每个通道中的每根线设置捕获模式时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 启动触发器,开始信号捕获
/// </summary>
[HttpPost("StartTrigger")]
[EnableCors("Users")]
@ -120,6 +246,46 @@ public class DebuggerController : ControllerBase
}
}
/// <summary>
/// 重新开始触发(刷新后再启动触发器)
/// </summary>
[HttpPost("RestartTrigger")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> RestartTrigger()
{
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var refreshResult = await debugger.Refresh();
if (!refreshResult.IsSuccessful)
{
logger.Error($"刷新调试器状态失败: {refreshResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "刷新调试器状态失败");
}
var startResult = await debugger.StartTrigger();
if (!startResult.IsSuccessful)
{
logger.Error($"启动触发器失败: {startResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "启动触发器失败");
}
return Ok(startResult.Value);
}
catch (Exception ex)
{
logger.Error(ex, "重新开始触发时发生异常");
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 读取触发器状态标志
/// </summary>
@ -187,32 +353,105 @@ public class DebuggerController : ControllerBase
}
/// <summary>
/// 读取捕获数据
/// 读取捕获数据(等待触发完成后返回各通道采样数据)
/// </summary>
/// <param name="config">调试器配置信息,包含采样深度、端口数、通道配置等</param>
/// <param name="cancellationToken">取消操作的令牌</param>
[HttpGet("ReadData")]
[EnableCors("Users")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ChannelCaptureData[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> ReadData([FromQuery] ushort offset = 0)
public async Task<IActionResult> ReadData([FromBody] DebuggerConfig config, CancellationToken cancellationToken)
{
// 检查每个通道的配置
foreach (var channel in config.channelConfigs)
{
if (channel.wireWidth > 32 ||
channel.wireStartIndex > 32 ||
channel.wireStartIndex + channel.wireWidth > 32)
{
return BadRequest($"通道 {channel.name} 配置错误");
}
}
try
{
var debugger = GetDebugger();
if (debugger == null)
return BadRequest("用户未绑定有效的实验板");
var result = await debugger.ReadData(offset);
if (!result.IsSuccessful)
// 等待捕获标志位
while (true)
{
logger.Error($"读取捕获数据失败: {result.Error}");
cancellationToken.ThrowIfCancellationRequested();
var flagResult = await debugger.ReadFlag();
if (!flagResult.IsSuccessful)
{
logger.Error($"读取捕获标志失败: {flagResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获标志失败");
}
if (flagResult.Value == 1)
{
var clearResult = await debugger.ClearFlag();
if (!clearResult.IsSuccessful)
{
logger.Error($"清除捕获标志失败: {clearResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "清除捕获标志失败");
}
break;
}
await Task.Delay(500, cancellationToken);
}
var dataResult = await debugger.ReadData(config.totalPortNum);
if (!dataResult.IsSuccessful)
{
logger.Error($"读取捕获数据失败: {dataResult.Error}");
return StatusCode(StatusCodes.Status500InternalServerError, "读取捕获数据失败");
}
// 返回Base64编码
var base64Data = Convert.ToBase64String(result.Value);
return Ok(base64Data);
var rawData = dataResult.Value;
int depth = (int)config.captureDepth;
int portDataLen = 4 * depth;
int portNum = (int)config.totalPortNum;
var channelDataList = new List<ChannelCaptureData>();
foreach (var channel in config.channelConfigs)
{
int port = (int)channel.parentPort;
int wireStart = (int)channel.wireStartIndex;
int wireWidth = (int)channel.wireWidth;
// 每个port的数据长度
int portOffset = port * portDataLen;
var channelUintArr = new UInt32[depth];
for (int i = 0; i < depth; i++)
{
// 取出该port的第i个采样点的4字节
int sampleOffset = portOffset + i * 4;
if (sampleOffset + 4 > rawData.Length)
{
logger.Error($"数据越界: port {port}, sample {i}");
return StatusCode(StatusCodes.Status500InternalServerError, "数据越界");
}
UInt32 sample = BitConverter.ToUInt32(rawData, sampleOffset);
// 提取wireWidth位
UInt32 mask = (wireWidth == 32) ? 0xFFFFFFFF : ((1u << wireWidth) - 1u);
channelUintArr[i] = (sample >> wireStart) & mask;
}
var base64 = Convert.ToBase64String(channelUintArr.SelectMany(BitConverter.GetBytes).ToArray());
channelDataList.Add(new ChannelCaptureData { name = channel.name, data = base64 });
}
return Ok(channelDataList.ToArray());
}
catch (OperationCanceledException)
{
logger.Info("读取捕获数据请求被取消");
return StatusCode(StatusCodes.Status499ClientClosedRequest, "客户端已取消请求");
}
catch (Exception ex)
{
@ -222,7 +461,7 @@ public class DebuggerController : ControllerBase
}
/// <summary>
/// 刷新调试器状态
/// 刷新调试器状态(重置采集状态等)
/// </summary>
[HttpPost("Refresh")]
[EnableCors("Users")]

View File

@ -119,18 +119,18 @@ public class DebuggerClient
/// <summary>
/// 设置信号捕获模式
/// </summary>
/// <param name="channelNum">要设置的通道</param>
/// <param name="wireNum">要设置的线</param>
/// <param name="mode">要设置的捕获模式</param>
/// <returns>操作结果成功返回true失败返回错误信息</returns>
public async ValueTask<Result<bool>> SetMode(byte channelNum, CaptureMode mode)
public async ValueTask<Result<bool>> SetMode(UInt32 wireNum, CaptureMode mode)
{
if (channelNum > 0x0F)
if (wireNum > 512)
{
return new(new ArgumentException($"Channel Num can't be over 16, but receive num: {channelNum}"));
return new(new ArgumentException($"Wire Num can't be over 512, but receive num: {wireNum}"));
}
UInt32 data = ((UInt32)mode);
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode + channelNum, data, this.timeout);
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, DebuggerAddr.Mode + wireNum, data, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to set mode: {ret.Error}");
@ -181,7 +181,7 @@ public class DebuggerClient
logger.Error("ReadAddr returned invalid data for flag");
return new(new Exception("Failed to read flag"));
}
return ret.Value.Options.Data[0];
return ret.Value.Options.Data[3];
}
/// <summary>
@ -207,30 +207,26 @@ public class DebuggerClient
/// <summary>
/// 从指定偏移地址读取捕获的数据
/// </summary>
/// <param name="offset">数据读取的偏移地址</param>
/// <returns>操作结果,成功返回32KB的捕获数据,失败返回错误信息</returns>
public async ValueTask<Result<byte[]>> ReadData(UInt16 offset)
/// <param name="portNum">Port数量</param>
/// <returns>操作结果,成功返回捕获数据,失败返回错误信息</returns>
public async ValueTask<Result<byte[]>> ReadData(UInt32 portNum)
{
var captureData = new byte[1024 * 32];
var captureData = new byte[1024 * 4 * portNum];
{
var ret = await UDPClientPool.ReadAddr4BytesAsync(this.ep, this.taskID, this.captureDataAddr + offset, 512, this.timeout);
var ret = await UDPClientPool.ReadAddr4BytesAsync(this.ep, this.taskID, this.captureDataAddr, captureData.Length, this.timeout);
if (!ret.IsSuccessful)
{
logger.Error($"Failed to read data: {ret.Error}");
return new(ret.Error);
}
Buffer.BlockCopy(ret.Value, 0, captureData, 0, 512 * 4);
}
{
var ret = await UDPClientPool.ReadAddr4BytesAsync(this.ep, this.taskID, this.captureDataAddr + offset + 512, 512, this.timeout);
if (!ret.IsSuccessful)
if (ret.Value.Length != captureData.Length)
{
logger.Error($"Failed to read data: {ret.Error}");
return new(ret.Error);
logger.Error($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}");
return new(new Exception($"Receive capture data length should be {captureData.Length} instead of {ret.Value.Length}"));
}
Buffer.BlockCopy(ret.Value, 0, captureData, 512 * 4, 512 * 4);
Buffer.BlockCopy(ret.Value, 0, captureData, 0, captureData.Length);
}
return captureData;

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@
<h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据
</h3>
<p class="text-sm text-slate-500">点击下方按钮生成测试数据用于观察</p>
<p class="text-sm text-slate-500">点击下方按钮开始捕获</p>
</div>
<!-- <button

View File

@ -14,6 +14,7 @@ import {
OscilloscopeApiClient,
DebuggerClient,
} from "@/APIClient";
import axios, { type AxiosInstance } from "axios";
// 支持的客户端类型联合类型
type SupportedClient =
@ -108,13 +109,27 @@ export class AuthManager {
};
}
// 私有方法创建带认证的Axios实例
private static createAuthenticatedAxiosInstance(): AxiosInstance | null {
const token = AuthManager.getToken();
if (!token) return null;
const instance = axios.create();
instance.interceptors.request.use(config => {
config.headers = config.headers || {};
(config.headers as any)["Authorization"] = `Bearer ${token}`;
return config;
});
return instance;
}
// 通用的创建已认证客户端的方法(使用泛型)
public static createAuthenticatedClient<T extends SupportedClient>(
ClientClass: new (baseUrl?: string, http?: any) => T,
ClientClass: new (baseUrl?: string, instance?: AxiosInstance) => T,
): T {
const customHttp = AuthManager.createAuthenticatedHttp();
return customHttp
? new ClientClass(undefined, customHttp)
const axiosInstance = AuthManager.createAuthenticatedAxiosInstance();
return axiosInstance
? new ClientClass(undefined, axiosInstance)
: new ClientClass();
}

View File

@ -8,6 +8,13 @@
调试器波形捕获
</div>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="startCapture(true)"
:disabled="!captureData"
>
重新捕获
</button>
<button
class="btn btn-sm btn-error"
@click="handleDeleteData"
@ -18,6 +25,12 @@
</div>
</h2>
<WaveformDisplay :data="captureData">
<div class="text-center">
<h3 class="text-xl font-semibold text-slate-600 mb-2">
暂无逻辑分析数据
</h3>
<p class="text-sm text-slate-500">点击下方按钮开始捕获</p>
</div>
<button
class="group relative px-8 py-3 bg-gradient-to-r text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 ease-in-out focus:outline-none focus:ring-4 active:scale-95"
:class="{
@ -45,51 +58,94 @@
<!-- Debugger 通道配置 -->
<div class="card m-5 bg-base-200 shadow-2xl">
<div class="card-body">
<h2 class="card-title mb-4">调试器通道配置</h2>
<div class="overflow-x-auto flex flex-col gap-10">
<!-- 通道状态概览 -->
<div class="flex justify-between">
<h2 class="card-title mb-4">调试器通道配置</h2>
<div class="flex items-center gap-2">
<button
class="btn btn-sm btn-primary"
@click="addChannel"
:disabled="!configInited"
>
添加通道
</button>
<button
class="btn btn-sm btn-secondary"
@click="showConfigDialog = true"
>
配置
</button>
</div>
</div>
<!-- 配置未初始化时 -->
<div
v-if="!configInited"
class="flex flex-col items-center justify-center py-10"
>
<div class="text-lg text-slate-500 mb-4">请先进行调试器基本配置</div>
<button class="btn btn-primary" @click="showConfigDialog = true">
配置调试器
</button>
</div>
<div v-if="configInited" class="overflow-x-auto flex flex-col gap-10">
<!-- 状态概览 -->
<div
class="stats stats-horizontal bg-base-100 shadow flex justify-between"
>
<div class="stat">
<div class="stat-title">总通道数</div>
<div class="stat-value text-primary">16</div>
<div class="stat-desc">逻辑分析仪通道</div>
</div>
<div class="stat">
<div class="stat-title">启用通道</div>
<div class="stat-value text-success">
{{ channels.filter((ch) => ch.visible).length }}
<div class="stat-title">启用端口数</div>
<div class="stat-value text-primary">
{{ config.totalPortNum }}
</div>
<div class="stat-desc">当前激活通道</div>
<div class="stat-desc">每端口最大32线</div>
</div>
<div class="stat">
<div class="stat-title">采样率</div>
<div class="stat-value text-info">5MHz</div>
<div class="stat-desc">最大采样频率</div>
<div class="stat-title">最大线宽数</div>
<div class="stat-value text-info">
{{ config.totalPortNum * 32 }}
</div>
<div class="stat-desc">启用端口数 × 32</div>
</div>
<div class="stat">
<div class="stat-title">已用线宽数</div>
<div class="stat-value text-success">
{{ channels.reduce((sum, ch) => sum + ch.width, 0) }}
</div>
<div class="stat-desc">所有通道线宽总和</div>
</div>
<div class="stat">
<div class="stat-title">采样深度</div>
<div class="stat-value text-warning">
{{ config.captureDepth }}
</div>
<div class="stat-desc">每通道采样点数</div>
</div>
<div class="stat">
<div class="stat-title">时钟频率</div>
<div class="stat-value text-accent">{{ config.clkFreq }} MHz</div>
<div class="stat-desc">采样时钟</div>
</div>
</div>
<!-- 表格 -->
<!-- 通道表格 -->
<div class="space-y-2">
<!-- 表头 -->
<div
class="grid grid-cols-6 justify-items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
class="grid grid-cols-7 justify-items-center gap-2 p-2 bg-base-300 rounded-lg text-sm font-medium"
>
<span>名称</span>
<span>显示</span>
<span>颜色</span>
<span>触发模式</span>
<span>数据位数</span>
<span>数据位宽(起始:宽度)</span>
<span>父端口编号</span>
<span>操作</span>
</div>
<!-- 通道列表 -->
<div
v-for="(ch, idx) in channels"
:key="idx"
class="grid grid-cols-6 justify-items-center gap-2 p-2 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
class="grid grid-cols-7 place-items-center gap-4 p-4 bg-base-200 rounded-lg hover:bg-base-300 transition-colors"
>
<input
v-model="ch.name"
@ -118,11 +174,17 @@
{{ mode.label }}
</option>
</select>
<input
v-model="ch.widthStr"
class="input input-bordered w-full"
placeholder="如0:8"
@change="parseWidthStr(idx)"
/>
<input
type="number"
min="1"
max="32"
v-model.number="ch.width"
min="0"
:max="config.totalPortNum - 1"
v-model.number="ch.parentPort"
class="input input-bordered w-full"
/>
<button class="btn btn-error" @click="removeChannel(idx)">
@ -139,19 +201,78 @@
</div>
</div>
</div>
<!-- 配置Dialog -->
<dialog v-if="showConfigDialog" class="modal modal-open">
<form
method="dialog"
class="modal-box max-w-fit"
@submit.prevent="onConfigSubmit"
>
<h3 class="font-bold text-lg mb-4">调试器基本配置</h3>
<div class="flex flex-col gap-4 w-80">
<BaseInputField
v-model="config.clkFreq"
label="时钟频率 (MHz)"
type="number"
min="1"
max="200"
:error="
config.clkFreq < 1 || config.clkFreq > 500 ? '范围1~200' : ''
"
required
/>
<BaseInputField
v-model="config.totalPortNum"
label="启用端口数"
type="number"
min="1"
max="16"
:error="
config.totalPortNum < 1 || config.totalPortNum > 16
? '范围1~16'
: ''
"
required
/>
<BaseInputField
v-model="config.captureDepth"
label="采样深度"
type="number"
min="1"
max="1048576"
:error="
config.captureDepth < 1 || config.captureDepth > 1048576
? '范围1~1048576'
: ''
"
required
/>
</div>
<div class="modal-action mt-6">
<button class="btn btn-primary" type="submit">确定</button>
<button class="btn" type="button" @click="showConfigDialog = false">
取消
</button>
</div>
</form>
</dialog>
</div>
</template>
<script setup lang="ts">
import { CaptureMode } from "@/APIClient";
import { CaptureMode, ChannelConfig, DebuggerConfig } from "@/APIClient";
import { useAlertStore } from "@/components/Alert";
import BaseInputField from "@/components/InputField/BaseInputField.vue";
import type { LogicDataType } from "@/components/WaveformDisplay";
import WaveformDisplay from "@/components/WaveformDisplay/WaveformDisplay.vue";
import { AuthManager } from "@/utils/AuthManager";
import { useRequiredInjection } from "@/utils/Common";
import { useLocalStorage } from "@vueuse/core";
import axios, { type CancelTokenSource } from "axios";
import { isNull } from "lodash";
import { Play, Square, Zap } from "lucide-vue-next";
import { ref } from "vue";
import { ref, reactive, computed } from "vue";
interface DebugChannel {
name: string;
@ -159,6 +280,15 @@ interface DebugChannel {
color: string;
trigger: CaptureMode;
width: number;
widthStr: string; // "start:"
start: number;
parentPort: number;
}
interface DebuggerSettings {
clkFreq: number;
totalPortNum: number;
captureDepth: number;
}
const triggerModes = [
@ -169,153 +299,234 @@ const triggerModes = [
{ value: CaptureMode.Fall, label: "↓ (下降沿)" },
];
//
const config = reactive<DebuggerSettings>({
clkFreq: 50,
totalPortNum: 1,
captureDepth: 1024,
});
const configInited = ref(false);
const showConfigDialog = ref(false);
function onConfigSubmit() {
configInited.value = true;
showConfigDialog.value = false;
//
channels.value = [];
}
//
const channels = useLocalStorage<DebugChannel[]>("debugger-channels", []);
const captureData = ref<LogicDataType>();
const alert = useRequiredInjection(useAlertStore);
const isCapturing = ref(false);
const readCancelTokenSource = ref<CancelTokenSource | null>(null);
async function startCapture() {
if (channels.value.length === 0) {
alert.error("请至少添加一个通道");
// widthStrstart/width
function parseWidthStr(idx: number) {
const ch = channels.value[idx];
const match = /^(\d+)\s*:\s*(\d+)$/.exec(ch.widthStr);
if (isNull(match)) {
alert.error("格式错误,应为 起始位:宽度,如 0:7");
ch.widthStr = `${ch.start}:${ch.width}`;
return;
}
isCapturing.value = true;
const client = AuthManager.createAuthenticatedDebuggerClient();
const min = Math.min(parseInt(match[1]), parseInt(match[2]));
const max = Math.max(parseInt(match[1]), parseInt(match[2]));
for (let i = 0; i < channels.value.length; i++) {
const channel = channels.value[i];
if (!channel.visible) continue;
if (!channel.name) {
alert.error(`通道 ${i + 1} 名称不能为空`);
isCapturing.value = false;
return;
}
if (channel.width < 1 || channel.width > 32) {
alert.error(`通道 ${i + 1} 数据位数必须在1到32之间`);
isCapturing.value = false;
return;
}
try {
let ret = await client.setMode(i, channel.trigger);
if (!ret) {
alert.error(`设置通道 ${i + 1} 触发模式失败`);
isCapturing.value = false;
return;
}
} catch (error: any) {
alert.error(`设置通道 ${i + 1} 触发模式失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
try {
let ret = await client.startTrigger();
if (!ret) {
alert.error("开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
while ((await client.readFlag()) !== 1 && isCapturing.value) {
await new Promise((resolve) => setTimeout(resolve, 500));
}
if (!isCapturing.value) {
alert.info("捕获已停止");
return;
}
const base64Data = await client.readData(0);
const binaryString = atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
//
// 1.
const enabledChannels = channels.value.filter((ch) => ch.visible);
const widths = enabledChannels.map((ch) => ch.width);
const totalBitsPerSample = widths.reduce((a, b) => a + b, 0);
// 2.
const totalBits = bytes.length * 8;
const sampleCount = Math.floor(totalBits / totalBitsPerSample);
// 3.
let bitOffset = 0;
const channelValues: number[][] = enabledChannels.map(() => []);
for (let sampleIdx = 0; sampleIdx < sampleCount; sampleIdx++) {
for (let chIdx = 0; chIdx < enabledChannels.length; chIdx++) {
const width = widths[chIdx];
let value = 0;
for (let w = 0; w < width; w++) {
const absBit = bitOffset + w;
const byteIdx = Math.floor(absBit / 8);
const bitInByte = absBit % 8;
const bit = (bytes[byteIdx] >> (7 - bitInByte)) & 1;
value = (value << 1) | bit;
}
channelValues[chIdx].push(value);
bitOffset += width;
}
}
// 4. LogicDataType
const x: number[] = [];
const xUnit = "us"; // 5MHz -> 0.2us/point, WaveformDisplayns/ms/us/sus
for (let i = 0; i < sampleCount; i++) {
x.push(i * 0.2); // 0.2us per sample
}
const y = enabledChannels.map((ch, idx) => ({
enabled: true,
type: ch.width === 1 ? ("logic" as const) : ("number" as const),
name: ch.name,
color: ch.color,
value: channelValues[idx],
base: ch.width === 1 ? ("bin" as const) : ("hex" as const),
}));
captureData.value = {
x: x,
y: y,
xUnit: "us",
};
} catch (error: any) {
alert.error(`开始捕获失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
function stopCapture() {
isCapturing.value = false;
}
function handleDeleteData() {
captureData.value = undefined;
ch.start = min;
ch.width = max - min + 1;
}
function addChannel() {
if (channels.value.length >= 16) {
alert.error("最多只能添加16个通道");
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length >= config.totalPortNum * 32) {
alert.error("通道数已达最大线宽数");
return;
}
channels.value.push({
name: `CH${channels.value.length + 1}`,
visible: true,
color: "#00bcd4",
trigger: CaptureMode.None,
width: 1,
widthStr: "0:1",
start: 0,
parentPort: 0,
});
}
function removeChannel(idx: number) {
channels.value.splice(idx, 1);
}
function handleDeleteData() {
captureData.value = undefined;
}
function stopCapture() {
isCapturing.value = false;
if (readCancelTokenSource.value) {
readCancelTokenSource.value.cancel("用户手动停止捕获");
readCancelTokenSource.value = null;
}
}
async function startCapture(isRestart = false) {
if (!configInited.value) {
alert.error("请先配置调试器基本参数");
return;
}
if (channels.value.length === 0) {
alert.error("请至少添加一个通道");
return;
}
//
if (!isRestart) {
let usedWires = 0;
for (let i = 0; i < channels.value.length; i++) {
const ch = channels.value[i];
if (!ch.visible) continue;
if (!ch.name) {
alert.error(`通道 ${i + 1} 名称不能为空`);
return;
}
if (ch.width < 1 || ch.width > 32) {
alert.error(`通道 ${i + 1} 数据位宽必须在1到32之间`);
return;
}
if (ch.start < 0 || ch.start + ch.width > 32) {
alert.error(`通道 ${i + 1} 起始位+宽度不能超过32`);
return;
}
if (ch.parentPort < 0 || ch.parentPort >= config.totalPortNum) {
alert.error(`通道 ${i + 1} 父端口编号超出范围`);
return;
}
usedWires += ch.width;
}
if (usedWires > config.totalPortNum * 32) {
alert.error("所有通道线宽总和不能超过最大线宽数");
return;
}
}
isCapturing.value = true;
const client = AuthManager.createAuthenticatedDebuggerClient();
// API
const channelConfigs = channels.value
.filter((ch) => ch.visible)
.map(
(ch) =>
new ChannelConfig({
name: ch.name,
color: ch.color,
wireWidth: ch.width,
wireStartIndex: ch.start,
parentPort: ch.parentPort,
mode: ch.trigger,
}),
);
const apiConfig = new DebuggerConfig({
clkFreq: config.clkFreq,
totalPortNum: config.totalPortNum,
captureDepth: config.captureDepth,
triggerNum: 0,
channelConfigs: channelConfigs,
});
try {
//
if (!isRestart) {
let ret = await client.setChannelsMode(apiConfig);
if (!ret) {
alert.error("设置通道模式失败");
isCapturing.value = false;
return;
}
//
ret = await client.startTrigger();
if (!ret) {
alert.error("开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
} else {
let ret = await client.restartTrigger();
if (!ret) {
alert.error("重新开始捕获失败,请检查连接");
isCapturing.value = false;
return;
}
}
//
readCancelTokenSource.value = axios.CancelToken.source();
const readDataPromise = client
.readData(apiConfig, readCancelTokenSource.value.token)
.then((data) => {
const enabledChannels = channelConfigs;
const sampleCount = config.captureDepth;
//
const y = data.map((cd, idx) => {
const ch = enabledChannels[idx];
const bin = atob(cd.data);
// UInt32
const arr = [];
for (let i = 0; i < bin.length; i += 4) {
arr.push(
(bin.charCodeAt(i) << 24) |
(bin.charCodeAt(i + 1) << 16) |
(bin.charCodeAt(i + 2) << 8) |
bin.charCodeAt(i + 3),
);
}
//
return {
enabled: true,
type: ch.wireWidth === 1 ? ("logic" as const) : ("number" as const),
name: ch.name,
color: ch.color,
value: arr.slice(0, sampleCount),
base: ch.wireWidth === 1 ? ("bin" as const) : ("hex" as const),
};
});
const x: number[] = [];
for (let i = 0; i < sampleCount; i++) {
x.push(i * (1 / config.clkFreq)); // us
}
captureData.value = {
x,
y,
xUnit: "us",
};
})
.catch((error) => {
if (axios.isCancel(error)) {
alert.info("捕获已取消");
} else {
alert.error(`读取数据失败: ${error.message}`);
}
})
.finally(() => {
isCapturing.value = false;
readCancelTokenSource.value = null;
});
} catch (error: any) {
alert.error(`开始捕获失败: ${error.message}`);
isCapturing.value = false;
return;
}
}
</script>