Merge branch 'csharp'

This commit is contained in:
SikongJueluo 2025-05-19 21:19:55 +08:00
commit 068576b60b
No known key found for this signature in database
42 changed files with 8757 additions and 2053 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
.vscode
dist dist
**/wwwroot **/wwwroot
dist-ssr dist-ssr

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "server/src/BsdlParser"]
path = server/src/BsdlParser
url = git@github.com:SikongJueluo/python-bsdl-parser.git

View File

@ -1,3 +1,4 @@
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
isSelfContained := "false" isSelfContained := "false"
@_show-dir: @_show-dir:
@ -12,17 +13,16 @@ clean:
rm -rf "server.test/bin" rm -rf "server.test/bin"
rm -rf "server.test/obj" rm -rf "server.test/obj"
rm -rf "dist" rm -rf "dist"
rm -rf "wwwroot"
update: update:
npm install npm install
cd server && dotnet restore dotnet restore ./server/server.csproj
git submodule update --init --remote --recursive git submodule update --init --remote --recursive
# 生成Restful API到网页客户端 # 生成Restful API到网页客户端
gen-api: gen-api:
cd server && dotnet run & npm run gen-api
npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts
pkill server
# 构建服务器包含win与linux平台 # 构建服务器包含win与linux平台
[working-directory: "server"] [working-directory: "server"]
@ -32,8 +32,10 @@ build-server self-contained=isSelfContained: _show-dir
rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/linux-x64/publish/wwwroot/ rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/linux-x64/publish/wwwroot/
rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/win-x64/publish/wwwroot/ rsync -avz --delete ../wwwroot/ ./bin/Release/net9.0/win-x64/publish/wwwroot/
run: run-server
run-server: (build-server "true") run-server: (build-server "true")
exec ./server/bin/Release/net9.0/linux-x64/publish/server ./server/bin/Release/net9.0/linux-x64/publish/server
run-web: run-web:
npm run build npm run build
@ -43,7 +45,7 @@ dev: dev-server
# 测试服务器 # 测试服务器
dev-server: _show-dir dev-server: _show-dir
cd server && dotnet run --watch dotnet run --watch --project ./server/server.csproj
# 运行网页客户端 # 运行网页客户端
dev-web: dev-web:

16
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"all": "^0.0.0", "all": "^0.0.0",
"async-mutex": "^0.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"marked": "^12.0.0", "marked": "^12.0.0",
@ -2014,6 +2015,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/async-mutex": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
"integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.21", "version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@ -3720,6 +3730,12 @@
"integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==", "integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typed-function": { "node_modules/typed-function": {
"version": "4.2.1", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",

View File

@ -9,7 +9,7 @@
"preview": "vite preview", "preview": "vite preview",
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"pregen-api": "cd server && dotnet run &", "pregen-api": "cd server && dotnet run --property:Configuration=Release &",
"gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts", "gen-api": "npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts",
"postgen-api": "pkill server" "postgen-api": "pkill server"
}, },
@ -17,6 +17,7 @@
"@svgdotjs/svg.js": "^3.2.4", "@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"all": "^0.0.0", "all": "^0.0.0",
"async-mutex": "^0.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"marked": "^12.0.0", "marked": "^12.0.0",

1
server/.gitignore vendored
View File

@ -1,4 +1,5 @@
obj obj
bin bin
bitstream bitstream
bsdl

View File

@ -94,13 +94,31 @@ try
logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}"); logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}");
app.UseDefaultFiles(); app.UseDefaultFiles();
app.UseStaticFiles(); // Serves files from wwwroot by default app.UseStaticFiles(); // Serves files from wwwroot by default
// Assets Files
app.UseStaticFiles(new StaticFileOptions app.UseStaticFiles(new StaticFileOptions
{ {
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")), FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")),
RequestPath = "/assets" RequestPath = "/assets"
}); });
// Public Files
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "EquipmentTemplates")),
RequestPath = "/public/EquipmentTemplates"
});
// Log Files
if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log")))
{
Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "log"));
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "log")),
RequestPath = "/log"
});
app.MapFallbackToFile("index.html"); app.MapFallbackToFile("index.html");
} }
// Add logs
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
app.UseCors(); app.UseCors();

File diff suppressed because it is too large Load Diff

View File

@ -25,7 +25,6 @@
<PackageReference Include="NLog" Version="5.4.0" /> <PackageReference Include="NLog" Version="5.4.0" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" /> <PackageReference Include="NLog.Web.AspNetCore" Version="5.4.0" />
<PackageReference Include="NSwag.AspNetCore" Version="14.3.0" /> <PackageReference Include="NSwag.AspNetCore" Version="14.3.0" />
<PackageReference Include="pythonnet" Version="3.0.5" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" /> <PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
</ItemGroup> </ItemGroup>

@ -1 +0,0 @@
Subproject commit ac164eb16d7d9a9a387ff1f62cef249c65565bef

192
server/src/BsdlParser.cs Normal file
View File

@ -0,0 +1,192 @@
using DotNext;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace BsdlParser;
/// <summary>
/// [TODO:description]
/// </summary>
public class BoundaryScanRegs
{
/// <summary>
/// [TODO:description]
/// </summary>
public class CellEntry
{
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("cell_number")]
[JsonRequired]
public int CellNumber { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("cell_name")]
[JsonRequired]
public string CellName { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("port_id")]
public string? PortID { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("function")]
[JsonRequired]
public string? Function { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("safe_bit")]
[JsonRequired]
public string? SafeBit { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("ccell")]
public string? CCell { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("disabel_value")]
public string? DisableValue { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("disabel_result")]
public string? DisableResult { get; set; }
}
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("register_length")]
[JsonRequired]
public int RegisterLength { get; set; }
/// <summary>
/// [TODO:description]
/// </summary>
[JsonProperty("registers")]
[JsonRequired]
public CellEntry[] Registers { get; set; } = new CellEntry[] { };
}
/// <summary>
/// [TODO:description]
/// </summary>
public class Parser
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BOUNDARY_REGS_DESP = "boundary_registers.json";
/// <summary>
/// [TODO:description]
/// </summary>
public JObject BoundaryRegsDesp { get; }
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Parser()
{
var filePath = Path.Combine(Environment.CurrentDirectory, BOUNDARY_REGS_DESP);
if (!Path.Exists(filePath))
throw new Exception($"Counld not find boundary_registers.json in {filePath}");
this.BoundaryRegsDesp = JObject.Parse(File.ReadAllText(filePath));
}
/// <summary>
/// [TODO:description]
/// </summary>
public Optional<int> GetBoundaryRegsNum()
{
var ret = this.BoundaryRegsDesp["register_length"];
if (ret is null) return new();
return Convert.ToInt32(ret);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryPorts()
{
var registers = this.BoundaryRegsDesp["registers"]?.ToList();
if (registers is null) return new();
var cellList = new List<BoundaryScanRegs.CellEntry>();
foreach (var item in registers)
{
var cell = item.ToObject<BoundaryScanRegs.CellEntry>();
if (cell is null) return new();
cellList.Add(cell);
}
return cellList;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public Optional<List<BoundaryScanRegs.CellEntry>> GetBoundaryLogicalPorts()
{
var registers = this.BoundaryRegsDesp["registers"]?.ToList().Where((item)=>{
return item["port_id"] is not null;
});
if (registers is null) return new();
var cellList = new List<BoundaryScanRegs.CellEntry>();
foreach (var item in registers)
{
var cell = item.ToObject<BoundaryScanRegs.CellEntry>();
if (cell is null) return new();
cellList.Add(cell);
}
return cellList;
}
// public Result<string> GetLogicalPorts()
// {
// using (Py.GIL())
// {
// using (PyModule scope = Py.CreateScope())
// {
// string code = $@"
// bsdl_parser = BsdlParser({this.filePath})
// result = json.dumps(bsdl_parser.GetLogicPortDesp(), indent=2)
// ";
//
// var localVariables = new PyDict();
// scope.Exec(code, localVariables);
// if (!localVariables.HasKey("result"))
// return new(new Exception($"PythonNet doesn't has result from dict: {localVariables}"));
//
// var result = localVariables.GetItem("result");
// if (result is null)
// return new(new Exception($"PythonNet get null from dict: {localVariables}"));
//
// var resultString = result.ToString();
// if (resultString is null)
// return new(new Exception($"Pythonnet convert PyObject to string failed :{result}"));
// return resultString;
// }
// }
// }
}

View File

@ -8,44 +8,6 @@ using WebProtocol;
namespace server.Controllers; namespace server.Controllers;
// /// <summary>
// /// [TODO:description]
// /// </summary>
// public class HomeController : ControllerBase
// {
// private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
//
// string INDEX_HTML_PATH = Path.Combine(Environment.CurrentDirectory, "index.html");
//
// /// <summary>
// /// [TODO:description]
// /// </summary>
// /// <returns>[TODO:return]</returns>
// [HttpGet("/")]
// public IResult Index()
// {
// return TypedResults.Content("Hello", "text/html");
// }
//
// // [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
// // public IResult Error()
// // {
// // return TypedResults.Ok();
// // }
//
// /// <summary>
// /// [TODO:description]
// /// </summary>
// /// <returns>[TODO:return]</returns>
// [HttpGet("/hello")]
// [HttpPost("/hello")]
// public IActionResult Hello()
// {
// string randomString = Guid.NewGuid().ToString();
// return this.Ok($"Hello World! GUID: {randomString}");
// }
// }
/// <summary> /// <summary>
/// UDP API /// UDP API
/// </summary> /// </summary>
@ -198,6 +160,7 @@ public class JtagController : ControllerBase
/// <param name="address"> 设备地址 </param> /// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param> /// <param name="port"> 设备端口 </param>
[HttpGet("GetDeviceIDCode")] [HttpGet("GetDeviceIDCode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)] [ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetDeviceIDCode(string address, int port) public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
@ -375,17 +338,65 @@ public class JtagController : ControllerBase
/// </summary> /// </summary>
/// <param name="address">[TODO:parameter]</param> /// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param> /// <param name="port">[TODO:parameter]</param>
/// <param name="portNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns> /// <returns>[TODO:return]</returns>
[HttpPost("BoundaryScan")] [HttpPost("BoundaryScanAllPorts")]
[EnableCors("Users")] [EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> BoundaryScan(string address, int port, int portNum) public async ValueTask<IResult> BoundaryScanAllPorts(string address, int port)
{ {
var jtagCtrl = new JtagClient.Jtag(address, port); var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScan(portNum); var ret = await jtagCtrl.BoundaryScan();
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
return TypedResults.Ok(ret.Value);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("BoundaryScanLogicalPorts")]
[EnableCors("Users")]
[ProducesResponseType(typeof(Dictionary<string, bool>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> BoundaryScanLogicalPorts(string address, int port)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.BoundaryScanLogicalPorts();
if (!ret.IsSuccessful)
{
if (ret.Error is ArgumentException)
return TypedResults.BadRequest(ret.Error);
else return TypedResults.InternalServerError(ret.Error);
}
return TypedResults.Ok(ret.Value);
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="speed">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetSpeed")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetSpeed(string address, int port, UInt32 speed)
{
var jtagCtrl = new JtagClient.Jtag(address, port);
var ret = await jtagCtrl.SetSpeed(speed);
if (!ret.IsSuccessful) if (!ret.IsSuccessful)
{ {
if (ret.Error is ArgumentException) if (ret.Error is ArgumentException)
@ -402,7 +413,7 @@ public class JtagController : ControllerBase
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class RemoteUpdater : ControllerBase public class RemoteUpdateController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@ -418,8 +429,9 @@ public class RemoteUpdater : ControllerBase
/// <param name="bitstream3">比特流文件3</param> /// <param name="bitstream3">比特流文件3</param>
/// <returns>上传结果</returns> /// <returns>上传结果</returns>
[HttpPost("UploadBitstream")] [HttpPost("UploadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)] [EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstreams( public async ValueTask<IResult> UploadBitstreams(
string address, string address,
IFormFile? goldenBitream, IFormFile? goldenBitream,
@ -469,7 +481,7 @@ public class RemoteUpdater : ControllerBase
} }
logger.Info($"Device {address} Upload Bitstream Successfully"); logger.Info($"Device {address} Upload Bitstream Successfully");
return TypedResults.Ok("Bitstream Upload Successfully"); return TypedResults.Ok(true);
} }
private async ValueTask<Result<byte[]>> ProcessBitstream(string filePath) private async ValueTask<Result<byte[]>> ProcessBitstream(string filePath)
@ -522,9 +534,10 @@ public class RemoteUpdater : ControllerBase
/// <param name="port"> 设备端口 </param> /// <param name="port"> 设备端口 </param>
/// <param name="bitstreamNum"> 比特流位号 </param> /// <param name="bitstreamNum"> 比特流位号 </param>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)] [EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UpdateBitstream(string address, int port, int bitstreamNum) public async ValueTask<IResult> UpdateBitstream(string address, int port, int bitstreamNum)
{ {
// 检查文件 // 检查文件
@ -541,7 +554,7 @@ public class RemoteUpdater : ControllerBase
if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error); if (!fileBytes.IsSuccessful) return TypedResults.InternalServerError(fileBytes.Error);
// 下载比特流 // 下载比特流
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port); var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value); var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
if (ret.IsSuccessful) if (ret.IsSuccessful)
@ -571,9 +584,10 @@ public class RemoteUpdater : ControllerBase
/// <param name="bitstreamNum">比特流编号</param> /// <param name="bitstreamNum">比特流编号</param>
/// <returns>总共上传比特流的数量</returns> /// <returns>总共上传比特流的数量</returns>
[HttpPost("DownloadMultiBitstreams")] [HttpPost("DownloadMultiBitstreams")]
[ProducesResponseType(StatusCodes.Status200OK)] [EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> DownloadMultiBitstreams(string address, int port, int? bitstreamNum) public async ValueTask<IResult> DownloadMultiBitstreams(string address, int port, int? bitstreamNum)
{ {
// 检查文件 // 检查文件
@ -600,7 +614,7 @@ public class RemoteUpdater : ControllerBase
} }
// 下载比特流 // 下载比特流
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port); var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
{ {
var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]); var ret = await remoteUpdater.UploadBitstreams(bitstreams[0], bitstreams[1], bitstreams[2], bitstreams[3]);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error); if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
@ -630,17 +644,18 @@ public class RemoteUpdater : ControllerBase
/// <param name="bitstreamNum">比特流编号</param> /// <param name="bitstreamNum">比特流编号</param>
/// <returns>操作结果</returns> /// <returns>操作结果</returns>
[HttpPost("HotResetBitstream")] [HttpPost("HotResetBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)] [EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum) public async ValueTask<IResult> HotResetBitstream(string address, int port, int bitstreamNum)
{ {
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port); var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.HotResetBitstream(bitstreamNum); var ret = await remoteUpdater.HotResetBitstream(bitstreamNum);
if (ret.IsSuccessful) if (ret.IsSuccessful)
{ {
logger.Info($"Device {address}即可 Update bitstream successfully"); logger.Info($"Device {address} Update bitstream successfully");
return TypedResults.Ok(ret.Value); return TypedResults.Ok(ret.Value);
} }
else else
@ -649,6 +664,160 @@ public class RemoteUpdater : ControllerBase
return TypedResults.InternalServerError(ret.Error); return TypedResults.InternalServerError(ret.Error);
} }
} }
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("GetFirmwareVersion")]
[EnableCors("Users")]
[ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetFirmwareVersion(string address, int port)
{
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.GetVersion();
if (ret.IsSuccessful)
{
logger.Info($"Device {address} get firmware version successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}
/// <summary>
/// [TODO:description]
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class DDSController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetWaveNum")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetWaveNum(string address, int port, int channelNum, int waveNum)
{
var dds = new DDSClient.DDS(address, port);
var ret = await dds.SetWaveNum(channelNum, waveNum);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} set output wave num successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="step">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetFreq")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetFreq(string address, int port, int channelNum, int waveNum, UInt32 step)
{
var dds = new DDSClient.DDS(address, port);
var ret = await dds.SetFreq(channelNum, waveNum, step);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} set output freqency successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="phase">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("SetPhase")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> SetPhase(string address, int port, int channelNum, int waveNum, int phase)
{
var dds = new DDSClient.DDS(address, port);
var ret = await dds.SetPhase(channelNum, waveNum, phase);
if (ret.IsSuccessful)
{
logger.Info($"Device {address} set output phase successfully");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error(ret.Error);
return TypedResults.InternalServerError(ret.Error);
}
}
}
/// <summary>
/// [TODO:description]
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class BsdlParserController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
[EnableCors("Development")]
[HttpGet("GetBoundaryLogicalPorts")]
public IResult GetBoundaryLogicalPorts()
{
var parser = new BsdlParser.Parser();
var ret = parser.GetBoundaryLogicalPorts();
if (ret.IsNull) return TypedResults.InternalServerError("Get Null");
return TypedResults.Ok(ret.Value);
}
} }
@ -657,7 +826,7 @@ public class RemoteUpdater : ControllerBase
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class Data : ControllerBase public class DataController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@ -716,19 +885,3 @@ public class Data : ControllerBase
} }
} }
/// <summary>
/// 日志控制器
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class Log : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 日志文件路径
/// </summary>
private readonly string _logFilePath = Directory.GetFiles(Directory.GetCurrentDirectory())[0];
}

170
server/src/DDSClient.cs Normal file
View File

@ -0,0 +1,170 @@
using System.Net;
using DotNext;
namespace DDSClient;
static class DDSAddr
{
/*24
00 R/W CHANNEL0 wave_sel
01 R/W CHANNEL0 STORE0 freq_ctrl
02 R/W CHANNEL0 STORE1 freq_ctrl
03 R/W CHANNEL0 STORE2 freq_ctrl
04 R/W CHANNEL0 STORE3 freq_ctrl
05 R/W CHANNEL0 STORE0 phase_ctrl
06 R/W CHANNEL0 STORE1 phase_ctrl
07 R/W CHANNEL0 STORE2 phase_ctrl
08 R/W CHANNEL0 STORE3 phase_ctrl
09 R/W CHANNEL0 dds_wr_enable
0A WO CHANNEL0 data
10 R/W CHANNEL1 wave_sel
11 R/W CHANNEL1 STORE0 freq_ctrl
12 R/W CHANNEL1 STORE1 freq_ctrl
13 R/W CHANNEL1 STORE2 freq_ctrl
14 R/W CHANNEL1 STORE3 freq_ctrl
15 R/W CHANNEL1 STORE0 phase_ctrl
16 R/W CHANNEL1 STORE1 phase_ctrl
17 R/W CHANNEL1 STORE2 phase_ctrl
18 R/W CHANNEL1 STORE3 phase_ctrl
19 R/W CHANNEL1 dds_wr_enable
1A WO CHANNEL1 data
*/
public const UInt32 Base = 0x4000_0000;
public static readonly ChannelAddr[] Channel = { new ChannelAddr(0x00), new ChannelAddr(0x10) };
public class ChannelAddr
{
private readonly UInt32 Offset;
public readonly UInt32 WaveSelect;
public readonly UInt32[] FreqCtrl;
public readonly UInt32[] PhaseCtrl;
public readonly UInt32 WriteEnable;
public readonly UInt32 WriteData;
public ChannelAddr(UInt32 offset)
{
this.Offset = offset;
this.WaveSelect = Base + Offset + 0x00;
this.FreqCtrl = new UInt32[]
{
Base + offset + 0x01,
Base + offset + 0x02,
Base + offset + 0x03,
Base + offset + 0x04
};
this.PhaseCtrl = new UInt32[] {
Base + Offset + 0x05,
Base + Offset + 0x06,
Base + Offset + 0x07,
Base + Offset + 0x08
};
this.WriteEnable = Base + Offset + 0x09;
this.WriteData = Base + Offset + 0x0A;
}
}
}
/// <summary>
/// [TODO:description]
/// </summary>
public class DDS
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000;
readonly int port;
readonly string address;
private IPEndPoint ep;
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="timeout">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public DDS(string address, int port, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
this.address = address;
this.port = port;
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
this.timeout = timeout;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetWaveNum(int channelNum, int waveNum)
{
if (channelNum < 0 || channelNum > 1) return new(new ArgumentException(
$"Channel number should be 0 ~ 1 instead of {channelNum}", nameof(channelNum)));
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].WaveSelect, (UInt32)waveNum, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="step">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetFreq(int channelNum, int waveNum, UInt32 step)
{
if (channelNum < 0 || channelNum > 1) return new(new ArgumentException(
$"Channel number should be 0 ~ 1 instead of {channelNum}", nameof(channelNum)));
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].FreqCtrl[waveNum], step, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="channelNum">[TODO:parameter]</param>
/// <param name="waveNum">[TODO:parameter]</param>
/// <param name="phase">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<bool>> SetPhase(int channelNum, int waveNum, int phase)
{
if (channelNum < 0 || channelNum > 1) return new(new ArgumentException(
$"Channel number should be 0 ~ 1 instead of {channelNum}", nameof(channelNum)));
if (waveNum < 0 || waveNum > 3) return new(new ArgumentException(
$"Wave number should be 0 ~ 3 instead of {waveNum}", nameof(waveNum)));
if (phase < 0 || phase > 4096) return new(new ArgumentException(
$"Phase should be 0 ~ 4096 instead of {phase}", nameof(phase)));
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
var ret = await UDPClientPool.WriteAddr(
this.ep, DDSAddr.Channel[channelNum].PhaseCtrl[waveNum], (UInt32)phase, this.timeout);
if (!ret.IsSuccessful)
return new(ret.Error);
return ret.Value;
}
}

View File

@ -1,5 +1,6 @@
using System.Collections; using System.Collections;
using System.Net; using System.Net;
using BsdlParser;
using DotNext; using DotNext;
using Newtonsoft.Json; using Newtonsoft.Json;
using WebProtocol; using WebProtocol;
@ -27,6 +28,10 @@ public static class JtagAddr
/// Jtag Write Command /// Jtag Write Command
/// </summary> /// </summary>
public const UInt32 WRITE_CMD = 0x10_00_00_03; public const UInt32 WRITE_CMD = 0x10_00_00_03;
/// <summary>
/// Jtag Speed Control
/// </summary>
public const UInt32 SPEED_CTRL = 0x10_00_00_04;
} }
/// <summary> /// <summary>
@ -773,12 +778,12 @@ public class Jtag
/// <summary> /// <summary>
/// [TODO:description] /// [TODO:description]
/// </summary> /// </summary>
/// <param name="portNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns> /// <returns>[TODO:return]</returns>
public async ValueTask<Result<BitArray>> BoundaryScan(int portNum) public async ValueTask<Result<BitArray>> BoundaryScan()
{ {
if (portNum <= 0) var paser = new BsdlParser.Parser();
return new(new ArgumentException("The number of port couldn't be negative", nameof(portNum))); var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}");
// Clear Data // Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address); await MsgBus.UDPServer.ClearUDPData(this.address);
@ -810,6 +815,48 @@ public class Jtag
var byteArray = Common.Number.UInt32ArrayToBytes(retData.Value); var byteArray = Common.Number.UInt32ArrayToBytes(retData.Value);
if (!byteArray.IsSuccessful) return new(byteArray.Error); if (!byteArray.IsSuccessful) return new(byteArray.Error);
return new BitArray(byteArray.Value);
var bitArray = new BitArray(byteArray.Value);
if (bitArray is null)
return new(new Exception($"Convert to BitArray failed"));
bitArray.Length = portNum;
return bitArray;
}
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<Dictionary<string, bool>>> BoundaryScanLogicalPorts()
{
var bitArray = await BoundaryScan();
if (!bitArray.IsSuccessful) return new(bitArray.Error);
var paser = new BsdlParser.Parser();
var cellList = paser.GetBoundaryLogicalPorts();
if (cellList.IsNull) return new(new Exception("Get boundary logical ports failed"));
var portStatus = new Dictionary<string, bool>();
foreach (var cell in cellList.Value)
{
portStatus.Add(cell.PortID ?? "UnknownPortID", bitArray.Value[cell.CellNumber]);
}
return portStatus;
}
public async ValueTask<Result<bool>> SetSpeed(UInt32 speed)
{
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace($"Clear up udp server {this.address} receive data");
var ret = await WriteFIFO(
JtagAddr.SPEED_CTRL, (speed << 16) | speed,
JtagState.CMD_EXEC_FINISH, JtagState.CMD_EXEC_FINISH);
if (!ret.IsSuccessful) return new (ret.Error);
return ret.Value;
} }
} }

View File

@ -1,8 +1,8 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
namespace RemoteUpdate; namespace RemoteUpdateClient;
static class RemoteUpdateClientAddr static class RemoteUpdaterAddr
{ {
public const UInt32 Base = 0x20_00_00_00; public const UInt32 Base = 0x20_00_00_00;
@ -91,7 +91,7 @@ static class FlashAddr
/// <summary> /// <summary>
/// [TODO:description] /// [TODO:description]
/// </summary> /// </summary>
public class RemoteUpdateClient public class RemoteUpdater
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@ -112,7 +112,7 @@ public class RemoteUpdateClient
/// <param name="timeout">[TODO:parameter]</param> /// <param name="timeout">[TODO:parameter]</param>
/// <param name="timeoutForWait">[TODO:parameter]</param> /// <param name="timeoutForWait">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns> /// <returns>[TODO:return]</returns>
public RemoteUpdateClient(string address, int port, int timeout = 2000, int timeoutForWait = 60 * 1000) public RemoteUpdater(string address, int port, int timeout = 2000, int timeoutForWait = 60 * 1000)
{ {
if (timeout < 0) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@ -142,7 +142,7 @@ public class RemoteUpdateClient
{ {
var ret = await UDPClientPool.WriteAddr( var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.WriteCtrl, this.ep, RemoteUpdaterAddr.WriteCtrl,
Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout); Convert.ToUInt32((writeSectorNum << 16) | (1 << 15) | Convert.ToInt32(flashAddr / 4096)), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Enable write flash failed")); if (!ret.Value) return new(new Exception("Enable write flash failed"));
@ -150,7 +150,7 @@ public class RemoteUpdateClient
{ {
var ret = await UDPClientPool.ReadAddrWithWait( var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.WriteSign, this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait); 0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception( if (!ret.Value) return new(new Exception(
@ -158,14 +158,14 @@ public class RemoteUpdateClient
} }
{ {
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdateClientAddr.WriteFIFO, bytesData, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.WriteFIFO, bytesData, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Send data to flash failed")); if (!ret.Value) return new(new Exception("Send data to flash failed"));
} }
{ {
var ret = await UDPClientPool.ReadAddrWithWait( var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.WriteSign, this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait); 0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value; return ret.Value;
@ -314,14 +314,14 @@ public class RemoteUpdateClient
private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum) private async ValueTask<Result<bool>> CheckBitstreamCRC(int bitstreamNum, int bitstreamLen, UInt32 checkSum)
{ {
{ {
var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdateClientAddr.ReadCtrl2, 0x00_00_00_00, this.timeout); var ret = await UDPClientPool.WriteAddr(this.ep, RemoteUpdaterAddr.ReadCtrl2, 0x00_00_00_00, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Write read control 2 failed")); if (!ret.Value) return new(new Exception("Write read control 2 failed"));
} }
{ {
var ret = await UDPClientPool.WriteAddr( var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.ReadCtrl1, this.ep, RemoteUpdaterAddr.ReadCtrl1,
Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)), Convert.ToUInt32((bitstreamLen << 16) | (1 << 15) | Convert.ToInt32(FlashAddr.Bitstream[bitstreamNum] / 4096)),
this.timeout); this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
@ -330,7 +330,7 @@ public class RemoteUpdateClient
{ {
var ret = await UDPClientPool.ReadAddrWithWait( var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.ReadSign, this.ep, RemoteUpdaterAddr.ReadSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait); 0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception( if (!ret.Value) return new(new Exception(
@ -338,7 +338,7 @@ public class RemoteUpdateClient
} }
{ {
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdateClientAddr.ReadCRC, this.timeout); var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.ReadCRC, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
var bytes = ret.Value.Options.Data; var bytes = ret.Value.Options.Data;
@ -368,7 +368,7 @@ public class RemoteUpdateClient
$"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum))); $"Bitsteam num should be 0 ~ 3 for HotRest, but given {bitstreamNum}", nameof(bitstreamNum)));
var ret = await UDPClientPool.WriteAddr( var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.HotResetCtrl, this.ep, RemoteUpdaterAddr.HotResetCtrl,
((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout); ((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error); if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value; return ret.Value;
@ -532,4 +532,23 @@ public class RemoteUpdateClient
} }
} }
/// <summary>
/// [TODO:description]
/// </summary>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<UInt32>> GetVersion()
{
await MsgBus.UDPServer.ClearUDPData(this.address);
logger.Trace("Clear udp data finished");
{
var ret = await UDPClientPool.ReadAddr(this.ep, RemoteUpdaterAddr.Version, this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
var retData = ret.Value.Options.Data;
if (retData is null || retData.Length != 4) return new(new Exception("Failed to read remote update firmware version"));
var version = Common.Number.BytesToUInt32(retData);
return version;
}
}
} }

File diff suppressed because it is too large Load Diff

22
src/Common.ts Normal file
View File

@ -0,0 +1,22 @@
import { type FileParameter } from "./APIClient";
import { isNull, isUndefined } from "lodash";
export namespace Common {
export function toFileParameter(object: File): FileParameter {
if (isNull(object) || isUndefined(object))
throw new Error("File is Null or Undefined");
return {
data: object,
fileName: object.name
}
}
export function toFileParameterOrNull(object?: File | null): FileParameter | null {
if (isNull(object) || isUndefined(object)) return null;
else return {
data: object,
fileName: object.name
}
}
}

View File

@ -5,10 +5,13 @@
<input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" /> <input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" />
<div class="drawer-side"> <div class="drawer-side">
<label for="component-drawer" aria-label="close sidebar" class="drawer-overlay !bg-opacity-50"></label> <label for="component-drawer" aria-label="close sidebar" class="drawer-overlay !bg-opacity-50"></label>
<div class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col"> <!-- 菜单头部 --> <div class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col">
<!-- 菜单头部 -->
<div class="p-6 border-b border-base-300 flex justify-between items-center"> <div class="p-6 border-b border-base-300 flex justify-between items-center">
<h3 class="text-xl font-bold text-primary flex items-center gap-2"> <h3 class="text-xl font-bold text-primary flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="text-primary">
<circle cx="12" cy="12" r="10"></circle> <circle cx="12" cy="12" r="10"></circle>
<path d="M12 8v8"></path> <path d="M12 8v8"></path>
<path d="M8 12h8"></path> <path d="M8 12h8"></path>
@ -16,7 +19,8 @@
添加元器件 添加元器件
</h3> </h3>
<label for="component-drawer" class="btn btn-ghost btn-sm btn-circle" @click="closeMenu"> <label for="component-drawer" class="btn btn-ghost btn-sm btn-circle" @click="closeMenu">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line> <line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line>
</svg> </svg>
@ -25,7 +29,8 @@
<!-- 导航栏 --> <!-- 导航栏 -->
<div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box"> <div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
<a class="tab" :class="{ 'tab-active': activeTab === 'components' }" @click="activeTab = 'components'">元器件</a> <a class="tab" :class="{ 'tab-active': activeTab === 'components' }"
@click="activeTab = 'components'">元器件</a>
<a class="tab" :class="{ 'tab-active': activeTab === 'templates' }" @click="activeTab = 'templates'">模板</a> <a class="tab" :class="{ 'tab-active': activeTab === 'templates' }" @click="activeTab = 'templates'">模板</a>
<a class="tab" :class="{ 'tab-active': activeTab === 'virtual' }" @click="activeTab = 'virtual'">虚拟外设</a> <a class="tab" :class="{ 'tab-active': activeTab === 'virtual' }" @click="activeTab = 'virtual'">虚拟外设</a>
</div> </div>
@ -34,19 +39,18 @@
<div class="px-6 py-4 border-b border-base-300"> <div class="px-6 py-4 border-b border-base-300">
<div class="join w-full"> <div class="join w-full">
<div class="join-item flex-1 relative"> <div class="join-item flex-1 relative">
<input <input type="text" placeholder="搜索..." class="input input-bordered input-sm w-full pl-10"
type="text" v-model="searchQuery" @keyup.enter="searchComponents" />
placeholder="搜索..." <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
class="input input-bordered input-sm w-full pl-10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
v-model="searchQuery" class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60">
@keyup.enter="searchComponents"
/>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60">
<circle cx="11" cy="11" r="8"></circle> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg> </svg>
</div> </div>
<button class="btn btn-sm join-item" @click="searchComponents">搜索</button> <button class="btn btn-sm join-item" @click="searchComponents">
搜索
</button>
</div> </div>
</div> </div>
@ -57,14 +61,11 @@
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer" class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(component)"> @click="addComponent(component)">
<div class="card-body p-3 items-center text-center"> <div class="card-body p-3 items-center text-center">
<div class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"> <div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
<!-- 直接使用组件作为预览 --> <!-- 直接使用组件作为预览 -->
<component <component v-if="componentModules[component.type]" :is="componentModules[component.type].default"
v-if="componentModules[component.type]" class="component-preview" :size="getPreviewSize(component.type)" />
:is="componentModules[component.type].default"
class="component-preview"
:size="getPreviewSize(component.type)"
/>
<!-- 加载中状态 --> <!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span> <span v-else class="text-xs text-gray-400">加载中...</span>
</div> </div>
@ -75,7 +76,9 @@
</div> </div>
<!-- 无搜索结果 --> <!-- 无搜索结果 -->
<div v-else class="py-16 text-center"> <div v-else class="py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto text-base-300 mb-3"> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="mx-auto text-base-300 mb-3">
<circle cx="11" cy="11" r="8"></circle> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line> <line x1="8" y1="11" x2="14" y2="11"></line>
@ -94,17 +97,23 @@
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer" class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addTemplate(template)"> @click="addTemplate(template)">
<div class="card-body p-3 items-center text-center"> <div class="card-body p-3 items-center text-center">
<div class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"> <div
<img :src="template.thumbnailUrl || '/placeholder-template.png'" alt="Template thumbnail" class="max-h-full max-w-full object-contain" /> class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
<img :src="template.thumbnailUrl || '/placeholder-template.png'
" alt="Template thumbnail" class="max-h-full max-w-full object-contain" />
</div> </div>
<h3 class="card-title text-sm mt-2">{{ template.name }}</h3> <h3 class="card-title text-sm mt-2">{{ template.name }}</h3>
<p class="text-xs opacity-70">{{ template.description || '模板' }}</p> <p class="text-xs opacity-70">
{{ template.description || "模板" }}
</p>
</div> </div>
</div> </div>
</div> </div>
<!-- 无搜索结果 --> <!-- 无搜索结果 -->
<div v-else class="py-16 text-center"> <div v-else class="py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto text-base-300 mb-3"> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="mx-auto text-base-300 mb-3">
<circle cx="11" cy="11" r="8"></circle> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line> <line x1="8" y1="11" x2="14" y2="11"></line>
@ -122,14 +131,11 @@
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer" class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(device)"> @click="addComponent(device)">
<div class="card-body p-3 items-center text-center"> <div class="card-body p-3 items-center text-center">
<div class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"> <div
class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
<!-- 直接使用组件作为预览 --> <!-- 直接使用组件作为预览 -->
<component <component v-if="componentModules[device.type]" :is="componentModules[device.type].default"
v-if="componentModules[device.type]" class="component-preview" :size="getPreviewSize(device.type)" />
:is="componentModules[device.type].default"
class="component-preview"
:size="getPreviewSize(device.type)"
/>
<!-- 加载中状态 --> <!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span> <span v-else class="text-xs text-gray-400">加载中...</span>
</div> </div>
@ -140,7 +146,9 @@
</div> </div>
<!-- 无搜索结果 --> <!-- 无搜索结果 -->
<div v-else class="py-16 text-center"> <div v-else class="py-16 text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto text-base-300 mb-3"> <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"
class="mx-auto text-base-300 mb-3">
<circle cx="11" cy="11" r="8"></circle> <circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line> <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="8" y1="11" x2="14" y2="11"></line> <line x1="8" y1="11" x2="14" y2="11"></line>
@ -155,7 +163,8 @@
<!-- 底部操作区 --> <!-- 底部操作区 -->
<div class="p-4 border-t border-base-300 bg-base-200 flex justify-between"> <div class="p-4 border-t border-base-300 bg-base-200 flex justify-between">
<label for="component-drawer" class="btn btn-sm btn-ghost" @click="closeMenu"> <label for="component-drawer" class="btn btn-sm btn-ghost" @click="closeMenu">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1">
<path d="M19 12H5M12 19l-7-7 7-7"></path> <path d="M19 12H5M12 19l-7-7 7-7"></path>
</svg> </svg>
返回 返回
@ -171,7 +180,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, shallowRef, onMounted } from 'vue'; import { ref, computed, shallowRef, onMounted } from "vue";
import motherboardSvg from "../components/equipments/svg/motherboard.svg";
// Props // Props
interface Props { interface Props {
@ -181,51 +191,54 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
// //
const emit = defineEmits(['close', 'add-component', 'add-template', 'update:open']); const emit = defineEmits([
"close",
"add-component",
"add-template",
"update:open",
]);
// //
const activeTab = ref('components'); const activeTab = ref("components");
// --- --- // --- ---
const searchQuery = ref(''); const searchQuery = ref("");
// --- --- // --- ---
const availableComponents = [ const availableComponents = [
{ type: 'MechanicalButton', name: '机械按钮' }, { type: "MechanicalButton", name: "机械按钮" },
{ type: 'Switch', name: '开关' }, { type: "Switch", name: "开关" },
{ type: 'Pin', name: '引脚' }, { type: "Pin", name: "引脚" },
{ type: 'SMT_LED', name: '贴片LED' }, { type: "SMT_LED", name: "贴片LED" },
{ type: 'SevenSegmentDisplay', name: '数码管' }, { type: "SevenSegmentDisplay", name: "数码管" },
{ type: 'HDMI', name: 'HDMI接口' }, { type: "HDMI", name: "HDMI接口" },
{ type: 'DDR', name: 'DDR内存' }, { type: "DDR", name: "DDR内存" },
{ type: 'ETH', name: '以太网接口' }, { type: "ETH", name: "以太网接口" },
{ type: 'SD', name: 'SD卡插槽' }, { type: "SD", name: "SD卡插槽" },
{ type: 'SFP', name: 'SFP光纤模块' }, { type: "SFP", name: "SFP光纤模块" },
{ type: 'SMA', name: 'SMA连接器' }, { type: "SMA", name: "SMA连接器" },
{ type: 'MotherBoard', name: '主板' }, { type: "MotherBoard", name: "主板" },
{ type: 'PG2L100H_FBG676', name: 'PG2L100H FBG676芯片' } { type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
]; ];
// --- --- // --- ---
const availableVirtualDevices = [ const availableVirtualDevices = [{ type: "DDS", name: "信号发生器" }];
{ type: 'DDS', name: '信号发生器' }
];
// --- --- // --- ---
const availableTemplates = ref([ const availableTemplates = ref([
{ {
name: 'PG2L100H 基础开发板', name: "PG2L100H 基础开发板",
id: 'PG2L100H_Pango100pro', id: "PG2L100H_Pango100pro",
description: '包含主板和两个LED的基本设置', description: "包含主板和两个LED的基本设置",
path: '/src/components/equipments/templates/PG2L100H_Pango100pro.json', path: "/public/EquipmentTemplates/PG2L100H_Pango100pro.json",
thumbnailUrl: '/src/components/equipments/svg/motherboard.svg' thumbnailUrl: motherboardSvg,
} },
]); ]);
// / // /
const showComponentsMenu = computed({ const showComponentsMenu = computed({
get: () => props.open, get: () => props.open,
set: (value) => emit('update:open', value) set: (value) => emit("update:open", value),
}); });
// //
@ -241,7 +254,7 @@ async function loadComponentModule(type: string) {
// //
componentModules.value = { componentModules.value = {
...componentModules.value, ...componentModules.value,
[type]: module [type]: module,
}; };
console.log(`Loaded module for ${type}:`, module); console.log(`Loaded module for ${type}:`, module);
@ -278,19 +291,19 @@ async function preloadComponentModules() {
function getPreviewSize(componentType: string): number { function getPreviewSize(componentType: string): number {
// //
const previewSizes: Record<string, number> = { const previewSizes: Record<string, number> = {
'MechanicalButton': 0.4, // MechanicalButton: 0.4, //
'Switch': 0.35, // Switch: 0.35, //
'Pin': 0.8, // Pin: 0.8, //
'SMT_LED': 0.7, // LED SMT_LED: 0.7, // LED
'SevenSegmentDisplay': 0.4, // SevenSegmentDisplay: 0.4, //
'HDMI': 0.5, // HDMI HDMI: 0.5, // HDMI
'DDR': 0.5, // DDR DDR: 0.5, // DDR
'ETH': 0.5, // ETH: 0.5, //
'SD': 0.6, // SD SD: 0.6, // SD
'SFP': 0.4, // SFP SFP: 0.4, // SFP
'SMA': 0.7, // SMA SMA: 0.7, // SMA
'MotherBoard': 0.13, // MotherBoard: 0.13, //
'DDS': 0.3 // DDS: 0.3, //
}; };
// 0.5 // 0.5
@ -306,7 +319,7 @@ function searchComponents() {
// //
function closeMenu() { function closeMenu() {
showComponentsMenu.value = false; showComponentsMenu.value = false;
emit('close'); emit("close");
} }
// //
@ -316,23 +329,26 @@ async function addComponent(componentTemplate: { type: string; name: string }) {
let defaultProps: Record<string, any> = {}; let defaultProps: Record<string, any> = {};
// getDefaultProps // getDefaultProps
if(moduleRef){ if (moduleRef) {
if (typeof moduleRef.getDefaultProps === 'function') { if (typeof moduleRef.getDefaultProps === "function") {
defaultProps = moduleRef.getDefaultProps(); defaultProps = moduleRef.getDefaultProps();
console.log(`Got default props from ${componentTemplate.type}:`, defaultProps); console.log(
`Got default props from ${componentTemplate.type}:`,
defaultProps,
);
} else { } else {
// 退 // 退
console.log(`No getDefaultProps found for ${componentTemplate.type}`); console.log(`No getDefaultProps found for ${componentTemplate.type}`);
} }
} else{ } else {
console.log(`Failed to load module for ${componentTemplate.type}`); console.log(`Failed to load module for ${componentTemplate.type}`);
} }
// //
emit('add-component', { emit("add-component", {
type: componentTemplate.type, type: componentTemplate.type,
name: componentTemplate.name, name: componentTemplate.name,
props: defaultProps props: defaultProps,
}); });
// //
@ -349,56 +365,60 @@ async function addTemplate(template: any) {
} }
const templateData = await response.json(); const templateData = await response.json();
console.log('加载模板:', templateData); console.log("加载模板:", templateData);
// //
emit('add-template', { emit("add-template", {
id: template.id, id: template.id,
name: template.name, name: template.name,
template: templateData template: templateData,
}); });
// //
closeMenu(); closeMenu();
} catch (error) { } catch (error) {
console.error('加载模板出错:', error); console.error("加载模板出错:", error);
alert('无法加载模板文件,请检查控制台错误信息'); alert("无法加载模板文件,请检查控制台错误信息");
} }
} }
// () // ()
const filteredComponents = computed(() => { const filteredComponents = computed(() => {
if (!searchQuery.value || activeTab.value !== 'components') { if (!searchQuery.value || activeTab.value !== "components") {
return availableComponents; return availableComponents;
} }
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
return availableComponents.filter(component => return availableComponents.filter(
(component) =>
component.name.toLowerCase().includes(query) || component.name.toLowerCase().includes(query) ||
component.type.toLowerCase().includes(query) component.type.toLowerCase().includes(query),
); );
}); });
// () // ()
const filteredTemplates = computed(() => { const filteredTemplates = computed(() => {
if (!searchQuery.value || activeTab.value !== 'templates') { if (!searchQuery.value || activeTab.value !== "templates") {
return availableTemplates.value; return availableTemplates.value;
} }
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
return availableTemplates.value.filter(template => return availableTemplates.value.filter(
(template) =>
template.name.toLowerCase().includes(query) || template.name.toLowerCase().includes(query) ||
(template.description && template.description.toLowerCase().includes(query)) (template.description &&
template.description.toLowerCase().includes(query)),
); );
}); });
// () // ()
const filteredVirtualDevices = computed(() => { const filteredVirtualDevices = computed(() => {
if (!searchQuery.value || activeTab.value !== 'virtual') { if (!searchQuery.value || activeTab.value !== "virtual") {
return availableVirtualDevices; return availableVirtualDevices;
} }
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
return availableVirtualDevices.filter(device => return availableVirtualDevices.filter(
(device) =>
device.name.toLowerCase().includes(query) || device.name.toLowerCase().includes(query) ||
device.type.toLowerCase().includes(query) device.type.toLowerCase().includes(query),
); );
}); });
@ -427,6 +447,7 @@ onMounted(() => {
opacity: 0; opacity: 0;
transform: translateY(20px); transform: translateY(20px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
import type { InjectionKey, Ref } from "vue";
export const CanvasCurrentSelectedComponentID = Symbol() as InjectionKey<Ref<string | null>>

View File

@ -152,7 +152,7 @@ const emit = defineEmits<{
}>(); }>();
// //
const generalPropsExpanded = ref(true); const generalPropsExpanded = ref(false);
const componentPropsExpanded = ref(true); const componentPropsExpanded = ref(true);
// //

View File

@ -44,19 +44,13 @@
</CollapsibleSection> </CollapsibleSection>
<CollapsibleSection title="组件功能" v-model:isExpanded="componentCapsExpanded" status="default" class="mt-4"> <CollapsibleSection title="组件功能" v-model:isExpanded="componentCapsExpanded" status="default" class="mt-4">
<div v-if="componentData && componentData.type"> <div id="ComponentCapabilities" ref="ComponentCapabilities"></div>
<component <div v-if="!(componentData && componentData.type)" class="text-gray-400">
v-if="capabilityComponent"
:is="capabilityComponent"
v-bind="componentData.attrs"
/>
<div v-else class="text-gray-400">
该组件没有提供特殊功能
</div>
</div>
<div v-else class="text-gray-400">
选择元件以查看其功能 选择元件以查看其功能
</div> </div>
<div v-else-if="!componentCaps?.hasChildNodes()" class="text-gray-400">
该组件没有提供特殊功能
</div>
</CollapsibleSection> </CollapsibleSection>
<!-- 未来可以在这里添加更多的分区 --> <!-- 未来可以在这里添加更多的分区 -->
@ -79,9 +73,12 @@ import { type PropertyConfig } from "@/components/equipments/componentConfig"; /
import CollapsibleSection from "./CollapsibleSection.vue"; // import CollapsibleSection from "./CollapsibleSection.vue"; //
import PropertyEditor from "./PropertyEditor.vue"; // import PropertyEditor from "./PropertyEditor.vue"; //
import DDSPropertyEditor from "./equipments/DDSPropertyEditor.vue"; // DDS import DDSPropertyEditor from "./equipments/DDSPropertyEditor.vue"; // DDS
import { ref, computed, watch, shallowRef, markRaw, h, createApp } from "vue"; // VueAPI import {
import { isNull, isUndefined } from "lodash"; ref,
import type { JSX } from "vue/jsx-runtime"; computed,
watch,
useTemplateRef,
} from "vue"; // VueAPI
// //
interface Pin { interface Pin {
@ -98,11 +95,13 @@ const props = defineProps<{
}>(); }>();
// //
const propertySectionExpanded = ref(true); // const propertySectionExpanded = ref(false); //
const pinsSectionExpanded = ref(false); // const pinsSectionExpanded = ref(false); //
const componentCapsExpanded = ref(true); // const componentCapsExpanded = ref(true); //
const wireSectionExpanded = ref(false); // 线 const wireSectionExpanded = ref(false); // 线
const componentCaps = useTemplateRef("ComponentCapabilities");
// DDS // DDS
const ddsProperties = ref({ const ddsProperties = ref({
frequency: 1000, // 1000Hz frequency: 1000, // 1000Hz
@ -215,116 +214,6 @@ function updateDDSProperties(newProperties: any) {
); );
} }
} }
//
const capabilityComponent = shallowRef(null);
//
async function getExposedCapabilities(componentType: string) {
try {
//
const module = await import(`./equipments/${componentType}.vue`);
const Component = module.default;
// div
const tempDiv = document.createElement('div');
//
let exposedMethods: any = null;
const app = createApp({
render() {
return h(Component, {
ref: (el: any) => {
if (el) {
//
exposedMethods = el;
}
}
});
}
});
//
const vm = app.mount(tempDiv);
//
await new Promise(resolve => setTimeout(resolve, 0));
// getCapabilities
if (exposedMethods && typeof exposedMethods.getCapabilities === 'function') {
//
const CapabilityComponent = exposedMethods.getCapabilities();
// DOM
app.unmount();
tempDiv.remove();
return CapabilityComponent;
}
// DOM
app.unmount();
tempDiv.remove();
return null;
} catch (error) {
console.error(`获取${componentType}能力页面失败:`, error);
return null;
}
}
//
watch(
() => props.componentData,
async (newComponentData) => {
if (newComponentData && newComponentData.type) {
try {
//
const capsComponent = await getExposedCapabilities(newComponentData.type);
if (capsComponent) {
capabilityComponent.value = markRaw(capsComponent);
console.log(`已从实例加载${newComponentData.type}组件的能力页面`);
return;
}
// 退
const module = await import(`./equipments/${newComponentData.type}.vue`);
if (
(module.default && typeof module.default.getCapabilities === "function") ||
typeof module.getCapabilities === "function"
) {
const getCapsFn =
typeof module.getCapabilities === "function"
? module.getCapabilities
: module.default.getCapabilities;
const moduleCapComponent = getCapsFn();
if (moduleCapComponent) {
capabilityComponent.value = markRaw(moduleCapComponent);
console.log(`已从模块加载${newComponentData.type}组件的能力页面`);
return;
}
}
capabilityComponent.value = null;
console.log(`组件${newComponentData.type}没有提供getCapabilities方法`);
} catch (error) {
console.error(`加载组件${newComponentData.type}能力页面失败:`, error);
capabilityComponent.value = null;
}
} else {
capabilityComponent.value = null;
}
},
{ immediate: true }
);
// hasComponentCaps
const hasComponentCaps = computed(() => {
return capabilityComponent.value !== null;
});
</script> </script>
<style scoped> <style scoped>

View File

@ -6,30 +6,34 @@
<!-- Input File --> <!-- Input File -->
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend> <legend class="fieldset-legend text-sm">选择或拖拽上传文件</legend>
<input type="file" class="file-input w-full" :value="fileInput" @change="handleFileChange" /> <input type="file" ref="fileInput" class="file-input w-full" @change="handleFileChange" />
<label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label> <label class="fieldset-label">文件最大容量: {{ maxMemory }}MB</label>
</fieldset> </fieldset>
<!-- Upload Button --> <!-- Upload Button -->
<div class="card-actions w-full"> <div class="card-actions w-full">
<button @click="handleClick" class="btn btn-primary grow"> <button @click="handleClick" class="btn btn-primary grow" :disabled="isUploading">
<div v-if="isUploading">
<span class="loading loading-spinner"></span>
下载中...
</div>
<div v-else>
{{ buttonText }} {{ buttonText }}
</div>
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { computed, ref, useTemplateRef, onMounted } from "vue";
import { useEquipments } from "@/stores/equipments";
import { useDialogStore } from "@/stores/dialog"; import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
interface Props { interface Props {
uploadEvent?: Function; uploadEvent?: (file: File) => Promise<boolean>;
downloadEvent?: Function; downloadEvent?: () => Promise<boolean>;
maxMemory?: number; maxMemory?: number;
defaultFile?: string;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -41,16 +45,23 @@ const emits = defineEmits<{
}>(); }>();
const dialog = useDialogStore(); const dialog = useDialogStore();
const eqps = useEquipments();
const isUploading = ref(false);
const buttonText = computed(() => { const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载"; return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
}); });
// var bitstream: File | null = null; const fileInput = useTemplateRef("fileInput");
const bitstream = defineModel<File | undefined>(); const bitstream = defineModel("bitstreamFile", {
const fileInput = computed(() => { type: File,
return !isUndefined(bitstream.value) ? bitstream.value.name : ""; default: undefined,
});
onMounted(() => {
if (!isUndefined(bitstream.value) && !isNull(fileInput.value)) {
let fileList = new DataTransfer();
fileList.items.add(bitstream.value);
fileInput.value.files = fileList.files;
}
}); });
function handleFileChange(event: Event): void { function handleFileChange(event: Event): void {
@ -85,9 +96,9 @@ async function handleClick(event: Event): Promise<void> {
return; return;
} }
// Upload - bitstream.valuebitstream isUploading.value = true;
try { try {
const ret = await props.uploadEvent(event, bitstream.value); const ret = await props.uploadEvent(bitstream.value);
if (isUndefined(props.downloadEvent)) { if (isUndefined(props.downloadEvent)) {
if (ret) { if (ret) {
dialog.info("上传成功"); dialog.info("上传成功");
@ -95,6 +106,10 @@ async function handleClick(event: Event): Promise<void> {
} else dialog.error("上传失败"); } else dialog.error("上传失败");
return; return;
} }
if (!ret) {
isUploading.value = false;
return;
}
} catch (e) { } catch (e) {
dialog.error("上传失败"); dialog.error("上传失败");
console.error(e); console.error(e);
@ -110,8 +125,9 @@ async function handleClick(event: Event): Promise<void> {
dialog.error("下载失败"); dialog.error("下载失败");
console.error(e); console.error(e);
} }
}
isUploading.value = false;
}
</script> </script>
<style scoped lang="postcss"> <style scoped lang="postcss">

View File

@ -1,5 +1,3 @@
import type { JSX } from "vue/jsx-runtime";
// 定义 diagram.json 的类型结构 // 定义 diagram.json 的类型结构
export interface DiagramData { export interface DiagramData {
version: number; version: number;
@ -17,7 +15,6 @@ export interface DiagramPart {
x: number; x: number;
y: number; y: number;
attrs: Record<string, any>; attrs: Record<string, any>;
capsPage?: JSX.Element;
rotate: number; rotate: number;
group: string; group: string;
positionlock: boolean; positionlock: boolean;

View File

@ -46,7 +46,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted, watchEffect } from 'vue';
import Pin from './Pin.vue'; import Pin from './Pin.vue';
// Pin // Pin
@ -94,6 +94,10 @@ const currentWaveformIndex = ref(0);
const waveformNames = ['正弦波', '方波', '三角波', '锯齿波']; const waveformNames = ['正弦波', '方波', '三角波', '锯齿波'];
const waveforms = ['sine', 'square', 'triangle', 'sawtooth']; const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
watchEffect(()=>{
console.log(`DDS Porperties: ${frequency.value} ${timebase.value}`)
})
// //
interface WaveformFunction { interface WaveformFunction {
(x: number, width: number, height: number, phaseRad: number): number; (x: number, width: number, height: number, phaseRad: number): number;

View File

@ -1,4 +1,5 @@
<template> <div class="dds-property-editor"> <template>
<div class="dds-property-editor">
<CollapsibleSection title="信号发生器" :isExpanded="true"> <CollapsibleSection title="信号发生器" :isExpanded="true">
<div class="dds-editor-container"> <div class="dds-editor-container">
<div class="dds-display"> <div class="dds-display">
@ -9,27 +10,35 @@
<path :d="currentWaveformPath" stroke="lime" stroke-width="2" fill="none" /> <path :d="currentWaveformPath" stroke="lime" stroke-width="2" fill="none" />
<!-- 频率和相位显示 --> <!-- 频率和相位显示 -->
<text x="20" y="25" fill="#0f0" font-size="14">{{ displayFrequency }}</text> <text x="20" y="25" fill="#0f0" font-size="14">
<text x="200" y="25" fill="#0f0" font-size="14">φ: {{ phase }}°</text> {{ displayFrequency }}
<text x="150" y="110" fill="#0f0" font-size="14" text-anchor="middle">{{ displayTimebase }}</text> </text>
<text x="200" y="25" fill="#0f0" font-size="14">
φ: {{ phase }}°
</text>
<text x="150" y="110" fill="#0f0" font-size="14" text-anchor="middle">
{{ displayTimebase }}
</text>
</svg> </svg>
<!-- 时基控制 --> <!-- 时基控制 -->
<div class="timebase-controls"> <div class="timebase-controls">
<button class="timebase-button" @click="decreaseTimebase">-</button> <button class="timebase-button" @click="decreaseTimebase">
-
</button>
<span class="timebase-label">时基</span> <span class="timebase-label">时基</span>
<button class="timebase-button" @click="increaseTimebase">+</button> <button class="timebase-button" @click="increaseTimebase">
+
</button>
</div> </div>
</div> </div>
<!-- 波形选择区 --> <!-- 波形选择区 -->
<div class="waveform-selector"> <div class="waveform-selector">
<div <div v-for="(name, index) in waveformNames" :key="`wave-${index}`" :class="[
v-for="(name, index) in waveformNames" 'waveform-option',
:key="`wave-${index}`" { active: currentWaveformIndex === index },
:class="['waveform-option', { active: currentWaveformIndex === index }]" ]" @click="selectWaveform(index)">
@click="selectWaveform(index)"
>
{{ name }} {{ name }}
</div> </div>
</div> </div>
@ -39,15 +48,14 @@
<div class="control-group"> <div class="control-group">
<span class="control-label">频率:</span> <span class="control-label">频率:</span>
<div class="control-buttons"> <div class="control-buttons">
<button class="control-button" @click="decreaseFrequency">-</button> <button class="control-button" @click="decreaseFrequency">
<input -
v-model="frequencyInput" </button>
@blur="applyFrequencyInput" <input v-model="frequencyInput" @blur="applyFrequencyInput" @keyup.enter="applyFrequencyInput"
@keyup.enter="applyFrequencyInput" class="control-input" type="text" />
class="control-input" <button class="control-button" @click="increaseFrequency">
type="text" +
/> </button>
<button class="control-button" @click="increaseFrequency">+</button>
</div> </div>
</div> </div>
@ -55,13 +63,8 @@
<span class="control-label">相位:</span> <span class="control-label">相位:</span>
<div class="control-buttons"> <div class="control-buttons">
<button class="control-button" @click="decreasePhase">-</button> <button class="control-button" @click="decreasePhase">-</button>
<input <input v-model="phaseInput" @blur="applyPhaseInput" @keyup.enter="applyPhaseInput" class="control-input"
v-model="phaseInput" type="text" />
@blur="applyPhaseInput"
@keyup.enter="applyPhaseInput"
class="control-input"
type="text"
/>
<button class="control-button" @click="increasePhase">+</button> <button class="control-button" @click="increasePhase">+</button>
</div> </div>
</div> </div>
@ -69,53 +72,48 @@
<!-- 自定义波形输入 --> <!-- 自定义波形输入 -->
<div class="custom-waveform"> <div class="custom-waveform">
<div class="section-heading">自定义波形</div> <div class="input-group"> <div class="section-heading">自定义波形</div>
<div class="input-group">
<label class="input-label">函数表达式:</label> <label class="input-label">函数表达式:</label>
<input <input v-model="customWaveformExpression" class="function-input"
v-model="customWaveformExpression"
class="function-input"
placeholder="例如: sin(t) 或 x^(2/3)+0.9*sqrt(3.3-x^2)*sin(a*PI*x) [a=7.8]" placeholder="例如: sin(t) 或 x^(2/3)+0.9*sqrt(3.3-x^2)*sin(a*PI*x) [a=7.8]"
@keyup.enter="applyCustomWaveform" @keyup.enter="applyCustomWaveform" />
/> <button class="apply-button" @click="applyCustomWaveform">
<button class="apply-button" @click="applyCustomWaveform">应用</button> 应用
</button>
</div> </div>
<div class="example-functions"> <div class="example-functions">
<div class="example-label">示例函数:</div> <div class="example-label">示例函数:</div>
<div class="example-buttons"> <div class="example-buttons">
<button <button class="example-button" @click="applyExampleFunction('sin(t)')">
class="example-button" 正弦波
@click="applyExampleFunction('sin(t)')" </button>
>正弦波</button> <button class="example-button" @click="applyExampleFunction('sin(t)^3')">
<button 立方正弦
class="example-button" </button>
@click="applyExampleFunction('sin(t)^3')" <button class="example-button" @click="
>立方正弦</button> <button applyExampleFunction(
class="example-button" '((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75',
@click="applyExampleFunction('((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75')" )
>心形函数</button> ">
心形函数
</button>
</div> </div>
</div> </div>
<div class="drawing-area"> <div class="drawing-area">
<div class="section-heading">波形绘制</div> <div class="section-heading">波形绘制</div>
<div <div class="waveform-canvas-container" ref="canvasContainer">
class="waveform-canvas-container" <canvas ref="drawingCanvas" class="drawing-canvas" width="280" height="100" @mousedown="startDrawing"
ref="canvasContainer" @mousemove="draw" @mouseup="stopDrawing" @mouseleave="stopDrawing"></canvas>
>
<canvas
ref="drawingCanvas"
class="drawing-canvas"
width="280"
height="100"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
></canvas>
<div class="canvas-actions"> <div class="canvas-actions">
<button class="canvas-button" @click="clearCanvas">清除</button> <button class="canvas-button" @click="clearCanvas">
<button class="canvas-button" @click="applyDrawnWaveform">应用绘制</button> 清除
</button>
<button class="canvas-button" @click="applyDrawnWaveform">
应用绘制
</button>
</div> </div>
</div> </div>
</div> </div>
@ -125,22 +123,25 @@
<div class="saved-waveforms"> <div class="saved-waveforms">
<div class="section-heading">波形存储槽</div> <div class="section-heading">波形存储槽</div>
<div class="slot-container"> <div class="slot-container">
<div <div v-for="(slot, index) in waveformSlots" :key="`slot-${index}`"
v-for="(slot, index) in waveformSlots" :class="['waveform-slot', { empty: !slot.name }]" @click="loadWaveformSlot(index)">
:key="`slot-${index}`" <span class="slot-name">{{
:class="['waveform-slot', { empty: !slot.name }]" slot.name || `${index + 1}`
@click="loadWaveformSlot(index)" }}</span>
> <button class="save-button" @click.stop="saveCurrentToSlot(index)">
<span class="slot-name">{{ slot.name || `${index+1}` }}</span>
<button
class="save-button"
@click.stop="saveCurrentToSlot(index)"
>
保存 保存
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<button class="btn btn-primary text-primary-content w-full" :disabled="isApplying" @click="applyOutputWave">
<div v-if="isApplying">
<span class="loading loading-spinner"></span>
应用中...
</div>
<div v-else>应用输出波形</div>
</button>
</div> </div>
</div> </div>
</CollapsibleSection> </CollapsibleSection>
@ -148,22 +149,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from "vue";
import CollapsibleSection from '../CollapsibleSection.vue'; import CollapsibleSection from "../CollapsibleSection.vue";
import { DDSClient } from "@/APIClient";
import { useEquipments } from "@/stores/equipments";
import { useDialogStore } from "@/stores/dialog";
import { toInteger } from "lodash";
// Component Attributes
const props = defineProps<{ const props = defineProps<{
modelValue: any; modelValue: any;
}>(); }>();
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(["update:modelValue"]);
// Global varibles
const dds = new DDSClient();
const eqps = useEquipments();
const dialog = useDialogStore();
// //
const frequency = ref(props.modelValue?.frequency || 1000); const frequency = ref<number>(props.modelValue?.frequency || 1000);
const phase = ref(props.modelValue?.phase || 0); const phase = ref<number>(props.modelValue?.phase || 0);
const timebase = ref(props.modelValue?.timebase || 1); // 1 const timebase = ref(props.modelValue?.timebase || 1); // 1
const currentWaveformIndex = ref(0); const currentWaveformIndex = ref(0);
const waveformNames = ['正弦波', '方波', '三角波', '锯齿波', '自定义']; const waveformNames = ["正弦波", "方波", "三角波", "锯齿波", "自定义"];
const waveforms = ['sine', 'square', 'triangle', 'sawtooth', 'custom']; const waveforms = ["sine", "square", "triangle", "sawtooth", "custom"];
const isApplying = ref(false);
// //
interface WaveformFunction { interface WaveformFunction {
@ -176,45 +188,72 @@ interface WaveformFunctions {
const waveformFunctions: WaveformFunctions = { const waveformFunctions: WaveformFunctions = {
// : sin(2π*x + φ) // : sin(2π*x + φ)
sine: (x: number, width: number, height: number, phaseRad: number): number => { sine: (
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad); x: number,
width: number,
height: number,
phaseRad: number,
): number => {
return (height / 2) * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad);
}, },
// : // :
square: (x: number, width: number, height: number, phaseRad: number): number => { square: (
x: number,
width: number,
height: number,
phaseRad: number,
): number => {
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1; const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
return normX < 0.5 ? height/4 : -height/4; return normX < 0.5 ? height / 4 : -height / 4;
}, },
// : 线 // : 线
triangle: (x: number, width: number, height: number, phaseRad: number): number => { triangle: (
x: number,
width: number,
height: number,
phaseRad: number,
): number => {
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1; const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
return height/2 - height * Math.abs(2 * normX - 1); return height / 2 - height * Math.abs(2 * normX - 1);
}, },
// 齿: 线, // 齿: 线,
sawtooth: (x: number, width: number, height: number, phaseRad: number): number => { sawtooth: (
x: number,
width: number,
height: number,
phaseRad: number,
): number => {
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1; const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
return height/2 - height/2 * (2 * normX); return height / 2 - (height / 2) * (2 * normX);
}, },
// //
custom: (x: number, width: number, height: number, phaseRad: number): number => { custom: (
x: number,
width: number,
height: number,
phaseRad: number,
): number => {
return 0; // 0 return 0; // 0
} },
}; };
// //
const frequencyInput = ref(formatFrequency(frequency.value)); const frequencyInput = ref(formatFrequency(frequency.value));
const phaseInput = ref(phase.value.toString()); const phaseInput = ref(phase.value.toString());
const customWaveformExpression = ref(''); const customWaveformExpression = ref("");
// //
const waveformSlots = ref<{ name: string; type: string; data: number[][] | null }[]>([ const waveformSlots = ref<
{ name: '正弦波', type: 'sine', data: null }, { name: string; type: string; data: number[][] | null }[]
{ name: '方波', type: 'square', data: null }, >([
{ name: '三角波', type: 'triangle', data: null }, { name: "正弦波", type: "sine", data: null },
{ name: '', type: '', data: null } { name: "方波", type: "square", data: null },
{ name: "三角波", type: "triangle", data: null },
{ name: "锯齿波", type: "sawtooth", data: null },
]); ]);
// //
@ -283,7 +322,7 @@ const currentWaveformPath = computed(() => {
const xOffset = 0; const xOffset = 0;
const yOffset = 30; const yOffset = 30;
const currentWaveform = waveforms[currentWaveformIndex.value]; const currentWaveform = waveforms[currentWaveformIndex.value];
const phaseRadians = phase.value * Math.PI / 180; const phaseRadians = (phase.value * Math.PI) / 180;
// //
// - // -
@ -297,9 +336,9 @@ const currentWaveformPath = computed(() => {
// //
const scaleFactor = timebaseFactor * frequencyFactor; const scaleFactor = timebaseFactor * frequencyFactor;
let path = ''; let path = "";
// 使 // 使
if (currentWaveform === 'custom') { if (currentWaveform === "custom") {
// //
if (drawPoints.value.length > 0) { if (drawPoints.value.length > 0) {
path = `M${xOffset + drawPoints.value[0][0]},${yOffset + drawPoints.value[0][1]}`; path = `M${xOffset + drawPoints.value[0][0]},${yOffset + drawPoints.value[0][1]}`;
@ -308,32 +347,34 @@ const currentWaveformPath = computed(() => {
} }
} else { } else {
// 使 // 使
const waveFunction = waveformFunctions.custom; path = `M${xOffset},${yOffset + height/2}`; const waveFunction = waveformFunctions.custom;
path = `M${xOffset},${yOffset + height / 2}`;
for (let x = 0; x <= width; x++) { for (let x = 0; x <= width; x++) {
const scaledX = x * scaleFactor; const scaledX = x * scaleFactor;
const y = waveFunction(scaledX, width, height, phaseRadians); const y = waveFunction(scaledX, width, height, phaseRadians);
// undefined // undefined
if (typeof y === 'number' && isFinite(y)) { if (typeof y === "number" && isFinite(y)) {
path += ` L${x + xOffset},${yOffset + height/2 - y}`; path += ` L${x + xOffset},${yOffset + height / 2 - y}`;
} else { } else {
// //
path += ` L${x + xOffset},${yOffset + height/2}`; path += ` L${x + xOffset},${yOffset + height / 2}`;
} }
} }
} }
} else { } else {
// 使 // 使
const waveFunction = waveformFunctions[currentWaveform as keyof typeof waveformFunctions]; const waveFunction =
waveformFunctions[currentWaveform as keyof typeof waveformFunctions];
// //
path = `M${xOffset},${yOffset + height/2}`; path = `M${xOffset},${yOffset + height / 2}`;
for (let x = 0; x <= width; x++) { for (let x = 0; x <= width; x++) {
// - x // - x
const scaledX = x * scaleFactor; const scaledX = x * scaleFactor;
const y = waveFunction(scaledX, width, height, phaseRadians); const y = waveFunction(scaledX, width, height, phaseRadians);
path += ` L${x + xOffset},${yOffset + height/2 - y}`; path += ` L${x + xOffset},${yOffset + height / 2 - y}`;
} }
} }
@ -346,6 +387,56 @@ function selectWaveform(index: number) {
updateModelValue(); updateModelValue();
} }
async function applyOutputWave() {
try {
isApplying.value = true;
{
const ret = await dds.setWaveNum(
eqps.boardAddr,
eqps.boardPort,
0,
currentWaveformIndex.value,
);
if (!ret) {
dialog.error("应用失败");
}
}
{
const ret = await dds.setFreq(
eqps.boardAddr,
eqps.boardPort,
0,
currentWaveformIndex.value,
toInteger(frequency.value * Math.pow(2, 32 - 20)),
);
if (!ret) {
dialog.error("应用失败");
}
}
{
const ret = await dds.setPhase(
eqps.boardAddr,
eqps.boardPort,
0,
currentWaveformIndex.value,
toInteger((phase.value * 4096) / 360),
);
if (ret) {
dialog.info("应用成功");
} else {
dialog.error("应用失败");
}
}
} catch (e) {
dialog.error("应用失败");
console.error(e);
} finally {
isApplying.value = false;
}
}
function increaseFrequency() { function increaseFrequency() {
if (frequency.value < 10) { if (frequency.value < 10) {
frequency.value += 0.1; frequency.value += 0.1;
@ -390,11 +481,11 @@ function applyFrequencyInput() {
let value = parseFloat(frequencyInput.value); let value = parseFloat(frequencyInput.value);
// //
if (frequencyInput.value.includes('MHz')) { if (frequencyInput.value.includes("MHz")) {
value = parseFloat(frequencyInput.value) * 1000000; value = parseFloat(frequencyInput.value) * 1000000;
} else if (frequencyInput.value.includes('kHz')) { } else if (frequencyInput.value.includes("kHz")) {
value = parseFloat(frequencyInput.value) * 1000; value = parseFloat(frequencyInput.value) * 1000;
} else if (frequencyInput.value.includes('Hz')) { } else if (frequencyInput.value.includes("Hz")) {
value = parseFloat(frequencyInput.value); value = parseFloat(frequencyInput.value);
} }
@ -444,11 +535,11 @@ function applyCustomWaveform() {
try { try {
// //
createCustomWaveformFunction(); createCustomWaveformFunction();
currentWaveformIndex.value = waveforms.indexOf('custom'); currentWaveformIndex.value = waveforms.indexOf("custom");
drawCustomWaveformFromExpression(); drawCustomWaveformFromExpression();
updateModelValue(); updateModelValue();
} catch (error) { } catch (error) {
console.error('Invalid expression:', error); console.error("Invalid expression:", error);
// //
} }
} }
@ -466,23 +557,29 @@ function createCustomWaveformFunction() {
const expression = customWaveformExpression.value; const expression = customWaveformExpression.value;
// mathjs // mathjs
import('mathjs').then((math) => { import("mathjs")
.then((math) => {
try { try {
// //
const compiledExpression = math.compile(expression); const compiledExpression = math.compile(expression);
// //
waveformFunctions.custom = (x: number, width: number, height: number, phaseRad: number): number => { waveformFunctions.custom = (
x: number,
width: number,
height: number,
phaseRad: number,
): number => {
try { try {
// - x // - x
const phaseShift = phaseRad / (2 * Math.PI); const phaseShift = phaseRad / (2 * Math.PI);
// 使x 0-1 // 使x 0-1
let normalizedX = ((x / width) + phaseShift) % 1; let normalizedX = (x / width + phaseShift) % 1;
// x[-1.5,1.5] // x[-1.5,1.5]
// [-1,1] // [-1,1]
const scaledX = (normalizedX * 3) - 1.5; const scaledX = normalizedX * 3 - 1.5;
// 使 // 使
const scope = { const scope = {
@ -496,7 +593,11 @@ function createCustomWaveformFunction() {
let result = compiledExpression.evaluate(scope); let result = compiledExpression.evaluate(scope);
// //
if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) { if (
typeof result !== "number" ||
isNaN(result) ||
!isFinite(result)
) {
result = 0; result = 0;
} }
@ -505,9 +606,9 @@ function createCustomWaveformFunction() {
result = Math.max(-1, Math.min(1, result)); result = Math.max(-1, Math.min(1, result));
// //
return height/2 * result; return (height / 2) * result;
} catch (e) { } catch (e) {
console.error('Error evaluating expression:', e); console.error("Error evaluating expression:", e);
return 0; return 0;
} }
}; };
@ -515,23 +616,31 @@ function createCustomWaveformFunction() {
// //
updateModelValue(); updateModelValue();
} catch (parseError) { } catch (parseError) {
console.error('Error parsing expression:', parseError); console.error("Error parsing expression:", parseError);
// 使 // 使
waveformFunctions.custom = (x: number, width: number, height: number, phaseRad: number): number => { waveformFunctions.custom = (
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad); x: number,
width: number,
height: number,
phaseRad: number,
): number => {
return (
(height / 2) * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad)
);
}; };
} }
}).catch(error => { })
console.error('Error loading mathjs:', error); .catch((error) => {
console.error("Error loading mathjs:", error);
}); });
}; }
// //
function drawCustomWaveformFromExpression() { function drawCustomWaveformFromExpression() {
const canvas = drawingCanvas.value; const canvas = drawingCanvas.value;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
const width = canvas.width; const width = canvas.width;
@ -539,13 +648,13 @@ function drawCustomWaveformFromExpression() {
// //
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#0f0'; ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2; ctx.lineWidth = 2;
// - // -
createCustomWaveformFunction(); createCustomWaveformFunction();
// 使 // 使
const phaseRad = phase.value * Math.PI / 180; const phaseRad = (phase.value * Math.PI) / 180;
// //
// //
@ -615,7 +724,7 @@ function startDrawing(event: MouseEvent) {
const x = event.clientX - rect.left; const x = event.clientX - rect.left;
const y = event.clientY - rect.top; const y = event.clientY - rect.top;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
// //
@ -624,7 +733,7 @@ function startDrawing(event: MouseEvent) {
// //
drawPoints.value = [[x, y]]; drawPoints.value = [[x, y]];
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = '#0f0'; ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.moveTo(x, y); ctx.moveTo(x, y);
} }
@ -635,7 +744,7 @@ function draw(event: MouseEvent) {
const canvas = drawingCanvas.value; const canvas = drawingCanvas.value;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
@ -658,7 +767,7 @@ function clearCanvas() {
const canvas = drawingCanvas.value; const canvas = drawingCanvas.value;
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (!ctx) return; if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
@ -667,7 +776,7 @@ function clearCanvas() {
function applyDrawnWaveform() { function applyDrawnWaveform() {
if (drawPoints.value.length > 0) { if (drawPoints.value.length > 0) {
currentWaveformIndex.value = waveforms.indexOf('custom'); currentWaveformIndex.value = waveforms.indexOf("custom");
updateModelValue(); updateModelValue();
} }
} }
@ -677,9 +786,11 @@ function saveCurrentToSlot(index: number) {
waveformSlots.value[index] = { waveformSlots.value[index] = {
name: waveformNames[currentWaveformIndex.value], name: waveformNames[currentWaveformIndex.value],
type: waveforms[currentWaveformIndex.value], type: waveforms[currentWaveformIndex.value],
data: drawPoints.value.length > 0 && currentWaveformIndex.value === waveforms.indexOf('custom') data:
drawPoints.value.length > 0 &&
currentWaveformIndex.value === waveforms.indexOf("custom")
? [...drawPoints.value] ? [...drawPoints.value]
: null : null,
}; };
} }
@ -692,17 +803,17 @@ function loadWaveformSlot(index: number) {
currentWaveformIndex.value = waveformIndex; currentWaveformIndex.value = waveformIndex;
// //
if (slot.type === 'custom' && slot.data !== null && slot.data.length > 0) { if (slot.type === "custom" && slot.data !== null && slot.data.length > 0) {
drawPoints.value = [...slot.data]; drawPoints.value = [...slot.data];
// //
const canvas = drawingCanvas.value; const canvas = drawingCanvas.value;
if (canvas) { if (canvas) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = '#0f0'; ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2; ctx.lineWidth = 2;
for (let i = 0; i < slot.data.length; i++) { for (let i = 0; i < slot.data.length; i++) {
@ -730,9 +841,12 @@ function updateModelValue() {
phase: phase.value, phase: phase.value,
timebase: timebase.value, timebase: timebase.value,
waveform: waveforms[currentWaveformIndex.value], waveform: waveforms[currentWaveformIndex.value],
customWaveformPoints: currentWaveformIndex.value === waveforms.indexOf('custom') ? [...drawPoints.value] : [] customWaveformPoints:
currentWaveformIndex.value === waveforms.indexOf("custom")
? [...drawPoints.value]
: [],
}; };
emit('update:modelValue', newValue); emit("update:modelValue", newValue);
} }
// //
@ -761,17 +875,23 @@ onMounted(() => {
} }
// //
if (props.modelValue.customWaveformPoints && props.modelValue.customWaveformPoints.length > 0) { if (
props.modelValue.customWaveformPoints &&
props.modelValue.customWaveformPoints.length > 0
) {
drawPoints.value = [...props.modelValue.customWaveformPoints]; drawPoints.value = [...props.modelValue.customWaveformPoints];
// //
const canvas = drawingCanvas.value; const canvas = drawingCanvas.value;
if (canvas && currentWaveformIndex.value === waveforms.indexOf('custom')) { if (
const ctx = canvas.getContext('2d'); canvas &&
currentWaveformIndex.value === waveforms.indexOf("custom")
) {
const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = '#0f0'; ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2; ctx.lineWidth = 2;
for (let i = 0; i < drawPoints.value.length; i++) { for (let i = 0; i < drawPoints.value.length; i++) {
@ -790,8 +910,14 @@ onMounted(() => {
}); });
// model // model
watch(() => props.modelValue, (newVal) => { watch(
if (newVal && newVal.frequency !== undefined && newVal.frequency !== frequency.value) { () => props.modelValue,
(newVal) => {
if (
newVal &&
newVal.frequency !== undefined &&
newVal.frequency !== frequency.value
) {
frequency.value = newVal.frequency; frequency.value = newVal.frequency;
frequencyInput.value = formatFrequency(frequency.value); frequencyInput.value = formatFrequency(frequency.value);
} }
@ -801,7 +927,11 @@ watch(() => props.modelValue, (newVal) => {
phaseInput.value = phase.value.toString(); phaseInput.value = phase.value.toString();
} }
if (newVal && newVal.timebase !== undefined && newVal.timebase !== timebase.value) { if (
newVal &&
newVal.timebase !== undefined &&
newVal.timebase !== timebase.value
) {
timebase.value = newVal.timebase; timebase.value = newVal.timebase;
} }
@ -811,7 +941,9 @@ watch(() => props.modelValue, (newVal) => {
currentWaveformIndex.value = index; currentWaveformIndex.value = index;
} }
} }
}, { deep: true }); },
{ deep: true },
);
</script> </script>
<style scoped> <style scoped>
@ -1002,7 +1134,9 @@ watch(() => props.modelValue, (newVal) => {
border-radius: 4px; border-radius: 4px;
color: var(--base-content, #a6adbb); color: var(--base-content, #a6adbb);
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease; transition:
background-color 0.2s ease,
color 0.2s ease;
font-size: 0.8rem; font-size: 0.8rem;
} }

View File

@ -1,12 +1,11 @@
<template> <template>
<div class="button-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <div class="button-container" :style="{
<svg width: width + 'px',
xmlns="http://www.w3.org/2000/svg" height: height + 'px',
:width="width" position: 'relative',
:height="height" }">
viewBox="400 400 800 800" <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="400 400 800 800"
class="mechanical-button" class="mechanical-button">
>
<!-- defs 和按钮底座保持不变 --> <!-- defs 和按钮底座保持不变 -->
<defs> <defs>
<filter id="btn-shadow"> <filter id="btn-shadow">
@ -26,7 +25,8 @@
<stop stop-color="#171717" offset="0" /> <stop stop-color="#171717" offset="0" />
<stop stop-color="#4b4b4b" offset="1" /> <stop stop-color="#4b4b4b" offset="1" />
</linearGradient> </linearGradient>
</defs> <!-- 按钮底座 --> </defs>
<!-- 按钮底座 -->
<rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" /> <rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" />
<rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" /> <rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" />
@ -38,65 +38,50 @@
<!-- 按钮主体 --> <!-- 按钮主体 -->
<circle r="220" cx="800" cy="800" fill="black" filter="url(#btn-shadow)" /> <circle r="220" cx="800" cy="800" fill="black" filter="url(#btn-shadow)" />
<circle <circle :r="btnHeight" cx="800" cy="800" :fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'"
:r="btnHeight" fill-opacity="0.9" @mousedown="toggleButtonState(true)" @mouseup="toggleButtonState(false)"
cx="800" @mouseleave="toggleButtonState(false)" style="
cy="800" pointer-events: auto;
:fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'" transition: all 20ms ease-in-out;
fill-opacity="0.9" cursor: pointer;
@mousedown="toggleButtonState(true)" " />
@mouseup="toggleButtonState(false)"
@mouseleave="toggleButtonState(false)"
style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;"
/>
<!-- 按键文字 - 仅显示绑定的按键 --> <!-- 按键文字 - 仅显示绑定的按键 -->
<text <text v-if="bindKeyDisplay" x="800" y="800" font-size="310" text-anchor="middle" dominant-baseline="central"
v-if="bindKeyDisplay" fill="#ccc" style="
x="800" font-family: Arial;
y="800" filter: url(#btn-shadow);
font-size="310" user-select: none;
text-anchor="middle" pointer-events: none;
dominant-baseline="central" mix-blend-mode: overlay;
fill="#ccc" ">
style="font-family: Arial; filter: url(#btn-shadow); user-select: none; pointer-events: none; mix-blend-mode: overlay;"
>
{{ bindKeyDisplay }} {{ bindKeyDisplay }}
</text> </text>
</svg> </svg>
<!-- 渲染自定义引脚数组 --> <!-- 渲染自定义引脚数组 -->
<div v-for="pin in props.pins" :key="pin.pinId" <div v-for="pin in props.pins" :key="pin.pinId" :style="{
:style="{
position: 'absolute', position: 'absolute',
left: `${pin.x * props.size}px`, left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`, top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: 3, zIndex: 3,
pointerEvents: 'auto' pointerEvents: 'auto',
}" }" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`"> :data-pin-y="`${pin.y * props.size}`">
<Pin <Pin :ref="(el) => {
:ref="el => { if(el) pinRefs[pin.pinId] = el }" if (el) pinRefs[pin.pinId] = el;
direction="output" }
type="digital" " direction="output" type="digital" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId"
:label="pin.pinId" :size="0.8" :componentId="props.componentId" @value-change="handlePinValueChange" @pin-click="handlePinClick" />
:constraint="pin.constraint"
:pinId="pin.pinId"
:size="0.8"
:componentId="props.componentId"
@value-change="handlePinValueChange"
@pin-click="handlePinClick"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from "vue";
import Pin from './Pin.vue'; import Pin from "./Pin.vue";
import { notifyConstraintChange } from '../../stores/constraints'; import { useConstraintsStore } from "../../stores/constraints";
const { notifyConstraintChange } = useConstraintsStore();
// Pin // Pin
const pinRefs = ref<Record<string, any>>({}); const pinRefs = ref<Record<string, any>>({});
@ -116,16 +101,16 @@ interface ButtonProps {
const props = withDefaults(defineProps<ButtonProps>(), { const props = withDefaults(defineProps<ButtonProps>(), {
size: 1, size: 1,
bindKey: '', bindKey: "",
componentId: 'button-default', componentId: "button-default",
pins: () => [ pins: () => [
{ {
pinId: 'BTN', pinId: "BTN",
constraint: '', constraint: "",
x: 80, x: 80,
y: 140 y: 140,
} },
] ],
}); });
// //
@ -133,17 +118,19 @@ const width = computed(() => 160 * props.size);
const height = computed(() => 160 * props.size); const height = computed(() => 160 * props.size);
// //
const bindKeyDisplay = computed(() => props.bindKey ? props.bindKey.toUpperCase() : ''); const bindKeyDisplay = computed(() =>
props.bindKey ? props.bindKey.toUpperCase() : "",
);
// //
const emit = defineEmits([ const emit = defineEmits([
'update:bindKey', "update:bindKey",
'update:constraint', "update:constraint",
'press', "press",
'release', "release",
'click', "click",
'value-change', "value-change",
'pin-click' "pin-click",
]); ]);
// //
@ -153,12 +140,12 @@ const colorMatrix = ref("1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0");
// Pin // Pin
function handlePinValueChange(value: any) { function handlePinValueChange(value: any) {
emit('value-change', value); emit("value-change", value);
} }
// Pin // Pin
function handlePinClick(info: any) { function handlePinClick(info: any) {
emit('pin-click', info); emit("pin-click", info);
} }
// --- --- // --- ---
@ -168,24 +155,24 @@ function toggleButtonState(isPressed: boolean) {
// //
if (isPressed) { if (isPressed) {
emit('press'); emit("press");
// //
// //
if (props.pins) { if (props.pins) {
props.pins.forEach(pin => { props.pins.forEach((pin) => {
if (pin.constraint) { if (pin.constraint) {
notifyConstraintChange(pin.constraint, 'high'); notifyConstraintChange(pin.constraint, "high");
} }
}); });
} }
} else { } else {
emit('release'); emit("release");
emit('click'); emit("click");
// //
if (props.pins) { if (props.pins) {
props.pins.forEach(pin => { props.pins.forEach((pin) => {
if (pin.constraint) { if (pin.constraint) {
notifyConstraintChange(pin.constraint, 'low'); notifyConstraintChange(pin.constraint, "low");
} }
}); });
} }
@ -202,11 +189,11 @@ function handleKeyDown(event: KeyboardEvent) {
// --- --- // --- ---
onMounted(() => { onMounted(() => {
document.addEventListener('keydown', handleKeyDown); document.addEventListener("keydown", handleKeyDown);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener("keydown", handleKeyDown);
}); });
// //
@ -216,30 +203,38 @@ defineExpose({
// //
bindKey: props.bindKey, bindKey: props.bindKey,
componentId: props.componentId, componentId: props.componentId,
pins: props.pins pins: props.pins,
}), }),
// //
getPinPosition: (pinId: string) => { getPinPosition: (pinId: string) => {
console.log(`[MechanicalButton] 调用getPinPosition寻找pinId: ${pinId}`); console.log(`[MechanicalButton] 调用getPinPosition寻找pinId: ${pinId}`);
console.log(`[MechanicalButton] 组件ID: ${props.componentId}, 当前尺寸: ${props.size}, 组件宽高: ${width.value}x${height.value}`); console.log(
`[MechanicalButton] 组件ID: ${props.componentId}, 当前尺寸: ${props.size}, 组件宽高: ${width.value}x${height.value}`,
);
console.log(`[MechanicalButton] 当前存在的pins:`, props.pins); console.log(`[MechanicalButton] 当前存在的pins:`, props.pins);
// ID // ID
if (props.pins && props.pins.length > 0) { if (props.pins && props.pins.length > 0) {
const customPin = props.pins.find(p => p.pinId === pinId); const customPin = props.pins.find((p) => p.pinId === pinId);
if (customPin) { if (customPin) {
console.log(`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`, { x: customPin.x, y: customPin.y }); console.log(`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`, {
x: customPin.x,
y: customPin.y,
});
// //
// xysize=1size // xysize=1size
const scaledX = customPin.x * props.size; const scaledX = customPin.x * props.size;
const scaledY = customPin.y * props.size; const scaledY = customPin.y * props.size;
console.log(`[MechanicalButton] 返回缩放后的坐标:`, { x: scaledX, y: scaledY }); console.log(`[MechanicalButton] 返回缩放后的坐标:`, {
x: scaledX,
y: scaledY,
});
return { return {
x: scaledX, x: scaledX,
y: scaledY y: scaledY,
}; };
} else { } else {
console.log(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`); console.log(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`);
@ -249,7 +244,7 @@ defineExpose({
} }
console.log(`[MechanicalButton] 返回null未找到引脚`); console.log(`[MechanicalButton] 返回null未找到引脚`);
return null; return null;
} },
}); });
</script> </script>
@ -258,15 +253,15 @@ defineExpose({
export function getDefaultProps() { export function getDefaultProps() {
return { return {
size: 1, size: 1,
bindKey: '', bindKey: "",
pins: [ pins: [
{ {
pinId: 'BTN', pinId: "BTN",
constraint: '', constraint: "",
x: 80, x: 80,
y: 140 y: 140,
} },
] ],
}; };
} }
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="motherboard-container" :style="{ <div class="motherboard-container" v-bind="$attrs" :style="{
width: width + 'px', width: width + 'px',
height: height + 'px', height: height + 'px',
position: 'relative', position: 'relative',
@ -9,35 +9,41 @@
<image href="../equipments/svg/motherboard.svg" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" /> <image href="../equipments/svg/motherboard.svg" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
</svg> </svg>
</div> </div>
<Teleport to="#ComponentCapabilities" v-if="selectecComponentID === props.componentId">
<MotherBoardCaps :jtagAddr="props.boardAddr" :jtagPort="toNumber(props.boardPort)" :jtagFreq="jtagFreq"
@change-jtag-freq="changeJtagFreq" />
</Teleport>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { JtagClient, type FileParameter } from "@/APIClient"; import MotherBoardCaps from "./MotherBoardCaps.vue";
import z from "zod"; import { ref, computed, watchEffect, inject } from "vue";
import { useEquipments } from "@/stores/equipments"; import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
import UploadCard from "@/components/UploadCard.vue"; import { toNumber } from "lodash";
import { useDialogStore } from "@/stores/dialog";
import { toNumber, toString } from "lodash";
import { computed, ref, defineComponent, watch } from "vue";
// //
interface MotherBoardProps { export interface MotherBoardProps {
size?: number; size?: number;
jtagAddr?: string; boardAddr?: string;
jtagPort?: string; boardPort?: string;
componentId?: string;
} }
const emit = defineEmits<{
(e: "pinClick", pinId: string): void;
}>();
const props = withDefaults(defineProps<MotherBoardProps>(), getDefaultProps()); const props = withDefaults(defineProps<MotherBoardProps>(), getDefaultProps());
const selectecComponentID = inject(CanvasCurrentSelectedComponentID, ref(null));
const jtagFreq = ref("25 MHz");
function changeJtagFreq(text: string) {
jtagFreq.value = text;
}
// //
const width = computed(() => 800 * props.size); const width = computed(() => 800 * props.size);
const height = computed(() => 600 * props.size); const height = computed(() => 600 * props.size);
const eqps = useEquipments();
watch([props.jtagAddr, props.jtagPort], () => {
eqps.jtagIPAddr = props.jtagAddr;
eqps.jtagPort = props.jtagPort;
});
// //
defineExpose({ defineExpose({
@ -47,144 +53,17 @@ defineExpose({
}), }),
// getPinPosition // getPinPosition
getPinPosition: () => null, getPinPosition: () => null,
getCapabilities: () => {
// JSX
return defineComponent({
name: "MotherBoardCapabilities",
props: {
jtagAddr: {
type: String,
default: props.jtagAddr,
},
jtagPort: {
type: String,
default: props.jtagPort,
},
},
setup(props) {
const jtagController = new JtagClient();
const dialog = useDialogStore();
// 使
const jtagIDCode = ref("");
const boardAddress = computed(() => props.jtagAddr);
const boardPort = computed(() => toNumber(props.jtagPort));
async function uploadBitstream(event: Event, bitstream: File) {
if (!boardAddress.value || !boardPort.value) {
dialog.error("开发板地址或端口空缺");
return;
}
const fileParam = {
data: bitstream,
fileName: bitstream.name,
};
try {
const resp = await jtagController.uploadBitstream(
boardAddress.value,
fileParam,
);
return resp;
} catch (e) {
dialog.error("上传错误");
}
}
async function downloadBitstream() {
if (!boardAddress.value || !boardPort.value) {
dialog.error("开发板地址或端口空缺");
return;
}
try {
const resp = await jtagController.downloadBitstream(
boardAddress.value,
boardPort.value,
);
return resp;
} catch (e) {
dialog.error("上传错误");
console.error(e);
}
}
watch([boardAddress, boardPort], () => {
if (
z.string().ip().safeParse(boardAddress).success &&
z.number().positive().safeParse(boardPort).success
)
getIDCode();
});
async function getIDCode() {
if (!boardAddress.value || !boardPort.value) {
dialog.error("开发板地址或端口空缺");
return;
}
try {
const resp = await jtagController.getDeviceIDCode(
boardAddress.value,
boardPort.value,
);
jtagIDCode.value = toString(resp);
} catch (e) {
dialog.error("获取IDCode错误");
console.error(e);
}
}
return () => (
<div>
<h1 class="font-bold text-center text-2xl">Jtag</h1>
<div class="flex">
<p class="grow">IDCode: {jtagIDCode.value}</p>
<button class="btn btn-circle w-8 h-8" onClick={getIDCode}>
<svg
class="icon opacity-70"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="4865"
width="200"
height="200"
>
<path
d="M894.481158 505.727133c0 49.589418-9.711176 97.705276-28.867468 143.007041-18.501376 43.74634-44.98454 83.031065-78.712713 116.759237-33.728172 33.728172-73.012897 60.211337-116.759237 78.712713-45.311998 19.156292-93.417623 28.877701-143.007041 28.877701s-97.695043-9.721409-142.996808-28.877701c-43.756573-18.501376-83.031065-44.98454-116.76947-78.712713-33.728172-33.728172-60.211337-73.012897-78.712713-116.759237-19.156292-45.301765-28.867468-93.417623-28.867468-143.007041 0-49.579185 9.711176-97.695043 28.867468-142.996808 18.501376-43.74634 44.98454-83.031065 78.712713-116.759237 33.738405-33.728172 73.012897-60.211337 116.76947-78.712713 45.301765-19.166525 93.40739-28.877701 142.996808-28.877701 52.925397 0 104.008842 11.010775 151.827941 32.745798 46.192042 20.977777 86.909395 50.79692 121.016191 88.608084 4.389984 4.860704 8.646937 9.854439 12.781094 14.97097l0-136.263453c0-11.307533 9.168824-20.466124 20.466124-20.466124 11.307533 0 20.466124 9.15859 20.466124 20.466124l0 183.64253c0 5.433756-2.148943 10.632151-5.986341 14.46955-3.847631 3.837398-9.046027 5.996574-14.479783 5.996574l-183.64253-0.020466c-11.307533 0-20.466124-9.168824-20.466124-20.466124 0-11.307533 9.168824-20.466124 20.466124-20.466124l132.293025 0.020466c-3.960195-4.952802-8.063653-9.782807-12.289907-14.479783-30.320563-33.605376-66.514903-60.098773-107.549481-78.753645-42.467207-19.289322-87.850837-29.072129-134.902456-29.072129-87.195921 0-169.172981 33.9533-230.816946 95.597265-61.654198 61.654198-95.597265 143.621025-95.597265 230.816946s33.943067 169.172981 95.597265 230.816946c61.643965 61.654198 143.621025 95.607498 230.816946 95.607498s169.172981-33.9533 230.816946-95.607498c61.654198-61.643965 95.597265-143.621025 95.597265-230.816946 0-11.2973 9.168824-20.466124 20.466124-20.466124C885.322567 485.261009 894.481158 494.429833 894.481158 505.727133z"
p-id="4866"
></path>
</svg>
</button>
</div>
<div class="divider"></div>
<UploadCard
class="bg-base-200"
upload-event={uploadBitstream}
download-event={downloadBitstream}
defaultFile={eqps.jtagBitstream}
onFinishedUpload={(file: File) => {
eqps.jtagBitstream = file;
}}
>
{" "}
</UploadCard>
</div>
);
},
});
},
}); });
</script> </script>
<script lang="tsx"> <script lang="tsx">
// props // props
export function getDefaultProps() { export function getDefaultProps(): MotherBoardProps {
return { return {
size: 1, size: 1,
jtagAddr: "127.0.0.1", boardAddr: "127.0.0.1",
jtagPort: "1234", boardPort: "1234",
componentId: "DefaultMotherBoardID",
}; };
} }
</script> </script>

View File

@ -0,0 +1,124 @@
<template>
<div>
<h1 class="font-bold text-center text-2xl">Jtag</h1>
<div class="flex flex-col">
<p class="grow">Jtag Addr: {{ eqps.boardAddr }}</p>
<p class="grow">Jtag Port: {{ eqps.boardPort.toString() }}</p>
<div class="flex justify-between grow">
<p>
IDCode: 0x{{ jtagIDCode.toString(16).padStart(8, "0").toUpperCase() }}
</p>
<button class="btn btn-circle w-6 h-6" :onclick="getIDCode">
<svg class="icon opacity-70 fill-primary" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="4865" width="200" height="200">
<path
d="M894.481158 505.727133c0 49.589418-9.711176 97.705276-28.867468 143.007041-18.501376 43.74634-44.98454 83.031065-78.712713 116.759237-33.728172 33.728172-73.012897 60.211337-116.759237 78.712713-45.311998 19.156292-93.417623 28.877701-143.007041 28.877701s-97.695043-9.721409-142.996808-28.877701c-43.756573-18.501376-83.031065-44.98454-116.76947-78.712713-33.728172-33.728172-60.211337-73.012897-78.712713-116.759237-19.156292-45.301765-28.867468-93.417623-28.867468-143.007041 0-49.579185 9.711176-97.695043 28.867468-142.996808 18.501376-43.74634 44.98454-83.031065 78.712713-116.759237 33.738405-33.728172 73.012897-60.211337 116.76947-78.712713 45.301765-19.166525 93.40739-28.877701 142.996808-28.877701 52.925397 0 104.008842 11.010775 151.827941 32.745798 46.192042 20.977777 86.909395 50.79692 121.016191 88.608084 4.389984 4.860704 8.646937 9.854439 12.781094 14.97097l0-136.263453c0-11.307533 9.168824-20.466124 20.466124-20.466124 11.307533 0 20.466124 9.15859 20.466124 20.466124l0 183.64253c0 5.433756-2.148943 10.632151-5.986341 14.46955-3.847631 3.837398-9.046027 5.996574-14.479783 5.996574l-183.64253-0.020466c-11.307533 0-20.466124-9.168824-20.466124-20.466124 0-11.307533 9.168824-20.466124 20.466124-20.466124l132.293025 0.020466c-3.960195-4.952802-8.063653-9.782807-12.289907-14.479783-30.320563-33.605376-66.514903-60.098773-107.549481-78.753645-42.467207-19.289322-87.850837-29.072129-134.902456-29.072129-87.195921 0-169.172981 33.9533-230.816946 95.597265-61.654198 61.654198-95.597265 143.621025-95.597265 230.816946s33.943067 169.172981 95.597265 230.816946c61.643965 61.654198 143.621025 95.607498 230.816946 95.607498s169.172981-33.9533 230.816946-95.607498c61.654198-61.643965 95.597265-143.621025 95.597265-230.816946 0-11.2973 9.168824-20.466124 20.466124-20.466124C885.322567 485.261009 894.481158 494.429833 894.481158 505.727133z"
p-id="4866"></path>
</svg>
</button>
</div>
</div>
<div class="divider"></div>
<UploadCard class="bg-base-200" :upload-event="eqps.jtagUploadBitstream"
:download-event="eqps.jtagDownloadBitstream" :bitstream-file="eqps.jtagBitstream"
@update:bitstream-file="handleBitstreamChange">
</UploadCard>
<div class="divider"></div>
<div class="w-full">
<legend class="fieldset-legend text-sm mb-0.3">Jtag运行频率</legend>
<select class="select w-full" @change="handleSelectJtagSpeed" :value="props.jtagFreq">
<option v-for="option in selectJtagSpeedOptions" :value="option.id">
{{ option.text }}
</option>
</select>
</div>
<div class="flex flex-row items-center">
<fieldset class="fieldset w-70">
<legend class="fieldset-legend text-sm">边界扫描刷新率 / Hz</legend>
<input type="number" class="input validator" required placeholder="Type a number between 1 to 1000" min="1"
max="1000" v-model="jtagBoundaryScanFreq" title="Type a number between 1 to 1000" />
<p class="validator-hint">输入一个1 ~ 1000的数</p>
</fieldset>
<button class="btn btn-primary grow mx-4" :class="eqps.enableJtagBoundaryScan ? '' : 'btn-soft'"
:onclick="toggleJtagBoundaryScan">
{{ eqps.enableJtagBoundaryScan ? "关闭边界扫描" : "启动边界扫描" }}
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import z from "zod";
import UploadCard from "@/components/UploadCard.vue";
import { useDialogStore } from "@/stores/dialog";
import { useEquipments } from "@/stores/equipments";
import { computed, ref, watchEffect } from "vue";
interface CapsProps {
jtagAddr?: string;
jtagPort?: number;
jtagFreq?: string;
}
const emits = defineEmits<{
changeJtagFreq: [text: string];
}>();
const props = withDefaults(defineProps<CapsProps>(), {});
const selectJtagSpeedOptions = ref([
{ text: "25 MHz", id: 1 },
{ text: "12.5 MHz", id: 2 },
{ text: "6.25 MHz", id: 3 },
{ text: "3.125 MHz", id: 4 },
{ text: "1562.5 KHz", id: 5 },
{ text: "781.25 KHz", id: 6 },
{ text: "390.625 KHz", id: 7 },
]);
// Global Stores
const dialog = useDialogStore();
const eqps = useEquipments();
const jtagBoundaryScanFreq = computed({
get: () => eqps.jtagBoundaryScanFreq,
set: (val) => {
if (z.number().positive().max(1000).safeParse(val).success)
eqps.jtagBoundaryScanFreq = val;
},
});
// 使
const jtagIDCode = ref(0xffff_ffff);
function handleBitstreamChange(file: File | undefined) {
eqps.jtagBitstream = file;
}
function handleSelectJtagSpeed(event: Event) {
const target = event.target as HTMLSelectElement;
eqps.jtagSetSpeed(target.selectedIndex);
emits("changeJtagFreq", target.value);
}
async function toggleJtagBoundaryScan() {
if (eqps.jtagClientMutex.isLocked()) {
dialog.warn("Jtag正在被占用");
return;
}
eqps.enableJtagBoundaryScan = !eqps.enableJtagBoundaryScan;
}
async function getIDCode(isQuiet: boolean = false) {
jtagIDCode.value = await eqps.jtagGetIDCode(isQuiet);
}
watchEffect(async () => {
if (eqps.setAddr(props.jtagAddr) && eqps.setPort(props.jtagPort))
getIDCode(true);
});
</script>
<style scoped lang="postcss">
@import "@/assets/main.css";
</style>

View File

@ -1,43 +1,36 @@
<template> <template>
<div class="chip-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <svg <div class="chip-container" :style="{
xmlns="http://www.w3.org/2000/svg" width: width + 'px',
:width="width" height: height + 'px',
:height="height" position: 'relative',
viewBox="0 0 400 400" }">
class="fbg676-chip" <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 400 400" class="fbg676-chip">
> <!-- 芯片外边框 - 用多段path代替rect这样可以实现缺角 --> <!-- 芯片外边框 - 用多段path代替rect这样可以实现缺角 -->
<path <path d="M30,0 H390 Q400,0 400,10 V390 Q400,400 390,400 H10 Q0,400 0,390 V30 L30,0 Z" fill="#1C4E2D" />
d="M30,0 H390 Q400,0 400,10 V390 Q400,400 390,400 H10 Q0,400 0,390 V30 L30,0 Z"
fill="#1C4E2D"
/>
<!-- 芯片内层 - 增大尺寸 --> <!-- 芯片内层 - 增大尺寸 -->
<rect width="280" height="280" x="60" y="60" fill="#0F1211" rx="2" ry="2" /> <rect width="280" height="280" x="60" y="60" fill="#0F1211" rx="2" ry="2" />
</svg> </svg>
<!-- 渲染芯片引脚 --> <div v-for="pin in computedPins" :key="pin.pinId" <!-- 渲染芯片引脚 -->
:style="{ <div v-for="pin in computedPins" :key="pin.pinId" :style="{
position: 'absolute', position: 'absolute',
left: `${(pin.x || 0) * props.size * 0.37}px`, left: `${(pin.x || 0) * props.size * 0.37}px`,
top: `${(pin.y || 0) * props.size * 0.37}px`, top: `${(pin.y || 0) * props.size * 0.37}px`,
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)',
}" }" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${(pin.x || 0) * props.size * 0.37}`"
:data-pin-wrapper="`${pin.pinId}`" :data-pin-y="`${(pin.y || 0) * props.size * 0.37}`">
:data-pin-x="`${(pin.x || 0) * props.size * 0.37}`" <Pin :ref="(el) => {
:data-pin-y="`${(pin.y || 0) * props.size * 0.37}`"> <Pin if (el) pinRefs[pin.pinId] = el;
:ref="el => { if(el) pinRefs[pin.pinId] = el }" }
:label="pin.pinId" " :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" :size="0.35"
:constraint="pin.constraint" @pin-click="$emit('pin-click', $event)" />
:pinId="pin.pinId"
:size="0.35"
@pin-click="$emit('pin-click', $event)"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref, computed } from "vue";
import Pin from './Pin.vue'; import Pin from "./Pin.vue";
// Pin // Pin
const pinRefs = ref<Record<string, any>>({}); const pinRefs = ref<Record<string, any>>({});
@ -55,7 +48,7 @@ interface ChipProps {
const props = withDefaults(defineProps<ChipProps>(), { const props = withDefaults(defineProps<ChipProps>(), {
size: 1, size: 1,
pins: () => [] pins: () => [],
}); });
// //
@ -104,9 +97,9 @@ const computedPins = computed(() => {
for (let col = 0; col < pinColumns; col++) { for (let col = 0; col < pinColumns; col++) {
pins.push({ pins.push({
pinId: `pin_${pinIndex++}`, pinId: `pin_${pinIndex++}`,
constraint: '', constraint: "",
x: xStart + col * xStep, x: xStart + col * xStep,
y: yStart + row * yStep y: yStart + row * yStep,
}); });
} }
} }
@ -116,15 +109,15 @@ const computedPins = computed(() => {
defineExpose({ defineExpose({
getInfo: () => ({ getInfo: () => ({
chipType: 'PG2L100H_FBG676', chipType: "PG2L100H_FBG676",
direction: 'inout', direction: "inout",
type: 'digital', type: "digital",
pins: computedPins.value pins: computedPins.value,
}), }),
getPinPosition: (pinId: string) => { getPinPosition: (pinId: string) => {
// ID // ID
if (computedPins.value && computedPins.value.length > 0) { if (computedPins.value && computedPins.value.length > 0) {
const customPin = computedPins.value.find(p => p.pinId === pinId); const customPin = computedPins.value.find((p) => p.pinId === pinId);
if (customPin && customPin.x !== undefined && customPin.y !== undefined) { if (customPin && customPin.x !== undefined && customPin.y !== undefined) {
// 0.37 // 0.37
@ -133,13 +126,13 @@ defineExpose({
return { return {
x: scaledX, x: scaledX,
y: scaledY y: scaledY,
}; };
} }
return null; return null;
} }
return null; return null;
} },
}); });
</script> </script>
@ -148,7 +141,7 @@ defineExpose({
export function getDefaultProps() { export function getDefaultProps() {
return { return {
size: 1, size: 1,
pins: [] // computedPins pins: [], // computedPins
}; };
} }
</script> </script>

View File

@ -1,30 +1,14 @@
<template> <template>
<svg <svg :width="width" :height="height" :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight" class="pin-component">
:width="width" <g :transform="`translate(${viewBoxWidth / 2}, ${viewBoxHeight / 2})`">
:height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component"
> <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<g> <g>
<g transform="translate(-12.5, -12.5)"> <!-- 添加一个透明的更大区域来增强点击能力但比原来小一点 --> <g transform="translate(-12.5, -12.5)">
<circle <!-- 添加一个透明的更大区域来增强点击能力但比原来小一点 -->
cx="12.5" <circle cx="12.5" cy="12.5" r="5" fill="transparent" class="interactive" @click.stop="handlePinClick"
cy="12.5" @mousedown.stop @touchstart.stop :data-pin-element="`${props.pinId}`"
r="5"
fill="transparent"
class="interactive"
@click.stop="handlePinClick"
@mousedown.stop
@touchstart.stop
:data-pin-element="`${props.pinId}`"
:data-component-id="props.componentId" /> :data-component-id="props.componentId" />
<!-- 实际可见的引脚圆点 --> <!-- 实际可见的引脚圆点 -->
<circle <circle :style="{ fill: pinColor }" cx="12.5" cy="12.5" r="3.75" pointer-events="none"
:style="{ fill: pinColor }"
cx="12.5"
cy="12.5"
r="3.75"
pointer-events="none"
:data-pin-visual="`${props.pinId}`" /> :data-pin-visual="`${props.pinId}`" />
</g> </g>
</g> </g>
@ -33,32 +17,33 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints'; import { useConstraintsStore } from "../../stores/constraints";
const { getConstraintColor, onConstraintStateChange } = useConstraintsStore();
interface Props { interface Props {
size?: number; size?: number;
label?: string; label?: string;
constraint?: string; constraint?: string;
direction?: 'input' | 'output' | 'inout'; direction?: "input" | "output" | "inout";
type?: 'digital' | 'analog'; type?: "digital" | "analog";
pinId?: string; // ID pinId?: string; // ID
componentId?: string; // ID componentId?: string; // ID
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
size: 1, size: 1,
label: 'PIN', label: "PIN",
constraint: '', constraint: "",
direction: 'input', direction: "input",
type: 'digital', type: "digital",
pinId: 'pin-default', // ID pinId: "pin-default", // ID
componentId: '' // componentId: "", //
}); });
const emit = defineEmits([ const emit = defineEmits([
'value-change', "value-change",
'pin-click' // Pin "pin-click", // Pin
]); ]);
// //
@ -71,15 +56,15 @@ function handlePinClick(event: MouseEvent) {
const rect = target.getBoundingClientRect(); const rect = target.getBoundingClientRect();
const pinCenter = { const pinCenter = {
x: rect.left + rect.width / 2, x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2 y: rect.top + rect.height / 2,
}; };
emit('pin-click', { emit("pin-click", {
label: props.label, label: props.label,
constraint: props.constraint, constraint: props.constraint,
type: props.type, type: props.type,
direction: props.direction, direction: props.direction,
position: pinCenter, position: pinCenter,
originalEvent: event originalEvent: event,
}); });
} }
@ -89,7 +74,7 @@ const viewBoxWidth = computed(() => 15);
const viewBoxHeight = computed(() => 15); const viewBoxHeight = computed(() => 15);
const getColorByType = computed(() => { const getColorByType = computed(() => {
return props.type === 'analog' ? '#2a6099' : '#444'; return props.type === "analog" ? "#2a6099" : "#444";
}); });
// //
@ -105,7 +90,7 @@ onMounted(() => {
if (props.constraint) { if (props.constraint) {
unsubscribe = onConstraintStateChange((constraint, level) => { unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === props.constraint) { if (constraint === props.constraint) {
emit('value-change', { constraint, level }); emit("value-change", { constraint, level });
} }
}); });
} }
@ -117,7 +102,9 @@ onUnmounted(() => {
} }
}); });
watch(() => props.constraint, (newConstraint, oldConstraint) => { watch(
() => props.constraint,
(newConstraint, oldConstraint) => {
if (unsubscribe) { if (unsubscribe) {
unsubscribe(); unsubscribe();
unsubscribe = null; unsubscribe = null;
@ -125,19 +112,20 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
if (newConstraint) { if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => { unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) { if (constraint === newConstraint) {
emit('value-change', { constraint, level }); emit("value-change", { constraint, level });
} }
}); });
} }
}); },
);
function updateAnalogValue(value: number) { function updateAnalogValue(value: number) {
if (props.type !== 'analog') return; if (props.type !== "analog") return;
analogValue.value = Math.max(0, Math.min(1, value)); analogValue.value = Math.max(0, Math.min(1, value));
emit('value-change', { emit("value-change", {
label: props.label, label: props.label,
constraint: props.constraint, constraint: props.constraint,
value: analogValue.value value: analogValue.value,
}); });
} }
@ -149,16 +137,16 @@ defineExpose({
constraint: props.constraint, constraint: props.constraint,
direction: props.direction, direction: props.direction,
type: props.type, type: props.type,
pinId: props.pinId pinId: props.pinId,
}), }),
getPinPosition: () => { getPinPosition: () => {
// PingetPinPosition // PingetPinPosition
// MechanicalButtonposition使 // MechanicalButtonposition使
return { return {
x: viewBoxWidth.value / 2, x: viewBoxWidth.value / 2,
y: viewBoxHeight.value / 2 y: viewBoxHeight.value / 2,
}; };
} },
}); });
</script> </script>
@ -167,20 +155,20 @@ defineExpose({
export function getDefaultProps() { export function getDefaultProps() {
return { return {
size: 1, size: 1,
label: 'PIN', label: "PIN",
constraint: '', constraint: "",
direction: 'input', direction: "input",
type: 'digital', type: "digital",
pinId: 'pin-default', pinId: "pin-default",
componentId: '', componentId: "",
pins: [ pins: [
{ {
pinId: 'PIN', pinId: "PIN",
constraint: '', constraint: "",
x: 0, x: 0,
y: 0 y: 0,
} },
] ],
}; };
} }
</script> </script>
@ -190,18 +178,25 @@ export function getDefaultProps() {
display: block; display: block;
user-select: none; user-select: none;
position: relative; position: relative;
z-index: 5; /* 提高引脚组件的z-index */ z-index: 5;
pointer-events: auto; /* 确保可以接收点击事件 */ /* 提高引脚组件的z-index */
overflow: visible; /* 确保可以看到引脚 */ pointer-events: auto;
/* 确保可以接收点击事件 */
overflow: visible;
/* 确保可以看到引脚 */
} }
.interactive { .interactive {
cursor: pointer; cursor: pointer;
transition: filter 0.2s; transition: filter 0.2s;
pointer-events: auto; /* 确保可以接收点击事件 */ pointer-events: auto;
/* 确保可以接收点击事件 */
} }
.interactive:hover { .interactive:hover {
filter: brightness(1.2); filter: brightness(1.2);
stroke: rgba(255, 255, 255, 0.3); /* 添加边框以便更容易看到点击区域 */ stroke: rgba(255, 255, 255, 0.3);
/* 添加边框以便更容易看到点击区域 */
stroke-width: 1; stroke-width: 1;
} }
</style> </style>

View File

@ -64,9 +64,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints'; import { useConstraintsStore } from '../../stores/constraints';
import Pin from './Pin.vue'; import Pin from './Pin.vue';
const {getConstraintState, onConstraintStateChange} = useConstraintsStore();
// Pin // Pin
const pinRefs = ref<Record<string, any>>({}); const pinRefs = ref<Record<string, any>>({});

View File

@ -1,106 +1,75 @@
<template> <template>
<div class="seven-segment-display" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <svg <div class="seven-segment-display" :style="{
xmlns="http://www.w3.org/2000/svg" width: width + 'px',
:width="width" height: height + 'px',
:height="height" position: 'relative',
viewBox="0 0 120 220" }">
class="display" <svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 120 220" class="display">
>
<!-- 数码管基座 --> <!-- 数码管基座 -->
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" /> <rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" /> <rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
<!-- 7 + 小数点每个段由多边形表示重新设计点位置使其更接近实际数码管 --> <!-- 7 + 小数点每个段由多边形表示重新设计点位置使其更接近实际数码管 -->
<!-- a段 (顶部横线) --> <!-- a段 (顶部横线) -->
<polygon <polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
:points="'30,20 90,20 98,28 82,36 38,36 22,28'"
:fill="isSegmentActive('a') ? segmentColor : inactiveColor" :fill="isSegmentActive('a') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
class="segment"
/>
<!-- b段 (右上竖线) --> <!-- b段 (右上竖线) -->
<polygon <polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
:points="'100,30 108,38 108,82 100,90 92,82 92,38'"
:fill="isSegmentActive('b') ? segmentColor : inactiveColor" :fill="isSegmentActive('b') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
class="segment"
/>
<!-- c段 (右下竖线) --> <!-- c段 (右下竖线) -->
<polygon <polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
:points="'100,90 108,98 108,142 100,150 92,142 92,98'"
:fill="isSegmentActive('c') ? segmentColor : inactiveColor" :fill="isSegmentActive('c') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
class="segment"
/>
<!-- d段 (底部横线) --> <!-- d段 (底部横线) -->
<polygon <polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
:points="'30,160 90,160 98,152 82,144 38,144 22,152'"
:fill="isSegmentActive('d') ? segmentColor : inactiveColor" :fill="isSegmentActive('d') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
class="segment"
/>
<!-- e段 (左下竖线) --> <!-- e段 (左下竖线) -->
<polygon <polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
:points="'20,90 28,98 28,142 20,150 12,142 12,98'"
:fill="isSegmentActive('e') ? segmentColor : inactiveColor" :fill="isSegmentActive('e') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
class="segment"
/>
<!-- f段 (左上竖线) --> <!-- f段 (左上竖线) -->
<polygon <polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
:points="'20,30 28,38 28,82 20,90 12,82 12,38'"
:fill="isSegmentActive('f') ? segmentColor : inactiveColor" :fill="isSegmentActive('f') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
class="segment"
/>
<!-- g段 (中间横线) --> <!-- g段 (中间横线) -->
<polygon <polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
:points="'30,90 38,82 82,82 90,90 82,98 38,98'"
:fill="isSegmentActive('g') ? segmentColor : inactiveColor" :fill="isSegmentActive('g') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" :style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
class="segment" <!-- dp段 (小数点) -->
/> <!-- dp () --> <circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
<circle :style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
cx="108"
cy="154"
r="6"
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
class="segment"
/>
</svg> </svg>
<!-- 引脚 --> <!-- 引脚 -->
<div v-for="pin in pins" :key="pin.pinId" <div v-for="pin in pins" :key="pin.pinId" :style="{
:style="{
position: 'absolute', position: 'absolute',
left: `${pin.x * props.size}px`, left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`, top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)' transform: 'translate(-50%, -50%)',
}" }" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`"> :data-pin-y="`${pin.y * props.size}`">
<Pin <Pin :ref="(el) => {
:ref="el => { if(el) pinRefs[pin.pinId] = el }" if (el) pinRefs[pin.pinId] = el;
:label="pin.pinId" }
:constraint="pin.constraint" " :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints'; import { useConstraintsStore } from "../../stores/constraints";
import Pin from './Pin.vue'; import Pin from "./Pin.vue";
const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
// Pin // Pin
const pinRefs = ref<Record<string, any>>({}); const pinRefs = ref<Record<string, any>>({});
@ -115,41 +84,41 @@ interface SevenSegmentDisplayProps {
x: number; x: number;
y: number; y: number;
}[]; }[];
cathodeType?: 'common' | 'anode'; // cathodeType?: "common" | "anode"; //
} }
const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), { const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), {
size: 1, size: 1,
color: 'red', color: "red",
cathodeType: 'common', // cathodeType: "common", //
pins: () => [ pins: () => [
{ pinId: 'a', constraint: '', x: 10 , y: 170 }, // a { pinId: "a", constraint: "", x: 10, y: 170 }, // a
{ pinId: 'b', constraint: '', x: 25-1 , y: 170 }, // b { pinId: "b", constraint: "", x: 25 - 1, y: 170 }, // b
{ pinId: 'c', constraint: '', x: 40-2 , y: 170 }, // c { pinId: "c", constraint: "", x: 40 - 2, y: 170 }, // c
{ pinId: 'd', constraint: '', x: 55-3 , y: 170 }, // d { pinId: "d", constraint: "", x: 55 - 3, y: 170 }, // d
{ pinId: 'e', constraint: '', x: 70-4 , y: 170 }, // e { pinId: "e", constraint: "", x: 70 - 4, y: 170 }, // e
{ pinId: 'f', constraint: '', x: 85-5 , y: 170 }, // f { pinId: "f", constraint: "", x: 85 - 5, y: 170 }, // f
{ pinId: 'g', constraint: '', x: 100-6, y: 170 }, // g { pinId: "g", constraint: "", x: 100 - 6, y: 170 }, // g
{ pinId: 'dp', constraint: '', x: 115-7, y: 170 }, // { pinId: "dp", constraint: "", x: 115 - 7, y: 170 }, //
{ pinId: 'COM', constraint: '', x: 60 , y: 10 } // { pinId: "COM", constraint: "", x: 60, y: 10 }, //
] ],
}); });
const width = computed(() => 120 * props.size); const width = computed(() => 120 * props.size);
const height = computed(() => 220 * props.size); const height = computed(() => 220 * props.size);
// //
const segmentColor = computed(() => props.color || 'red'); const segmentColor = computed(() => props.color || "red");
const inactiveColor = computed(() => '#FFFFFF'); const inactiveColor = computed(() => "#FFFFFF");
// props // props
watch( watch(
() => props, () => props,
(newProps) => { (newProps) => {
console.log('SevenSegmentDisplay props changed:', newProps); console.log("SevenSegmentDisplay props changed:", newProps);
updateSegmentStates(); updateSegmentStates();
}, },
{ deep: true } { deep: true },
); );
// //
@ -165,29 +134,34 @@ const segmentStates = ref({
}); });
// //
function isSegmentActive(segment: 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'dp'): boolean { function isSegmentActive(
segment: "a" | "b" | "c" | "d" | "e" | "f" | "g" | "dp",
): boolean {
return segmentStates.value[segment]; return segmentStates.value[segment];
} }
// //
function updateSegmentStates() { function updateSegmentStates() {
for (const pin of props.pins) { for (const pin of props.pins) {
if (['a', 'b', 'c', 'd', 'e', 'f', 'g', 'dp'].includes(pin.pinId)) { if (["a", "b", "c", "d", "e", "f", "g", "dp"].includes(pin.pinId)) {
// constraint // constraint
if (!pin.constraint) { if (!pin.constraint) {
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false; segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
false;
continue; continue;
} }
const pinState = getConstraintState(pin.constraint); const pinState = getConstraintState(pin.constraint);
// / // /
if (props.cathodeType === 'common') { if (props.cathodeType === "common") {
// : // :
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = pinState === 'high'; segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
pinState === "high";
} else { } else {
// : // :
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = pinState === 'low'; segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
pinState === "low";
} }
} }
} }
@ -195,8 +169,11 @@ function updateSegmentStates() {
// //
function onConstraintChange(constraint: string, level: string) { function onConstraintChange(constraint: string, level: string) {
const affectedPin = props.pins.find(pin => pin.constraint === constraint); const affectedPin = props.pins.find((pin) => pin.constraint === constraint);
if (affectedPin && ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'dp'].includes(affectedPin.pinId)) { if (
affectedPin &&
["a", "b", "c", "d", "e", "f", "g", "dp"].includes(affectedPin.pinId)
) {
updateSegmentStates(); updateSegmentStates();
} }
} }
@ -213,7 +190,7 @@ onUnmounted(() => {
// //
defineExpose({ defineExpose({
updateSegmentStates updateSegmentStates,
}); });
</script> </script>
@ -224,7 +201,9 @@ defineExpose({
} }
.segment { .segment {
transition: opacity 0.2s, fill 0.2s; transition:
opacity 0.2s,
fill 0.2s;
} }
/* 数码管发光效果 */ /* 数码管发光效果 */
@ -238,19 +217,19 @@ defineExpose({
export function getDefaultProps() { export function getDefaultProps() {
return { return {
size: 1, size: 1,
color: 'red', color: "red",
cathodeType: 'common', cathodeType: "common",
pins: [ pins: [
{ pinId: 'a', constraint: '', x: 10 , y: 170 }, { pinId: "a", constraint: "", x: 10, y: 170 },
{ pinId: 'b', constraint: '', x: 25-1 , y: 170 }, { pinId: "b", constraint: "", x: 25 - 1, y: 170 },
{ pinId: 'c', constraint: '', x: 40-2 , y: 170 }, { pinId: "c", constraint: "", x: 40 - 2, y: 170 },
{ pinId: 'd', constraint: '', x: 55-3 , y: 170 }, { pinId: "d", constraint: "", x: 55 - 3, y: 170 },
{ pinId: 'e', constraint: '', x: 70-4 , y: 170 }, { pinId: "e", constraint: "", x: 70 - 4, y: 170 },
{ pinId: 'f', constraint: '', x: 85-5 , y: 170 }, { pinId: "f", constraint: "", x: 85 - 5, y: 170 },
{ pinId: 'g', constraint: '', x: 100-6, y: 170 }, { pinId: "g", constraint: "", x: 100 - 6, y: 170 },
{ pinId: 'dp', constraint: '', x: 115-7, y: 170 }, { pinId: "dp", constraint: "", x: 115 - 7, y: 170 },
{ pinId: 'COM', constraint: '', x: 60 , y: 10 } { pinId: "COM", constraint: "", x: 60, y: 10 },
] ],
}; };
} }
</script> </script>

View File

@ -1,28 +1,18 @@
<template> <template>
<g :data-wire-id="props.id"> <g :data-wire-id="props.id">
<path <path :d="pathData" fill="none" :stroke="computedStroke" :stroke-width="strokeWidth" stroke-linecap="round"
:d="pathData" stroke-linejoin="round" :class="{ 'wire-active': props.isActive }" @click="handleClick" />
fill="none"
:stroke="computedStroke"
:stroke-width="strokeWidth"
stroke-linecap="round"
stroke-linejoin="round"
:class="{ 'wire-active': props.isActive }"
@click="handleClick"
/>
<!-- 可选添加连线标签或状态指示器 --> <!-- 可选添加连线标签或状态指示器 -->
<text <text v-if="showLabel" :x="labelPosition.x" :y="labelPosition.y" class="wire-label">{{ props.constraint || ""
v-if="showLabel" }}</text>
:x="labelPosition.x"
:y="labelPosition.y"
class="wire-label"
>{{ props.constraint || '' }}</text>
</g> </g>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineEmits, reactive, watch, onMounted, onUnmounted } from 'vue'; import { useConstraintsStore } from "@/stores/constraints";
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints'; import { computed, defineEmits, watch, onMounted, onUnmounted } from "vue";
const { getConstraintColor, onConstraintStateChange } = useConstraintsStore();
interface Props { interface Props {
id: string; id: string;
@ -33,7 +23,7 @@ interface Props {
strokeColor?: string; strokeColor?: string;
strokeWidth?: number; strokeWidth?: number;
isActive?: boolean; isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path'; routingMode?: "auto" | "orthogonal" | "direct" | "path";
// //
startComponentId?: string; startComponentId?: string;
startPinId?: string; startPinId?: string;
@ -48,12 +38,12 @@ interface Props {
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
strokeColor: '#4a5568', strokeColor: "#4a5568",
strokeWidth: 2, strokeWidth: 2,
isActive: false, isActive: false,
routingMode: 'orthogonal', routingMode: "orthogonal",
showLabel: false, showLabel: false,
constraint: '' constraint: "",
}); });
// //
@ -64,37 +54,46 @@ const constraintColor = computed(() => {
// 使isActiveconstraint // 使isActiveconstraint
const computedStroke = computed(() => { const computedStroke = computed(() => {
if (props.isActive) return '#ff9800'; if (props.isActive) return "#ff9800";
return constraintColor.value || props.strokeColor; return constraintColor.value || props.strokeColor;
}); });
const emit = defineEmits(['click', 'update:active', 'update:position']); const emit = defineEmits(["click", "update:active", "update:position"]);
function handleClick(event: MouseEvent) { function handleClick(event: MouseEvent) {
emit('click', { id: props.id, event }); emit("click", { id: props.id, event });
} }
// - 线 // - 线
const labelPosition = computed(() => { const labelPosition = computed(() => {
return { return {
x: (props.startX + props.endX) / 2, x: (props.startX + props.endX) / 2,
y: (props.startY + props.endY) / 2 - 5 y: (props.startY + props.endY) / 2 - 5,
}; };
}); });
const pathData = computed(() => { const pathData = computed(() => {
// 使线 // 使线
if (props.routingMode === 'path' && props.pathCommands && props.pathCommands.length > 0) { if (
props.routingMode === "path" &&
props.pathCommands &&
props.pathCommands.length > 0
) {
return calculatePathFromCommands( return calculatePathFromCommands(
props.startX, props.startX,
props.startY, props.startY,
props.endX, props.endX,
props.endY, props.endY,
props.pathCommands props.pathCommands,
); );
} }
// 使 // 使
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY); return calculateOrthogonalPath(
props.startX,
props.startY,
props.endX,
props.endY,
);
}); });
function calculatePathFromCommands( function calculatePathFromCommands(
@ -102,10 +101,10 @@ function calculatePathFromCommands(
startY: number, startY: number,
endX: number, endX: number,
endY: number, endY: number,
commands: string[] commands: string[],
) { ) {
// "*" // "*"
const splitterIndex = commands.indexOf('*'); const splitterIndex = commands.indexOf("*");
if (splitterIndex === -1) { if (splitterIndex === -1) {
// 退 // 退
return calculateOrthogonalPath(startX, startY, endX, endY); return calculateOrthogonalPath(startX, startY, endX, endY);
@ -149,9 +148,12 @@ function calculatePathFromCommands(
// //
// //
let combinedPoints; let combinedPoints;
if (pathPoints.length > 0 && reversedEndPoints.length > 0 && if (
pathPoints.length > 0 &&
reversedEndPoints.length > 0 &&
pathPoints[pathPoints.length - 1][0] === reversedEndPoints[0][0] && pathPoints[pathPoints.length - 1][0] === reversedEndPoints[0][0] &&
pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]) { pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]
) {
combinedPoints = [...pathPoints, ...reversedEndPoints.slice(1)]; combinedPoints = [...pathPoints, ...reversedEndPoints.slice(1)];
} else { } else {
combinedPoints = [...pathPoints, ...reversedEndPoints]; combinedPoints = [...pathPoints, ...reversedEndPoints];
@ -160,19 +162,27 @@ function calculatePathFromCommands(
const allPoints = combinedPoints; const allPoints = combinedPoints;
// //
if (allPoints.length >= 2) { if (allPoints.length >= 2) {
const startPathEndPoint = pathPoints.length > 0 ? pathPoints[pathPoints.length - 1] : null; const startPathEndPoint =
const endPathStartPoint = reversedEndPoints.length > 0 ? reversedEndPoints[0] : null; pathPoints.length > 0 ? pathPoints[pathPoints.length - 1] : null;
const endPathStartPoint =
reversedEndPoints.length > 0 ? reversedEndPoints[0] : null;
// 线 // 线
if (startPathEndPoint && endPathStartPoint && if (
(startPathEndPoint[0] !== endPathStartPoint[0] || startPathEndPoint[1] !== endPathStartPoint[1])) { startPathEndPoint &&
endPathStartPoint &&
(startPathEndPoint[0] !== endPathStartPoint[0] ||
startPathEndPoint[1] !== endPathStartPoint[1])
) {
// 使 // 使
let middlePoints: [number, number][] = []; let middlePoints: [number, number][] = [];
// //
middlePoints = generateOrthogonalConnection( middlePoints = generateOrthogonalConnection(
startPathEndPoint[0], startPathEndPoint[1], startPathEndPoint[0],
endPathStartPoint[0], endPathStartPoint[1] startPathEndPoint[1],
endPathStartPoint[0],
endPathStartPoint[1],
); );
// //
@ -194,25 +204,29 @@ function calculatePathFromCommands(
} }
// //
function executePathCommand(x: number, y: number, command: string): { newX: number; newY: number } { function executePathCommand(
x: number,
y: number,
command: string,
): { newX: number; newY: number } {
// "down10", "right20", "downright5" // "down10", "right20", "downright5"
let newX = x; let newX = x;
let newY = y; let newY = y;
if (command.startsWith('right')) { if (command.startsWith("right")) {
const distance = parseInt(command.substring(5), 10) || 10; const distance = parseInt(command.substring(5), 10) || 10;
newX = x + distance; newX = x + distance;
newY = y; newY = y;
} else if (command.startsWith('left')) { } else if (command.startsWith("left")) {
const distance = parseInt(command.substring(4), 10) || 10; const distance = parseInt(command.substring(4), 10) || 10;
newX = x - distance; newX = x - distance;
newY = y; newY = y;
} else if (command.startsWith('down')) { } else if (command.startsWith("down")) {
if (command.startsWith('downright')) { if (command.startsWith("downright")) {
const distance = parseInt(command.substring(9), 10) || 10; const distance = parseInt(command.substring(9), 10) || 10;
newX = x + distance; newX = x + distance;
newY = y + distance; newY = y + distance;
} else if (command.startsWith('downleft')) { } else if (command.startsWith("downleft")) {
const distance = parseInt(command.substring(8), 10) || 10; const distance = parseInt(command.substring(8), 10) || 10;
newX = x - distance; newX = x - distance;
newY = y + distance; newY = y + distance;
@ -221,12 +235,12 @@ function executePathCommand(x: number, y: number, command: string): { newX: numb
newX = x; newX = x;
newY = y + distance; newY = y + distance;
} }
} else if (command.startsWith('up')) { } else if (command.startsWith("up")) {
if (command.startsWith('upright')) { if (command.startsWith("upright")) {
const distance = parseInt(command.substring(7), 10) || 10; const distance = parseInt(command.substring(7), 10) || 10;
newX = x + distance; newX = x + distance;
newY = y - distance; newY = y - distance;
} else if (command.startsWith('upleft')) { } else if (command.startsWith("upleft")) {
const distance = parseInt(command.substring(6), 10) || 10; const distance = parseInt(command.substring(6), 10) || 10;
newX = x - distance; newX = x - distance;
newY = y - distance; newY = y - distance;
@ -241,7 +255,12 @@ function executePathCommand(x: number, y: number, command: string): { newX: numb
} }
// //
function generateOrthogonalConnection(x1: number, y1: number, x2: number, y2: number): [number, number][] { function generateOrthogonalConnection(
x1: number,
y1: number,
x2: number,
y2: number,
): [number, number][] {
const dx = x2 - x1; const dx = x2 - x1;
const dy = y2 - y1; const dy = y2 - y1;
@ -267,7 +286,12 @@ function generateOrthogonalConnection(x1: number, y1: number, x2: number, y2: nu
} }
// //
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) { function calculateOrthogonalPath(
startX: number,
startY: number,
endX: number,
endY: number,
) {
// //
const dx = endX - startX; const dx = endX - startX;
const dy = endY - startY; const dy = endY - startY;
@ -298,32 +322,26 @@ function reversePathCommand(command: string): string {
// //
// //
if (command.startsWith('right')) { if (command.startsWith("right")) {
return `left${distance}`; return `left${distance}`;
} } else if (command.startsWith("left")) {
else if (command.startsWith('left')) {
return `right${distance}`; return `right${distance}`;
} }
// //
else if (command.startsWith('down')) { else if (command.startsWith("down")) {
if (command.startsWith('downright')) { if (command.startsWith("downright")) {
return `upleft${distance}`; return `upleft${distance}`;
} } else if (command.startsWith("downleft")) {
else if (command.startsWith('downleft')) {
return `upright${distance}`; return `upright${distance}`;
} } else {
else {
return `up${distance}`; return `up${distance}`;
} }
} } else if (command.startsWith("up")) {
else if (command.startsWith('up')) { if (command.startsWith("upright")) {
if (command.startsWith('upright')) {
return `downleft${distance}`; return `downleft${distance}`;
} } else if (command.startsWith("upleft")) {
else if (command.startsWith('upleft')) {
return `downright${distance}`; return `downright${distance}`;
} } else {
else {
return `down${distance}`; return `down${distance}`;
} }
} }
@ -354,7 +372,9 @@ onUnmounted(() => {
}); });
// //
watch(() => props.constraint, (newConstraint, oldConstraint) => { watch(
() => props.constraint,
(newConstraint, oldConstraint) => {
if (unsubscribe) { if (unsubscribe) {
unsubscribe(); unsubscribe();
unsubscribe = null; unsubscribe = null;
@ -367,26 +387,34 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
} }
}); });
} }
}); },
);
// 线 // 线
defineExpose({ id: props.id, getInfo: () => ({ defineExpose({
id: props.id,
getInfo: () => ({
id: props.id, id: props.id,
startComponentId: props.startComponentId, startComponentId: props.startComponentId,
startPinId: props.startPinId, startPinId: props.startPinId,
endComponentId: props.endComponentId, endComponentId: props.endComponentId,
endPinId: props.endPinId, endPinId: props.endPinId,
constraint: props.constraint constraint: props.constraint,
}), }),
// 线 // 线
updatePosition: (newStartX: number, newStartY: number, newEndX: number, newEndY: number) => { updatePosition: (
newStartX: number,
newStartY: number,
newEndX: number,
newEndY: number,
) => {
// props // props
emit('update:position', { emit("update:position", {
id: props.id, id: props.id,
startX: newStartX, startX: newStartX,
startY: newStartY, startY: newStartY,
endX: newEndX, endX: newEndX,
endY: newEndY endY: newEndY,
}); });
}, },
// 线 // 线
@ -395,8 +423,8 @@ defineExpose({ id: props.id, getInfo: () => ({
getRoutingMode: () => props.routingMode, getRoutingMode: () => props.routingMode,
// 线 // 线
setActive: (active: boolean) => { setActive: (active: boolean) => {
emit('update:active', active); emit("update:active", active);
} },
}); });
</script> </script>

View File

@ -1,36 +1,45 @@
import { ref, reactive } from 'vue'; import { ref, computed, reactive } from 'vue'
import { defineStore } from 'pinia'
import { isBoolean } from 'lodash';
// 约束电平状态类型 // 约束电平状态类型
export type ConstraintLevel = 'high' | 'low' | 'undefined'; export type ConstraintLevel = 'high' | 'low' | 'undefined';
// 约束状态存储 export const useConstraintsStore = defineStore('constraints', () => {
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 约束颜色映射 // 约束状态存储
export const constraintColors = { const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 约束颜色映射
const constraintColors = {
high: '#ff3333', // 高电平为红色 high: '#ff3333', // 高电平为红色
low: '#3333ff', // 低电平为蓝色 low: '#3333ff', // 低电平为蓝色
undefined: '#999999' // 未定义为灰色 undefined: '#999999' // 未定义为灰色
}; };
// 获取约束状态 // 获取约束状态
export function getConstraintState(constraint: string): ConstraintLevel { function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined'; if (!constraint) return 'undefined';
return constraintStates[constraint] || 'undefined'; return constraintStates[constraint] || 'undefined';
} }
// 设置约束状态 // 设置约束状态
export function setConstraintState(constraint: string, level: ConstraintLevel) { function setConstraintState(constraint: string, level: ConstraintLevel) {
if (!constraint) return; if (!constraint) return;
constraintStates[constraint] = level; constraintStates[constraint] = level;
} }
// 批量设置约束状态 // 批量设置约束状态
export function batchSetConstraintStates(states: Record<string, ConstraintLevel>) { function batchSetConstraintStates(states: Record<string, ConstraintLevel> | Record<string, boolean>) {
// 收集发生变化的约束 // 收集发生变化的约束
const changedConstraints: [string, ConstraintLevel][] = []; const changedConstraints: [string, ConstraintLevel][] = [];
// 更新状态并收集变化 // 更新状态并收集变化
Object.entries(states).forEach(([constraint, level]) => { Object.entries(states).forEach(([constraint, level]) => {
if (isBoolean(level)) {
level = level ? "high" : "low";
}
if (constraintStates[constraint] !== level) { if (constraintStates[constraint] !== level) {
constraintStates[constraint] = level; constraintStates[constraint] = level;
changedConstraints.push([constraint, level]); changedConstraints.push([constraint, level]);
@ -41,30 +50,30 @@ export function batchSetConstraintStates(states: Record<string, ConstraintLevel>
changedConstraints.forEach(([constraint, level]) => { changedConstraints.forEach(([constraint, level]) => {
stateChangeCallbacks.forEach(callback => callback(constraint, level)); stateChangeCallbacks.forEach(callback => callback(constraint, level));
}); });
} }
// 获取约束对应的颜色 // 获取约束对应的颜色
export function getConstraintColor(constraint: string): string { function getConstraintColor(constraint: string): string {
const state = getConstraintState(constraint); const state = getConstraintState(constraint);
return constraintColors[state]; return constraintColors[state];
} }
// 清除所有约束状态 // 清除所有约束状态
export function clearAllConstraintStates() { function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => { Object.keys(constraintStates).forEach(key => {
delete constraintStates[key]; delete constraintStates[key];
}); });
} }
// 获取所有约束状态 // 获取所有约束状态
export function getAllConstraintStates(): Record<string, ConstraintLevel> { function getAllConstraintStates(): Record<string, ConstraintLevel> {
return { ...constraintStates }; return { ...constraintStates };
} }
// 注册约束状态变化回调 // 注册约束状态变化回调
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = []; const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = [];
export function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) { function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) {
stateChangeCallbacks.push(callback); stateChangeCallbacks.push(callback);
return () => { return () => {
const index = stateChangeCallbacks.indexOf(callback); const index = stateChangeCallbacks.indexOf(callback);
@ -72,10 +81,23 @@ export function onConstraintStateChange(callback: (constraint: string, level: Co
stateChangeCallbacks.splice(index, 1); stateChangeCallbacks.splice(index, 1);
} }
}; };
} }
// 触发约束变化 // 触发约束变化
export function notifyConstraintChange(constraint: string, level: ConstraintLevel) { function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
setConstraintState(constraint, level); setConstraintState(constraint, level);
stateChangeCallbacks.forEach(callback => callback(constraint, level)); stateChangeCallbacks.forEach(callback => callback(constraint, level));
} }
return {
getConstraintState,
setConstraintState,
batchSetConstraintStates,
getConstraintColor,
clearAllConstraintStates,
getAllConstraintStates,
onConstraintStateChange,
notifyConstraintChange,
}
})

View File

@ -1,22 +1,157 @@
import { ref, computed } from 'vue' import { ref, watchEffect } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { isUndefined } from 'lodash'; import { isString, toNumber, isUndefined } from 'lodash';
import { Common } from '@/Common';
import z from "zod"
import { isNumber } from 'mathjs';
import { JtagClient } from "@/APIClient";
import { Mutex, withTimeout } from 'async-mutex';
import { useConstraintsStore } from "@/stores/constraints";
import { useDialogStore } from './dialog';
export const useEquipments = defineStore('equipments', () => { export const useEquipments = defineStore('equipments', () => {
const jtagIPAddr = ref("127.0.0.1") // Global Stores
const jtagPort = ref("1234") const constrainsts = useConstraintsStore();
const jtagBitstream = ref<File | undefined>() const dialog = useDialogStore();
const remoteUpdateIPAddr = ref("127.0.0.1")
const remoteUpdatePort = ref("1234") const boardAddr = ref("127.0.0.1");
const remoteUpdateBitstream = ref<File | undefined>() const boardPort = ref(1234);
const jtagBitstream = ref<File>();
const jtagBoundaryScanFreq = ref(100);
const jtagClientMutex = withTimeout(new Mutex(), 1000, new Error("JtagClient Mutex Timeout!"))
const jtagClient = new JtagClient();
const enableJtagBoundaryScan = ref(false);
function setAddr(address: string | undefined): boolean {
if (isString(address) && z.string().ip("4").safeParse(address).success) {
boardAddr.value = address;
return true;
}
return false;
}
function setPort(port: string | number | undefined): boolean {
if (isString(port) && port.length != 0) {
const portNumber = toNumber(port);
if (z.number().nonnegative().max(65535).safeParse(portNumber).success) {
boardPort.value = portNumber;
return true;
}
}
else if (isNumber(port)) {
if (z.number().nonnegative().max(65535).safeParse(port).success) {
boardPort.value = port;
return true;
}
}
return false;
}
watchEffect(() => {
if (enableJtagBoundaryScan.value) jtagBoundaryScan();
});
async function jtagBoundaryScan() {
const release = await jtagClientMutex.acquire();
try {
const portStates = await jtagClient.boundaryScanLogicalPorts(
boardAddr.value,
boardPort.value,
);
constrainsts.batchSetConstraintStates(portStates);
} catch (error) {
dialog.error("边界扫描发生错误");
console.error(error);
enableJtagBoundaryScan.value = false;
} finally {
release();
if (enableJtagBoundaryScan.value)
setTimeout(jtagBoundaryScan, 1000 / jtagBoundaryScanFreq.value);
}
}
async function jtagUploadBitstream(bitstream: File): Promise<boolean> {
try {
const resp = await jtagClient.uploadBitstream(
boardAddr.value,
Common.toFileParameterOrNull(bitstream),
);
return resp;
} catch (e) {
dialog.error("上传错误");
console.error(e);
return false;
}
}
async function jtagDownloadBitstream(): Promise<boolean> {
const release = await jtagClientMutex.acquire();
try {
const resp = await jtagClient.downloadBitstream(
boardAddr.value,
boardPort.value
);
return resp;
} catch (e) {
dialog.error("上传错误");
console.error(e);
return false;
} finally {
release();
}
}
async function jtagGetIDCode(isQuiet: boolean = false): Promise<number> {
const release = await jtagClientMutex.acquire();
try {
const resp = await jtagClient.getDeviceIDCode(
boardAddr.value,
boardPort.value
);
return resp;
} catch (e) {
if (!isQuiet) dialog.error("获取IDCode错误");
return 0xffff_ffff;
} finally {
release();
}
}
async function jtagSetSpeed(speed: number): Promise<boolean> {
const release = await jtagClientMutex.acquire();
try {
const resp = await jtagClient.setSpeed(
boardAddr.value,
boardPort.value,
speed
);
return resp;
} catch (e) {
dialog.error("设置Jtag速度失败");
return false;
} finally {
release();
}
}
return { return {
jtagIPAddr, boardAddr,
jtagPort, boardPort,
setAddr,
setPort,
jtagBitstream, jtagBitstream,
remoteUpdateIPAddr, jtagBoundaryScanFreq,
remoteUpdatePort, jtagClientMutex,
remoteUpdateBitstream, jtagClient,
jtagUploadBitstream,
jtagDownloadBitstream,
jtagGetIDCode,
jtagSetSpeed,
enableJtagBoundaryScan,
} }
}) })

View File

@ -1,23 +1,35 @@
<template> <template>
<div class="bg-base-200 min-h-screen p-6"> <div class="bg-base-200 min-h-screen p-6">
<div class="flex flex-row justify-between items-center">
<h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1> <h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
<button class="btn btn-ghost text-error hover:underline" @click="
() => {
isEditMode = !isEditMode;
}
">
编辑
</button>
</div>
<div class="card bg-base-100 shadow-xl"> <div class="card bg-base-100 shadow-xl">
<div class="card-body"> <div class="card-body">
<div class="flex flex-row justify-between items-center">
<h2 class="card-title mb-4">IP 地址列表</h2> <h2 class="card-title mb-4">IP 地址列表</h2>
<button class="btn btn-ghost" @click="">刷新</button>
</div>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table w-full"> <table class="table w-full">
<!-- 表头 --> <!-- 表头 -->
<thead> <thead>
<tr class="bg-base-300"> <tr class="bg-base-300">
<th>IP 地址</th> <th class="w-50">IP 地址</th>
<th>版本号</th> <th class="w-30">版本号</th>
<th>默认启动位流</th> <th class="w-50">默认启动位流</th>
<th>黄金位流</th> <th class="w-80">黄金位流</th>
<th>应用位流1</th> <th class="w-80">应用位流1</th>
<th>应用位流2</th> <th class="w-80">应用位流2</th>
<th>应用位流3</th> <th class="w-80">应用位流3</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
@ -25,10 +37,13 @@
<!-- 表格内容 --> <!-- 表格内容 -->
<tbody> <tbody>
<tr class="hover"> <tr class="hover">
<td class="font-medium">192.168.1.100</td> <td class="font-medium">
<input v-if="isEditMode" type="text" placeholder="Type here" class="input m-0" v-model="devAddr" />
<span v-else>{{ devAddr }}</span>
</td>
<td>v1.2.3</td> <td>v1.2.3</td>
<td> <td>
<select class="select select-bordered w-full max-w-xs"> <select class="select select-bordered w-full max-w-xs" v-model="selectBitstream">
<option selected>黄金位流</option> <option selected>黄金位流</option>
<option>应用位流1</option> <option>应用位流1</option>
<option>应用位流2</option> <option>应用位流2</option>
@ -38,56 +53,46 @@
<!-- 黄金位流上传区 --> <!-- 黄金位流上传区 -->
<td> <td>
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-primary"> <input type="file" class="file-input file-input-primary" @change="handleFileChange($event, 0)" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<input type="file" class="hidden" @change="handleFileChange($event, goldBitstreamFile)" />
</label>
<span class="text-xs text-primary truncate max-w-32" v-if="goldBitstreamFile">{{ goldBitstreamFile }}</span>
</div> </div>
</td> </td>
<!-- 应用位流1上传区 --> <!-- 应用位流1上传区 -->
<td> <td>
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-secondary"> <input type="file" class="file-input file-input-secondary" @change="handleFileChange($event, 1)" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<input type="file" class="hidden" @change="handleFileChange($event, appBitstream1File)" />
</label>
<span class="text-xs text-secondary truncate max-w-32" v-if="appBitstream1File">{{ appBitstream1File }}</span>
</div> </div>
</td> </td>
<!-- 应用位流2上传区 --> <!-- 应用位流2上传区 -->
<td> <td>
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-accent"> <input type="file" class="file-input file-input-accent" @change="handleFileChange($event, 2)" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<input type="file" class="hidden" @change="handleFileChange($event, appBitstream2File)" />
</label>
<span class="text-xs text-accent truncate max-w-32" v-if="appBitstream2File">{{ appBitstream2File }}</span>
</div> </div>
</td> </td>
<!-- 应用位流3上传区 --> <!-- 应用位流3上传区 -->
<td> <td>
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-info"> <input type="file" class="file-input file-input-info" @change="handleFileChange($event, 3)" />
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<input type="file" class="hidden" @change="handleFileChange($event, appBitstream3File)" />
</label>
<span class="text-xs text-info truncate max-w-32" v-if="appBitstream3File">{{ appBitstream3File }}</span>
</div> </div>
</td> </td>
<!-- 操作按钮 --> <!-- 操作按钮 -->
<td class="flex gap-2"> <td class="flex gap-2">
<button class="btn btn-sm btn-warning">固化</button> <button class="btn grow btn-warning" @click="
<button class="btn btn-sm btn-success">切换并热启动</button> uploadAndDownloadBitstreams(
devAddr,
goldBitstreamFile,
appBitstream1File,
appBitstream2File,
appBitstream3File,
)
">
固化
</button>
<button class="btn grow btn-success" @click="
hotresetBitstream(devAddr, getSelectedBitstreamNum())
">
切换并热启动
</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -96,7 +101,7 @@
<div class="mt-6 bg-base-300 p-4 rounded-lg"> <div class="mt-6 bg-base-300 p-4 rounded-lg">
<p class="text-sm opacity-80"> <p class="text-sm opacity-80">
<span class="font-semibold text-warning">提示</span> <span class="font-semibold text-error">提示</span>
请谨慎操作FPGA固化和热启动功能确保上传的位流文件无误以避免设备损坏 请谨慎操作FPGA固化和热启动功能确保上传的位流文件无误以避免设备损坏
</p> </p>
</div> </div>
@ -106,21 +111,139 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { isNull, isUndefined } from "lodash";
import { ref } from "vue";
import { RemoteUpdateClient } from "@/APIClient";
import { useDialogStore } from "@/stores/dialog";
import { Common } from "@/Common";
const dialog = useDialogStore();
//
const isEditMode = ref(false);
//
const selectBitstream = ref("黄金位流");
// //
const goldBitstreamFile = ref<string>(''); const goldBitstreamFile = ref<File>();
const appBitstream1File = ref<string>(''); const appBitstream1File = ref<File>();
const appBitstream2File = ref<string>(''); const appBitstream2File = ref<File>();
const appBitstream3File = ref<string>(''); const appBitstream3File = ref<File>();
//
const devAddr = ref("192.168.1.100");
const devPort = 1234;
const remoteUpdater = new RemoteUpdateClient();
// //
function handleFileChange(event: Event, fileRef: any) { function handleFileChange(event: Event, bistreamNum: number) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const file = target.files?.[0]; const file = target.files?.[0]; //
if (file) { if (!file) {
fileRef.value = file.name; return;
}
if (bistreamNum === 0) {
goldBitstreamFile.value = file;
} else if (bistreamNum === 1) {
appBitstream1File.value = file;
} else if (bistreamNum === 2) {
appBitstream2File.value = file;
} else if (bistreamNum === 3) {
appBitstream3File.value = file;
} else {
goldBitstreamFile.value = file;
}
}
function getSelectedBitstreamNum(): number {
if (selectBitstream.value == "黄金位流") {
return 0;
} else if (selectBitstream.value == "应用位流1") {
return 1;
} else if (selectBitstream.value == "应用位流2") {
return 2;
} else if (selectBitstream.value == "应用位流3") {
return 3;
}
return 0;
}
async function uploadAndDownloadBitstreams(
devAddr: string,
goldBitstream?: File,
appBitstream1?: File,
appBitstream2?: File,
appBitstream3?: File,
) {
let cnt = 0;
if (!isUndefined(goldBitstream)) cnt++;
if (!isUndefined(appBitstream1)) cnt++;
if (!isUndefined(appBitstream2)) cnt++;
if (!isUndefined(appBitstream3)) cnt++;
if (cnt === 0) {
dialog.error("未选择比特流");
return;
}
try {
{
const ret = await remoteUpdater.uploadBitstreams(
devAddr,
Common.toFileParameterOrNull(goldBitstream),
Common.toFileParameterOrNull(appBitstream1),
Common.toFileParameterOrNull(appBitstream2),
Common.toFileParameterOrNull(appBitstream3),
);
if (!ret) {
dialog.warn("上传比特流出错");
return;
}
}
{
const ret = await remoteUpdater.downloadMultiBitstreams(
devAddr,
devPort,
getSelectedBitstreamNum(),
);
if (ret != cnt) {
dialog.warn("固化比特流出错");
} else {
dialog.info("固化比特流成功");
}
}
} catch (e) {
dialog.error("比特流上传错误");
console.error(e);
}
}
async function hotresetBitstream(devAddr: string, bitstreamNum: number) {
try {
const ret = await remoteUpdater.hotResetBitstream(
devAddr,
devPort,
bitstreamNum,
);
if (ret) {
dialog.info("切换比特流成功");
} else {
dialog.error("切换比特流失败");
}
} catch (e) {
dialog.error("切换比特流失败");
console.error(e);
}
}
async function refreshData() {
try {
const ret = await remoteUpdater.getFirmwareVersion(devAddr.value, devPort);
} catch (e) {
dialog.error("获取数据失败");
console.error(e);
} }
} }
</script> </script>

View File

@ -1,9 +1,12 @@
<template> <div class="bg-base-200 min-h-screen"> <template>
<div class="bg-base-200 min-h-screen">
<main class="hero min-h-screen bg-base-200"> <main class="hero min-h-screen bg-base-200">
<div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4"> <div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
<!-- 图片容器 --> <!-- 图片容器 -->
<div class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out"> <div
<img src="https://placehold.co/600x400" class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" /> class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
<img src="https://placehold.co/600x400"
class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" />
<!-- 这里使用relative定位限制覆盖层只在图片容器内 --> <!-- 这里使用relative定位限制覆盖层只在图片容器内 -->
<div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div> <div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
</div> </div>
@ -14,64 +17,72 @@
Welcome to Welcome to
</span> </span>
<span class="text-base-content">FPGA Web Lab!</span> <span class="text-base-content">FPGA Web Lab!</span>
<span class="absolute bottom-0 left-0 w-0 h-1 bg-primary transition-all duration-500 ease-in-out group-hover:w-3/4"></span> <span
class="absolute bottom-0 left-0 w-0 h-1 bg-primary transition-all duration-500 ease-in-out group-hover:w-3/4"></span>
</h1> </h1>
<p class="py-6 text-lg opacity-80 leading-relaxed"> <p class="py-6 text-lg opacity-80 leading-relaxed">
Prototype and simulate electronic circuits in your browser with our modern, intuitive interface. Create, test, and share your FPGA designs seamlessly. Prototype and simulate electronic circuits in your browser with our
modern, intuitive interface. Create, test, and share your FPGA
designs seamlessly.
</p> </p>
<div class="flex flex-wrap gap-4 actions-container"> <div class="flex flex-wrap gap-4 actions-container">
<router-link to="/project" class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"> <router-link to="/project"
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path> <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path> <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg> </svg>
进入工程界面 进入工程界面
</router-link> </router-link>
<router-link to="/login" class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"> <router-link to="/login"
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg> </svg>
登录 登录
</router-link> </router-link>
<router-link to="/user" class="btn btn-accent text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"> <router-link to="/user"
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn btn-accent text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle> <circle cx="12" cy="7" r="4"></circle>
</svg> </svg>
用户中心 用户中心
</router-link> </router-link>
<router-link to="/test" class="btn btn-info text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"> <router-link to="/test"
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn btn-info text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path> <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline> <polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline> <polyline points="7 3 7 8 15 8"></polyline>
</svg> </svg>
测试功能 测试功能
</router-link> </router-link>
<router-link to="/test/jtag" class="btn btn-warning text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
JTAG测试
</router-link>
<router-link to="/admin" class="btn btn-error text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"> <router-link to="/admin"
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> class="btn btn-error text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5z"></path> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5z">
</path>
<circle cx="12" cy="12" r="3"></circle> <circle cx="12" cy="12" r="3"></circle>
</svg> </svg>
管理控制台 管理控制台
</router-link> </router-link>
</div> </div>
<div class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md"> <div
class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md">
<p class="text-sm"> <p class="text-sm">
<span class="font-semibold text-primary">提示</span> 您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计 <span class="font-semibold text-primary">提示</span>
您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计
</p> </p>
</div> </div>
</div> </div>

View File

@ -379,7 +379,6 @@ async function handleAddComponent(componentData: {
x: Math.round(position.x + offsetX), x: Math.round(position.x + offsetX),
y: Math.round(position.y + offsetY), y: Math.round(position.y + offsetY),
attrs: componentData.props, attrs: componentData.props,
capsPage: capsPage, //
rotate: 0, rotate: 0,
group: "", group: "",
positionlock: false, positionlock: false,
@ -572,36 +571,58 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
if (moduleRef) { if (moduleRef) {
try { try {
// 使 //
const propConfigs: PropertyConfig[] = []; const propConfigs: PropertyConfig[] = [];
// 1. - DiagramPart //
const directPropConfigs = generatePropertyConfigs(componentData); const addedProps = new Set<string>();
propConfigs.push(...directPropConfigs);
// 2. 使getDefaultProps // 1. getDefaultProps
if (typeof moduleRef.getDefaultProps === "function") { if (typeof moduleRef.getDefaultProps === "function") {
// getDefaultProps
const defaultProps = moduleRef.getDefaultProps(); const defaultProps = moduleRef.getDefaultProps();
const defaultPropConfigs = generatePropsFromDefault(defaultProps); const defaultPropConfigs = generatePropsFromDefault(defaultProps);
propConfigs.push(...defaultPropConfigs);
//
defaultPropConfigs.forEach((config) => {
propConfigs.push(config);
addedProps.add(config.name);
});
}
// 2.
const directPropConfigs = generatePropertyConfigs(componentData);
//
const newDirectProps = directPropConfigs.filter(
(config) => !addedProps.has(config.name),
);
propConfigs.push(...newDirectProps);
// 3. attrs
if (componentData.attrs) {
const attrs = componentData.attrs;
const attrPropConfigs = generatePropsFromAttrs(attrs);
//
attrPropConfigs.forEach((attrConfig) => {
const existingIndex = propConfigs.findIndex(
(p) => p.name === attrConfig.name,
);
if (existingIndex >= 0) {
//
propConfigs[existingIndex] = attrConfig;
} else {
//
propConfigs.push(attrConfig);
}
});
}
selectedComponentConfig.value = { props: propConfigs }; selectedComponentConfig.value = { props: propConfigs };
console.log( console.log(
`Built config for ${componentData.type} from getDefaultProps:`, `Built config for ${componentData.type}:`,
selectedComponentConfig.value, selectedComponentConfig.value,
); );
} else {
console.warn(
`Component ${componentData.type} does not export getDefaultProps method.`,
);
//
const attrs = componentData.attrs || {};
const attrPropConfigs = generatePropsFromAttrs(attrs);
propConfigs.push(...attrPropConfigs);
selectedComponentConfig.value = { props: propConfigs };
}
} catch (error) { } catch (error) {
console.error( console.error(
`Error building config for ${componentData.type}:`, `Error building config for ${componentData.type}:`,

View File

@ -44,7 +44,8 @@
import { JtagClient, type FileParameter } from "@/APIClient"; import { JtagClient, type FileParameter } from "@/APIClient";
import UploadCard from "@/components/UploadCard.vue"; import UploadCard from "@/components/UploadCard.vue";
import { useDialogStore } from "@/stores/dialog"; import { useDialogStore } from "@/stores/dialog";
import { toNumber } from "lodash"; import { Common } from "@/Common";
import { toNumber, isUndefined } from "lodash";
import { ref } from "vue"; import { ref } from "vue";
const jtagController = new JtagClient(); const jtagController = new JtagClient();
@ -54,39 +55,29 @@ const dialog = useDialogStore();
const boardAddress = ref("127.0.0.1"); // IP const boardAddress = ref("127.0.0.1"); // IP
const boardPort = ref("1234"); // const boardPort = ref("1234"); //
async function uploadBitstream(event: Event, bitstream: File) { async function uploadBitstream(bitstream: File): Promise<boolean> {
if (boardAddress.value.length == 0) { if (isUndefined(boardAddress.value) || isUndefined(boardPort.value)) {
dialog.error("开发板地址空缺"); dialog.error("开发板地址或端口空缺");
return; return false;
}
if (boardPort.value.length == 0) {
dialog.error("开发板端口空缺");
return;
} }
const fileParam: FileParameter = {
data: bitstream,
fileName: bitstream.name,
};
try { try {
const resp = await jtagController.uploadBitstream( const resp = await jtagController.uploadBitstream(
boardAddress.value, boardAddress.value,
fileParam, Common.toFileParameterOrNull(bitstream),
); );
return resp return resp;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("上传错误");
console.error(e);
return false;
} }
} }
async function downloadBitstream() { async function downloadBitstream(): Promise<boolean> {
if (boardAddress.value.length == 0) { if (isUndefined(boardAddress.value) || isUndefined(boardPort.value)) {
dialog.error("开发板地址空缺"); dialog.error("开发板地址或端口空缺");
return; return false;
}
if (boardPort.value.length == 0) {
dialog.error("开发板端口空缺");
return;
} }
try { try {
@ -98,6 +89,7 @@ async function downloadBitstream() {
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("上传错误");
console.error(e); console.error(e);
return false;
} }
} }
</script> </script>