Merge branch 'csharp'

This commit is contained in:
2025-05-19 21:19:55 +08:00
42 changed files with 8757 additions and 2053 deletions

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ lerna-debug.log*
node_modules
.DS_Store
.vscode
dist
**/wwwroot
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"
@_show-dir:
@@ -12,17 +13,16 @@ clean:
rm -rf "server.test/bin"
rm -rf "server.test/obj"
rm -rf "dist"
rm -rf "wwwroot"
update:
npm install
cd server && dotnet restore
dotnet restore ./server/server.csproj
git submodule update --init --remote --recursive
# 生成Restful API到网页客户端
gen-api:
cd server && dotnet run &
npx nswag openapi2tsclient /input:http://localhost:5000/swagger/v1/swagger.json /output:src/APIClient.ts
pkill server
npm run gen-api
# 构建服务器包含win与linux平台
[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/win-x64/publish/wwwroot/
run: run-server
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:
npm run build
@@ -43,7 +45,7 @@ dev: dev-server
# 测试服务器
dev-server: _show-dir
cd server && dotnet run --watch
dotnet run --watch --project ./server/server.csproj
# 运行网页客户端
dev-web:

16
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16",
"all": "^0.0.0",
"async-mutex": "^0.5.0",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"marked": "^12.0.0",
@@ -2014,6 +2015,15 @@
"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": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -3720,6 +3730,12 @@
"integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==",
"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": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",

View File

@@ -9,7 +9,7 @@
"preview": "vite preview",
"build-only": "vite 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",
"postgen-api": "pkill server"
},
@@ -17,6 +17,7 @@
"@svgdotjs/svg.js": "^3.2.4",
"@types/lodash": "^4.17.16",
"all": "^0.0.0",
"async-mutex": "^0.5.0",
"lodash": "^4.17.21",
"log-symbols": "^7.0.0",
"marked": "^12.0.0",

1
server/.gitignore vendored
View File

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

View File

@@ -94,13 +94,31 @@ try
logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}");
app.UseDefaultFiles();
app.UseStaticFiles(); // Serves files from wwwroot by default
// Assets Files
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "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");
}
// Add logs
app.UseHttpsRedirection();
app.UseRouting();
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.Web.AspNetCore" Version="5.4.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" />
</ItemGroup>

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;
// /// <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>
/// UDP API
/// </summary>
@@ -198,6 +160,7 @@ public class JtagController : ControllerBase
/// <param name="address"> 设备地址 </param>
/// <param name="port"> 设备端口 </param>
[HttpGet("GetDeviceIDCode")]
[EnableCors("Users")]
[ProducesResponseType(typeof(uint), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> GetDeviceIDCode(string address, int port)
@@ -375,17 +338,65 @@ public class JtagController : ControllerBase
/// </summary>
/// <param name="address">[TODO:parameter]</param>
/// <param name="port">[TODO:parameter]</param>
/// <param name="portNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
[HttpPost("BoundaryScan")]
[HttpPost("BoundaryScanAllPorts")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[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 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.Error is ArgumentException)
@@ -402,7 +413,7 @@ public class JtagController : ControllerBase
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class RemoteUpdater : ControllerBase
public class RemoteUpdateController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -418,8 +429,9 @@ public class RemoteUpdater : ControllerBase
/// <param name="bitstream3">比特流文件3</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
public async ValueTask<IResult> UploadBitstreams(
string address,
IFormFile? goldenBitream,
@@ -469,7 +481,7 @@ public class RemoteUpdater : ControllerBase
}
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)
@@ -522,9 +534,10 @@ public class RemoteUpdater : ControllerBase
/// <param name="port"> 设备端口 </param>
/// <param name="bitstreamNum"> 比特流位号 </param>
[HttpPost("DownloadBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
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);
// 下载比特流
var remoteUpdater = new RemoteUpdate.RemoteUpdateClient(address, port);
var remoteUpdater = new RemoteUpdateClient.RemoteUpdater(address, port);
var ret = await remoteUpdater.UpdateBitstream(bitstreamNum, fileBytes.Value);
if (ret.IsSuccessful)
@@ -571,9 +584,10 @@ public class RemoteUpdater : ControllerBase
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>总共上传比特流的数量</returns>
[HttpPost("DownloadMultiBitstreams")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[EnableCors("Users")]
[ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
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]);
if (!ret.IsSuccessful) return TypedResults.InternalServerError(ret.Error);
@@ -630,17 +644,18 @@ public class RemoteUpdater : ControllerBase
/// <param name="bitstreamNum">比特流编号</param>
/// <returns>操作结果</returns>
[HttpPost("HotResetBitstream")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ArgumentException), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
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);
if (ret.IsSuccessful)
{
logger.Info($"Device {address}即可 Update bitstream successfully");
logger.Info($"Device {address} Update bitstream successfully");
return TypedResults.Ok(ret.Value);
}
else
@@ -649,6 +664,160 @@ public class RemoteUpdater : ControllerBase
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>
[ApiController]
[Route("api/[controller]")]
public class Data : ControllerBase
public class DataController : ControllerBase
{
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
{
/*地址定义以2路输出4波形存储为例
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.Net;
using BsdlParser;
using DotNext;
using Newtonsoft.Json;
using WebProtocol;
@@ -27,6 +28,10 @@ public static class JtagAddr
/// Jtag Write Command
/// </summary>
public const UInt32 WRITE_CMD = 0x10_00_00_03;
/// <summary>
/// Jtag Speed Control
/// </summary>
public const UInt32 SPEED_CTRL = 0x10_00_00_04;
}
/// <summary>
@@ -773,12 +778,12 @@ public class Jtag
/// <summary>
/// [TODO:description]
/// </summary>
/// <param name="portNum">[TODO:parameter]</param>
/// <returns>[TODO:return]</returns>
public async ValueTask<Result<BitArray>> BoundaryScan(int portNum)
public async ValueTask<Result<BitArray>> BoundaryScan()
{
if (portNum <= 0)
return new(new ArgumentException("The number of port couldn't be negative", nameof(portNum)));
var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}");
// Clear Data
await MsgBus.UDPServer.ClearUDPData(this.address);
@@ -810,6 +815,48 @@ public class Jtag
var byteArray = Common.Number.UInt32ArrayToBytes(retData.Value);
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 DotNext;
namespace RemoteUpdate;
namespace RemoteUpdateClient;
static class RemoteUpdateClientAddr
static class RemoteUpdaterAddr
{
public const UInt32 Base = 0x20_00_00_00;
@@ -91,7 +91,7 @@ static class FlashAddr
/// <summary>
/// [TODO:description]
/// </summary>
public class RemoteUpdateClient
public class RemoteUpdater
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -112,7 +112,7 @@ public class RemoteUpdateClient
/// <param name="timeout">[TODO:parameter]</param>
/// <param name="timeoutForWait">[TODO:parameter]</param>
/// <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)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -142,7 +142,7 @@ public class RemoteUpdateClient
{
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);
if (!ret.IsSuccessful) return new(ret.Error);
if (!ret.Value) return new(new Exception("Enable write flash failed"));
@@ -150,7 +150,7 @@ public class RemoteUpdateClient
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.WriteSign,
this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_00_01, 0x00_00_00_01, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error);
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.Value) return new(new Exception("Send data to flash failed"));
}
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.WriteSign,
this.ep, RemoteUpdaterAddr.WriteSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error);
return ret.Value;
@@ -314,14 +314,14 @@ public class RemoteUpdateClient
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.Value) return new(new Exception("Write read control 2 failed"));
}
{
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)),
this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
@@ -330,7 +330,7 @@ public class RemoteUpdateClient
{
var ret = await UDPClientPool.ReadAddrWithWait(
this.ep, RemoteUpdateClientAddr.ReadSign,
this.ep, RemoteUpdaterAddr.ReadSign,
0x00_00_01_00, 0x00_00_01_00, this.timeoutForWait);
if (!ret.IsSuccessful) return new(ret.Error);
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);
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)));
var ret = await UDPClientPool.WriteAddr(
this.ep, RemoteUpdateClientAddr.HotResetCtrl,
this.ep, RemoteUpdaterAddr.HotResetCtrl,
((FlashAddr.Bitstream[bitstreamNum] << 8) | 1), this.timeout);
if (!ret.IsSuccessful) return new(ret.Error);
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" />
<div class="drawer-side">
<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">
<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>
<path d="M12 8v8"></path>
<path d="M8 12h8"></path>
@@ -16,7 +19,8 @@
添加元器件
</h3>
<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="6" y1="6" x2="18" y2="18"></line>
</svg>
@@ -25,7 +29,8 @@
<!-- 导航栏 -->
<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 === 'virtual' }" @click="activeTab = 'virtual'">虚拟外设</a>
</div>
@@ -34,19 +39,18 @@
<div class="px-6 py-4 border-b border-base-300">
<div class="join w-full">
<div class="join-item flex-1 relative">
<input
type="text"
placeholder="搜索..."
class="input input-bordered input-sm w-full pl-10"
v-model="searchQuery"
@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">
<input type="text" placeholder="搜索..." class="input input-bordered input-sm w-full pl-10"
v-model="searchQuery" @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>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<button class="btn btn-sm join-item" @click="searchComponents">搜索</button>
<button class="btn btn-sm join-item" @click="searchComponents">
搜索
</button>
</div>
</div>
@@ -57,14 +61,11 @@
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(component)">
<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
v-if="componentModules[component.type]"
:is="componentModules[component.type].default"
class="component-preview"
:size="getPreviewSize(component.type)"
/>
<component v-if="componentModules[component.type]" :is="componentModules[component.type].default"
class="component-preview" :size="getPreviewSize(component.type)" />
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
@@ -75,7 +76,9 @@
</div>
<!-- 无搜索结果 -->
<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>
<line x1="21" y1="21" x2="16.65" y2="16.65"></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"
@click="addTemplate(template)">
<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">
<img :src="template.thumbnailUrl || '/placeholder-template.png'" alt="Template thumbnail" class="max-h-full max-w-full object-contain" />
<div
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>
<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 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>
<line x1="21" y1="21" x2="16.65" y2="16.65"></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"
@click="addComponent(device)">
<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
v-if="componentModules[device.type]"
:is="componentModules[device.type].default"
class="component-preview"
:size="getPreviewSize(device.type)"
/>
<component v-if="componentModules[device.type]" :is="componentModules[device.type].default"
class="component-preview" :size="getPreviewSize(device.type)" />
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
@@ -140,7 +146,9 @@
</div>
<!-- 无搜索结果 -->
<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>
<line x1="21" y1="21" x2="16.65" y2="16.65"></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">
<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>
</svg>
返回
@@ -171,7 +180,8 @@
</template>
<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 定义
interface Props {
@@ -181,51 +191,54 @@ interface 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 = [
{ type: 'MechanicalButton', name: '机械按钮' },
{ type: 'Switch', name: '开关' },
{ type: 'Pin', name: '引脚' },
{ type: 'SMT_LED', name: '贴片LED' },
{ type: 'SevenSegmentDisplay', name: '数码管' },
{ type: 'HDMI', name: 'HDMI接口' },
{ type: 'DDR', name: 'DDR内存' },
{ type: 'ETH', name: '以太网接口' },
{ type: 'SD', name: 'SD卡插槽' },
{ type: 'SFP', name: 'SFP光纤模块' },
{ type: 'SMA', name: 'SMA连接器' },
{ type: 'MotherBoard', name: '主板' },
{ type: 'PG2L100H_FBG676', name: 'PG2L100H FBG676芯片' }
{ type: "MechanicalButton", name: "机械按钮" },
{ type: "Switch", name: "开关" },
{ type: "Pin", name: "引脚" },
{ type: "SMT_LED", name: "贴片LED" },
{ type: "SevenSegmentDisplay", name: "数码管" },
{ type: "HDMI", name: "HDMI接口" },
{ type: "DDR", name: "DDR内存" },
{ type: "ETH", name: "以太网接口" },
{ type: "SD", name: "SD卡插槽" },
{ type: "SFP", name: "SFP光纤模块" },
{ type: "SMA", name: "SMA连接器" },
{ type: "MotherBoard", name: "主板" },
{ type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
];
// --- 可用虚拟外设列表 ---
const availableVirtualDevices = [
{ type: 'DDS', name: '信号发生器' }
];
const availableVirtualDevices = [{ type: "DDS", name: "信号发生器" }];
// --- 可用模板列表 ---
const availableTemplates = ref([
{
name: 'PG2L100H 基础开发板',
id: 'PG2L100H_Pango100pro',
description: '包含主板和两个LED的基本设置',
path: '/src/components/equipments/templates/PG2L100H_Pango100pro.json',
thumbnailUrl: '/src/components/equipments/svg/motherboard.svg'
}
name: "PG2L100H 基础开发板",
id: "PG2L100H_Pango100pro",
description: "包含主板和两个LED的基本设置",
path: "/public/EquipmentTemplates/PG2L100H_Pango100pro.json",
thumbnailUrl: motherboardSvg,
},
]);
// 显示/隐藏组件菜单
const showComponentsMenu = computed({
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,
[type]: module
[type]: module,
};
console.log(`Loaded module for ${type}:`, module);
@@ -278,19 +291,19 @@ async function preloadComponentModules() {
function getPreviewSize(componentType: string): number {
// 根据组件类型返回适当的预览尺寸
const previewSizes: Record<string, number> = {
'MechanicalButton': 0.4, // 按钮较大,需要更小尺寸
'Switch': 0.35, // 开关较大,需要更小尺寸
'Pin': 0.8, // 引脚较小,可以大一些
'SMT_LED': 0.7, // LED可以保持适中
'SevenSegmentDisplay': 0.4, // 数码管较大,需要较小尺寸
'HDMI': 0.5, // HDMI接口较大
'DDR': 0.5, // DDR内存较大
'ETH': 0.5, // 以太网接口较大
'SD': 0.6, // SD卡插槽适中
'SFP': 0.4, // SFP光纤模块较大
'SMA': 0.7, // SMA连接器可以适中
'MotherBoard': 0.13, // 主板最大,需要最小尺寸
'DDS': 0.3 // 信号发生器较大,需要较小尺寸
MechanicalButton: 0.4, // 按钮较大,需要更小尺寸
Switch: 0.35, // 开关较大,需要更小尺寸
Pin: 0.8, // 引脚较小,可以大一些
SMT_LED: 0.7, // LED可以保持适中
SevenSegmentDisplay: 0.4, // 数码管较大,需要较小尺寸
HDMI: 0.5, // HDMI接口较大
DDR: 0.5, // DDR内存较大
ETH: 0.5, // 以太网接口较大
SD: 0.6, // SD卡插槽适中
SFP: 0.4, // SFP光纤模块较大
SMA: 0.7, // SMA连接器可以适中
MotherBoard: 0.13, // 主板最大,需要最小尺寸
DDS: 0.3, // 信号发生器较大,需要较小尺寸
};
// 返回对应尺寸如果没有特定配置则返回默认值0.5
@@ -306,7 +319,7 @@ function searchComponents() {
// 关闭菜单
function closeMenu() {
showComponentsMenu.value = false;
emit('close');
emit("close");
}
// 添加新元器件
@@ -317,9 +330,12 @@ async function addComponent(componentTemplate: { type: string; name: string }) {
// 尝试直接调用组件导出的getDefaultProps方法
if (moduleRef) {
if (typeof moduleRef.getDefaultProps === 'function') {
if (typeof moduleRef.getDefaultProps === "function") {
defaultProps = moduleRef.getDefaultProps();
console.log(`Got default props from ${componentTemplate.type}:`, defaultProps);
console.log(
`Got default props from ${componentTemplate.type}:`,
defaultProps,
);
} else {
// 回退到配置文件
console.log(`No getDefaultProps found for ${componentTemplate.type}`);
@@ -329,10 +345,10 @@ async function addComponent(componentTemplate: { type: string; name: string }) {
}
// 发送添加组件事件给父组件
emit('add-component', {
emit("add-component", {
type: componentTemplate.type,
name: componentTemplate.name,
props: defaultProps
props: defaultProps,
});
// 关闭菜单
@@ -349,56 +365,60 @@ async function addTemplate(template: any) {
}
const templateData = await response.json();
console.log('加载模板:', templateData);
console.log("加载模板:", templateData);
// 发出事件,将模板数据传递给父组件
emit('add-template', {
emit("add-template", {
id: template.id,
name: template.name,
template: templateData
template: templateData,
});
// 关闭菜单
closeMenu();
} catch (error) {
console.error('加载模板出错:', error);
alert('无法加载模板文件,请检查控制台错误信息');
console.error("加载模板出错:", error);
alert("无法加载模板文件,请检查控制台错误信息");
}
}
// 过滤后的元器件列表 (用于菜单)
const filteredComponents = computed(() => {
if (!searchQuery.value || activeTab.value !== 'components') {
if (!searchQuery.value || activeTab.value !== "components") {
return availableComponents;
}
const query = searchQuery.value.toLowerCase();
return availableComponents.filter(component =>
return availableComponents.filter(
(component) =>
component.name.toLowerCase().includes(query) ||
component.type.toLowerCase().includes(query)
component.type.toLowerCase().includes(query),
);
});
// 过滤后的模板列表 (用于菜单)
const filteredTemplates = computed(() => {
if (!searchQuery.value || activeTab.value !== 'templates') {
if (!searchQuery.value || activeTab.value !== "templates") {
return availableTemplates.value;
}
const query = searchQuery.value.toLowerCase();
return availableTemplates.value.filter(template =>
return availableTemplates.value.filter(
(template) =>
template.name.toLowerCase().includes(query) ||
(template.description && template.description.toLowerCase().includes(query))
(template.description &&
template.description.toLowerCase().includes(query)),
);
});
// 过滤后的虚拟外设列表 (用于菜单)
const filteredVirtualDevices = computed(() => {
if (!searchQuery.value || activeTab.value !== 'virtual') {
if (!searchQuery.value || activeTab.value !== "virtual") {
return availableVirtualDevices;
}
const query = searchQuery.value.toLowerCase();
return availableVirtualDevices.filter(device =>
return availableVirtualDevices.filter(
(device) =>
device.name.toLowerCase().includes(query) ||
device.type.toLowerCase().includes(query)
device.type.toLowerCase().includes(query),
);
});
@@ -427,6 +447,7 @@ onMounted(() => {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
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);
// 更新组件属性方法

View File

@@ -44,19 +44,13 @@
</CollapsibleSection>
<CollapsibleSection title="组件功能" v-model:isExpanded="componentCapsExpanded" status="default" class="mt-4">
<div v-if="componentData && componentData.type">
<component
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 id="ComponentCapabilities" ref="ComponentCapabilities"></div>
<div v-if="!(componentData && componentData.type)" class="text-gray-400">
选择元件以查看其功能
</div>
<div v-else-if="!componentCaps?.hasChildNodes()" class="text-gray-400">
该组件没有提供特殊功能
</div>
</CollapsibleSection>
<!-- 未来可以在这里添加更多的分区 -->
@@ -79,9 +73,12 @@ import { type PropertyConfig } from "@/components/equipments/componentConfig"; /
import CollapsibleSection from "./CollapsibleSection.vue"; // 可折叠区域组件
import PropertyEditor from "./PropertyEditor.vue"; // 属性编辑器组件
import DDSPropertyEditor from "./equipments/DDSPropertyEditor.vue"; // DDS专用属性编辑器组件
import { ref, computed, watch, shallowRef, markRaw, h, createApp } from "vue"; // Vue核心API
import { isNull, isUndefined } from "lodash";
import type { JSX } from "vue/jsx-runtime";
import {
ref,
computed,
watch,
useTemplateRef,
} from "vue"; // Vue核心API
// 引脚接口定义
interface Pin {
@@ -98,11 +95,13 @@ const props = defineProps<{
}>();
// 控制各个属性分区的展开状态
const propertySectionExpanded = ref(true); // 基本属性区域默认展开
const propertySectionExpanded = ref(false); // 基本属性区域默认展开
const pinsSectionExpanded = ref(false); // 引脚配置区域默认折叠
const componentCapsExpanded = ref(true); // 组件功能区域默认展开
const wireSectionExpanded = ref(false); // 连线管理区域默认折叠
const componentCaps = useTemplateRef("ComponentCapabilities");
// DDS组件特殊属性的本地状态
const ddsProperties = ref({
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>
<style scoped>

View File

@@ -6,30 +6,34 @@
<!-- Input File -->
<fieldset class="fieldset w-full">
<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>
</fieldset>
<!-- Upload Button -->
<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 }}
</div>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useEquipments } from "@/stores/equipments";
import { computed, ref, useTemplateRef, onMounted } from "vue";
import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash";
interface Props {
uploadEvent?: Function;
downloadEvent?: Function;
uploadEvent?: (file: File) => Promise<boolean>;
downloadEvent?: () => Promise<boolean>;
maxMemory?: number;
defaultFile?: string;
}
const props = withDefaults(defineProps<Props>(), {
@@ -41,16 +45,23 @@ const emits = defineEmits<{
}>();
const dialog = useDialogStore();
const eqps = useEquipments();
const isUploading = ref(false);
const buttonText = computed(() => {
return isUndefined(props.downloadEvent) ? "上传" : "上传并下载";
});
// var bitstream: File | null = null;
const bitstream = defineModel<File | undefined>();
const fileInput = computed(() => {
return !isUndefined(bitstream.value) ? bitstream.value.name : "";
const fileInput = useTemplateRef("fileInput");
const bitstream = defineModel("bitstreamFile", {
type: File,
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 {
@@ -85,9 +96,9 @@ async function handleClick(event: Event): Promise<void> {
return;
}
// Upload - 修改这里传递bitstream.value而不是bitstream
isUploading.value = true;
try {
const ret = await props.uploadEvent(event, bitstream.value);
const ret = await props.uploadEvent(bitstream.value);
if (isUndefined(props.downloadEvent)) {
if (ret) {
dialog.info("上传成功");
@@ -95,6 +106,10 @@ async function handleClick(event: Event): Promise<void> {
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false;
return;
}
} catch (e) {
dialog.error("上传失败");
console.error(e);
@@ -110,8 +125,9 @@ async function handleClick(event: Event): Promise<void> {
dialog.error("下载失败");
console.error(e);
}
}
isUploading.value = false;
}
</script>
<style scoped lang="postcss">

View File

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

View File

@@ -46,7 +46,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import { ref, computed, watch, onMounted, watchEffect } from 'vue';
import Pin from './Pin.vue';
// 存储Pin引用
@@ -94,6 +94,10 @@ const currentWaveformIndex = ref(0);
const waveformNames = ['正弦波', '方波', '三角波', '锯齿波'];
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
watchEffect(()=>{
console.log(`DDS Porperties: ${frequency.value} ${timebase.value}`)
})
// 波形函数集合
interface WaveformFunction {
(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">
<div class="dds-editor-container">
<div class="dds-display">
@@ -9,27 +10,35 @@
<path :d="currentWaveformPath" stroke="lime" stroke-width="2" fill="none" />
<!-- 频率和相位显示 -->
<text x="20" y="25" fill="#0f0" font-size="14">{{ displayFrequency }}</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>
<text x="20" y="25" fill="#0f0" font-size="14">
{{ displayFrequency }}
</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>
<!-- 时基控制 -->
<div class="timebase-controls">
<button class="timebase-button" @click="decreaseTimebase">-</button>
<button class="timebase-button" @click="decreaseTimebase">
-
</button>
<span class="timebase-label">时基</span>
<button class="timebase-button" @click="increaseTimebase">+</button>
<button class="timebase-button" @click="increaseTimebase">
+
</button>
</div>
</div>
<!-- 波形选择区 -->
<div class="waveform-selector">
<div
v-for="(name, index) in waveformNames"
:key="`wave-${index}`"
:class="['waveform-option', { active: currentWaveformIndex === index }]"
@click="selectWaveform(index)"
>
<div v-for="(name, index) in waveformNames" :key="`wave-${index}`" :class="[
'waveform-option',
{ active: currentWaveformIndex === index },
]" @click="selectWaveform(index)">
{{ name }}
</div>
</div>
@@ -39,15 +48,14 @@
<div class="control-group">
<span class="control-label">频率:</span>
<div class="control-buttons">
<button class="control-button" @click="decreaseFrequency">-</button>
<input
v-model="frequencyInput"
@blur="applyFrequencyInput"
@keyup.enter="applyFrequencyInput"
class="control-input"
type="text"
/>
<button class="control-button" @click="increaseFrequency">+</button>
<button class="control-button" @click="decreaseFrequency">
-
</button>
<input v-model="frequencyInput" @blur="applyFrequencyInput" @keyup.enter="applyFrequencyInput"
class="control-input" type="text" />
<button class="control-button" @click="increaseFrequency">
+
</button>
</div>
</div>
@@ -55,13 +63,8 @@
<span class="control-label">相位:</span>
<div class="control-buttons">
<button class="control-button" @click="decreasePhase">-</button>
<input
v-model="phaseInput"
@blur="applyPhaseInput"
@keyup.enter="applyPhaseInput"
class="control-input"
type="text"
/>
<input v-model="phaseInput" @blur="applyPhaseInput" @keyup.enter="applyPhaseInput" class="control-input"
type="text" />
<button class="control-button" @click="increasePhase">+</button>
</div>
</div>
@@ -69,53 +72,48 @@
<!-- 自定义波形输入 -->
<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>
<input
v-model="customWaveformExpression"
class="function-input"
<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]"
@keyup.enter="applyCustomWaveform"
/>
<button class="apply-button" @click="applyCustomWaveform">应用</button>
@keyup.enter="applyCustomWaveform" />
<button class="apply-button" @click="applyCustomWaveform">
应用
</button>
</div>
<div class="example-functions">
<div class="example-label">示例函数:</div>
<div class="example-buttons">
<button
class="example-button"
@click="applyExampleFunction('sin(t)')"
>正弦波</button>
<button
class="example-button"
@click="applyExampleFunction('sin(t)^3')"
>立方正弦</button> <button
class="example-button"
@click="applyExampleFunction('((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75')"
>心形函数</button>
<button class="example-button" @click="applyExampleFunction('sin(t)')">
正弦波
</button>
<button class="example-button" @click="applyExampleFunction('sin(t)^3')">
立方正弦
</button>
<button class="example-button" @click="
applyExampleFunction(
'((x)^(2/3)+0.9*sqrt(3.3-(x)^2)*sin(10*PI*(x)))*0.75',
)
">
心形函数
</button>
</div>
</div>
<div class="drawing-area">
<div class="section-heading">波形绘制</div>
<div
class="waveform-canvas-container"
ref="canvasContainer"
>
<canvas
ref="drawingCanvas"
class="drawing-canvas"
width="280"
height="100"
@mousedown="startDrawing"
@mousemove="draw"
@mouseup="stopDrawing"
@mouseleave="stopDrawing"
></canvas>
<div class="waveform-canvas-container" ref="canvasContainer">
<canvas ref="drawingCanvas" class="drawing-canvas" width="280" height="100" @mousedown="startDrawing"
@mousemove="draw" @mouseup="stopDrawing" @mouseleave="stopDrawing"></canvas>
<div class="canvas-actions">
<button class="canvas-button" @click="clearCanvas">清除</button>
<button class="canvas-button" @click="applyDrawnWaveform">应用绘制</button>
<button class="canvas-button" @click="clearCanvas">
清除
</button>
<button class="canvas-button" @click="applyDrawnWaveform">
应用绘制
</button>
</div>
</div>
</div>
@@ -125,22 +123,25 @@
<div class="saved-waveforms">
<div class="section-heading">波形存储槽</div>
<div class="slot-container">
<div
v-for="(slot, index) in waveformSlots"
:key="`slot-${index}`"
:class="['waveform-slot', { empty: !slot.name }]"
@click="loadWaveformSlot(index)"
>
<span class="slot-name">{{ slot.name || `${index+1}` }}</span>
<button
class="save-button"
@click.stop="saveCurrentToSlot(index)"
>
<div v-for="(slot, index) in waveformSlots" :key="`slot-${index}`"
:class="['waveform-slot', { empty: !slot.name }]" @click="loadWaveformSlot(index)">
<span class="slot-name">{{
slot.name || `${index + 1}`
}}</span>
<button class="save-button" @click.stop="saveCurrentToSlot(index)">
保存
</button>
</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>
</CollapsibleSection>
@@ -148,22 +149,33 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import CollapsibleSection from '../CollapsibleSection.vue';
import { ref, computed, watch, onMounted } from "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<{
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 phase = ref(props.modelValue?.phase || 0);
const frequency = ref<number>(props.modelValue?.frequency || 1000);
const phase = ref<number>(props.modelValue?.phase || 0);
const timebase = ref(props.modelValue?.timebase || 1); // 时基默认为1倍
const currentWaveformIndex = ref(0);
const waveformNames = ['正弦波', '方波', '三角波', '锯齿波', '自定义'];
const waveforms = ['sine', 'square', 'triangle', 'sawtooth', 'custom'];
const waveformNames = ["正弦波", "方波", "三角波", "锯齿波", "自定义"];
const waveforms = ["sine", "square", "triangle", "sawtooth", "custom"];
const isApplying = ref(false);
// 波形函数集合
interface WaveformFunction {
@@ -176,45 +188,72 @@ interface WaveformFunctions {
const waveformFunctions: WaveformFunctions = {
// 正弦波函数: sin(2π*x + φ)
sine: (x: number, width: number, height: number, phaseRad: number): number => {
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad);
sine: (
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;
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;
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;
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会在应用自定义表达式时更新
}
},
};
// 输入控制
const frequencyInput = ref(formatFrequency(frequency.value));
const phaseInput = ref(phase.value.toString());
const customWaveformExpression = ref('');
const customWaveformExpression = ref("");
// 波形槽
const waveformSlots = ref<{ name: string; type: string; data: number[][] | null }[]>([
{ name: '正弦波', type: 'sine', data: null },
{ name: '方波', type: 'square', data: null },
{ name: '三角波', type: 'triangle', data: null },
{ name: '', type: '', data: null }
const waveformSlots = ref<
{ name: string; type: string; data: number[][] | null }[]
>([
{ name: "正弦波", type: "sine", 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 yOffset = 30;
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;
let path = '';
let path = "";
// 使用函数生成波形
if (currentWaveform === 'custom') {
if (currentWaveform === "custom") {
// 自定义波形
if (drawPoints.value.length > 0) {
path = `M${xOffset + drawPoints.value[0][0]},${yOffset + drawPoints.value[0][1]}`;
@@ -308,13 +347,14 @@ const currentWaveformPath = computed(() => {
}
} 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++) {
const scaledX = x * scaleFactor;
const y = waveFunction(scaledX, width, height, phaseRadians);
// 注意心形函数可能返回undefined或极端值需要处理
if (typeof y === 'number' && isFinite(y)) {
if (typeof y === "number" && isFinite(y)) {
path += ` L${x + xOffset},${yOffset + height / 2 - y}`;
} else {
// 如果返回异常值,保持当前位置
@@ -324,7 +364,8 @@ const currentWaveformPath = computed(() => {
}
} else {
// 使用预定义的波形函数
const waveFunction = waveformFunctions[currentWaveform as keyof typeof waveformFunctions];
const waveFunction =
waveformFunctions[currentWaveform as keyof typeof waveformFunctions];
// 生成路径点
path = `M${xOffset},${yOffset + height / 2}`;
@@ -346,6 +387,56 @@ function selectWaveform(index: number) {
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() {
if (frequency.value < 10) {
frequency.value += 0.1;
@@ -390,11 +481,11 @@ function applyFrequencyInput() {
let value = parseFloat(frequencyInput.value);
// 处理单位
if (frequencyInput.value.includes('MHz')) {
if (frequencyInput.value.includes("MHz")) {
value = parseFloat(frequencyInput.value) * 1000000;
} else if (frequencyInput.value.includes('kHz')) {
} else if (frequencyInput.value.includes("kHz")) {
value = parseFloat(frequencyInput.value) * 1000;
} else if (frequencyInput.value.includes('Hz')) {
} else if (frequencyInput.value.includes("Hz")) {
value = parseFloat(frequencyInput.value);
}
@@ -444,11 +535,11 @@ function applyCustomWaveform() {
try {
// 创建自定义波形函数
createCustomWaveformFunction();
currentWaveformIndex.value = waveforms.indexOf('custom');
currentWaveformIndex.value = waveforms.indexOf("custom");
drawCustomWaveformFromExpression();
updateModelValue();
} catch (error) {
console.error('Invalid expression:', error);
console.error("Invalid expression:", error);
// 这里可以添加一些错误提示
}
}
@@ -466,23 +557,29 @@ function createCustomWaveformFunction() {
const expression = customWaveformExpression.value;
// 导入 mathjs
import('mathjs').then((math) => {
import("mathjs")
.then((math) => {
try {
// 预编译表达式以提高性能
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 {
// 相位调整 - 将相位转换为x轴的位移
const phaseShift = phaseRad / (2 * Math.PI);
// 标准化参数使x落在 0-1 范围内,并应用相位
let normalizedX = ((x / width) + phaseShift) % 1;
let normalizedX = (x / width + phaseShift) % 1;
// 心形函数需要x映射到[-1.5,1.5]范围,以确保完整显示心形
// 这个范围比[-1,1]稍大,确保心形两侧完整显示
const scaledX = (normalizedX * 3) - 1.5;
const scaledX = normalizedX * 3 - 1.5;
// 创建参数对象,包括各种变量和常量供表达式使用
const scope = {
@@ -496,7 +593,11 @@ function createCustomWaveformFunction() {
let result = compiledExpression.evaluate(scope);
// 确保结果在合理范围内
if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) {
if (
typeof result !== "number" ||
isNaN(result) ||
!isFinite(result)
) {
result = 0;
}
@@ -505,9 +606,9 @@ function createCustomWaveformFunction() {
result = Math.max(-1, Math.min(1, result));
// 返回适当的振幅
return height/2 * result;
return (height / 2) * result;
} catch (e) {
console.error('Error evaluating expression:', e);
console.error("Error evaluating expression:", e);
return 0;
}
};
@@ -515,23 +616,31 @@ function createCustomWaveformFunction() {
// 立即更新波形显示
updateModelValue();
} 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 => {
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad);
waveformFunctions.custom = (
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() {
const canvas = drawingCanvas.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
const width = canvas.width;
@@ -539,13 +648,13 @@ function drawCustomWaveformFromExpression() {
// 清除画布
ctx.clearRect(0, 0, width, height);
ctx.strokeStyle = '#0f0';
ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2;
// 创建自定义函数 - 先创建函数再绘制
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 y = event.clientY - rect.top;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
// 清除画布
@@ -624,7 +733,7 @@ function startDrawing(event: MouseEvent) {
// 开始新的绘制
drawPoints.value = [[x, y]];
ctx.beginPath();
ctx.strokeStyle = '#0f0';
ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2;
ctx.moveTo(x, y);
}
@@ -635,7 +744,7 @@ function draw(event: MouseEvent) {
const canvas = drawingCanvas.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
const rect = canvas.getBoundingClientRect();
@@ -658,7 +767,7 @@ function clearCanvas() {
const canvas = drawingCanvas.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
@@ -667,7 +776,7 @@ function clearCanvas() {
function applyDrawnWaveform() {
if (drawPoints.value.length > 0) {
currentWaveformIndex.value = waveforms.indexOf('custom');
currentWaveformIndex.value = waveforms.indexOf("custom");
updateModelValue();
}
}
@@ -677,9 +786,11 @@ function saveCurrentToSlot(index: number) {
waveformSlots.value[index] = {
name: waveformNames[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]
: null
: null,
};
}
@@ -692,17 +803,17 @@ function loadWaveformSlot(index: number) {
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];
// 更新画布
const canvas = drawingCanvas.value;
if (canvas) {
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.strokeStyle = '#0f0';
ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2;
for (let i = 0; i < slot.data.length; i++) {
@@ -730,9 +841,12 @@ function updateModelValue() {
phase: phase.value,
timebase: timebase.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];
// 绘制到画布上
const canvas = drawingCanvas.value;
if (canvas && currentWaveformIndex.value === waveforms.indexOf('custom')) {
const ctx = canvas.getContext('2d');
if (
canvas &&
currentWaveformIndex.value === waveforms.indexOf("custom")
) {
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.strokeStyle = '#0f0';
ctx.strokeStyle = "#0f0";
ctx.lineWidth = 2;
for (let i = 0; i < drawPoints.value.length; i++) {
@@ -790,8 +910,14 @@ onMounted(() => {
});
// 监听model变化
watch(() => props.modelValue, (newVal) => {
if (newVal && newVal.frequency !== undefined && newVal.frequency !== frequency.value) {
watch(
() => props.modelValue,
(newVal) => {
if (
newVal &&
newVal.frequency !== undefined &&
newVal.frequency !== frequency.value
) {
frequency.value = newVal.frequency;
frequencyInput.value = formatFrequency(frequency.value);
}
@@ -801,7 +927,11 @@ watch(() => props.modelValue, (newVal) => {
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;
}
@@ -811,7 +941,9 @@ watch(() => props.modelValue, (newVal) => {
currentWaveformIndex.value = index;
}
}
}, { deep: true });
},
{ deep: true },
);
</script>
<style scoped>
@@ -1002,7 +1134,9 @@ watch(() => props.modelValue, (newVal) => {
border-radius: 4px;
color: var(--base-content, #a6adbb);
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;
}

View File

@@ -1,12 +1,11 @@
<template>
<div class="button-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="400 400 800 800"
class="mechanical-button"
>
<div class="button-container" :style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="400 400 800 800"
class="mechanical-button">
<!-- defs 和按钮底座保持不变 -->
<defs>
<filter id="btn-shadow">
@@ -26,7 +25,8 @@
<stop stop-color="#171717" offset="0" />
<stop stop-color="#4b4b4b" offset="1" />
</linearGradient>
</defs> <!-- 按钮底座 -->
</defs>
<!-- 按钮底座 -->
<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" />
@@ -38,65 +38,50 @@
<!-- 按钮主体 -->
<circle r="220" cx="800" cy="800" fill="black" filter="url(#btn-shadow)" />
<circle
:r="btnHeight"
cx="800"
cy="800"
:fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'"
fill-opacity="0.9"
@mousedown="toggleButtonState(true)"
@mouseup="toggleButtonState(false)"
@mouseleave="toggleButtonState(false)"
style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;"
/>
<circle :r="btnHeight" cx="800" cy="800" :fill="isKeyPressed ? 'url(#pressed)' : 'url(#normal)'"
fill-opacity="0.9" @mousedown="toggleButtonState(true)" @mouseup="toggleButtonState(false)"
@mouseleave="toggleButtonState(false)" style="
pointer-events: auto;
transition: all 20ms ease-in-out;
cursor: pointer;
" />
<!-- 按键文字 - 仅显示绑定的按键 -->
<text
v-if="bindKeyDisplay"
x="800"
y="800"
font-size="310"
text-anchor="middle"
dominant-baseline="central"
fill="#ccc"
style="font-family: Arial; filter: url(#btn-shadow); user-select: none; pointer-events: none; mix-blend-mode: overlay;"
>
<text v-if="bindKeyDisplay" x="800" y="800" font-size="310" text-anchor="middle" dominant-baseline="central"
fill="#ccc" style="
font-family: Arial;
filter: url(#btn-shadow);
user-select: none;
pointer-events: none;
mix-blend-mode: overlay;
">
{{ bindKeyDisplay }}
</text>
</svg>
<!-- 渲染自定义引脚数组 -->
<div v-for="pin in props.pins" :key="pin.pinId"
:style="{
<div v-for="pin in props.pins" :key="pin.pinId" :style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)',
zIndex: 3,
pointerEvents: 'auto'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
pointerEvents: 'auto',
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`">
<Pin
:ref="el => { if(el) pinRefs[pin.pinId] = el }"
direction="output"
type="digital"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
:size="0.8"
:componentId="props.componentId"
@value-change="handlePinValueChange"
@pin-click="handlePinClick"
/>
<Pin :ref="(el) => {
if (el) pinRefs[pin.pinId] = el;
}
" direction="output" type="digital" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId"
:size="0.8" :componentId="props.componentId" @value-change="handlePinValueChange" @pin-click="handlePinClick" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import Pin from './Pin.vue';
import { notifyConstraintChange } from '../../stores/constraints';
import { ref, onMounted, onUnmounted, computed } from "vue";
import Pin from "./Pin.vue";
import { useConstraintsStore } from "../../stores/constraints";
const { notifyConstraintChange } = useConstraintsStore();
// 存储多个Pin引用
const pinRefs = ref<Record<string, any>>({});
@@ -116,16 +101,16 @@ interface ButtonProps {
const props = withDefaults(defineProps<ButtonProps>(), {
size: 1,
bindKey: '',
componentId: 'button-default',
bindKey: "",
componentId: "button-default",
pins: () => [
{
pinId: 'BTN',
constraint: '',
pinId: "BTN",
constraint: "",
x: 80,
y: 140
}
]
y: 140,
},
],
});
// 计算实际宽高
@@ -133,17 +118,19 @@ const width = 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([
'update:bindKey',
'update:constraint',
'press',
'release',
'click',
'value-change',
'pin-click'
"update:bindKey",
"update:constraint",
"press",
"release",
"click",
"value-change",
"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值变化
function handlePinValueChange(value: any) {
emit('value-change', value);
emit("value-change", value);
}
// 处理Pin点击事件
function handlePinClick(info: any) {
emit('pin-click', info);
emit("pin-click", info);
}
// --- 按键状态逻辑 ---
@@ -168,24 +155,24 @@ function toggleButtonState(isPressed: boolean) {
// 发出事件通知父组件
if (isPressed) {
emit('press');
emit("press");
// 如果有约束,通知约束状态变化为高电平
// 对所有引脚应用相同的状态
if (props.pins) {
props.pins.forEach(pin => {
props.pins.forEach((pin) => {
if (pin.constraint) {
notifyConstraintChange(pin.constraint, 'high');
notifyConstraintChange(pin.constraint, "high");
}
});
}
} else {
emit('release');
emit('click');
emit("release");
emit("click");
// 如果有约束,通知约束状态变化为低电平
if (props.pins) {
props.pins.forEach(pin => {
props.pins.forEach((pin) => {
if (pin.constraint) {
notifyConstraintChange(pin.constraint, 'low');
notifyConstraintChange(pin.constraint, "low");
}
});
}
@@ -202,11 +189,11 @@ function handleKeyDown(event: KeyboardEvent) {
// --- 生命周期钩子 ---
onMounted(() => {
document.addEventListener('keydown', handleKeyDown);
document.addEventListener("keydown", handleKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener("keydown", handleKeyDown);
});
// 向外暴露方法
@@ -216,30 +203,38 @@ defineExpose({
// 按钮特有属性
bindKey: props.bindKey,
componentId: props.componentId,
pins: props.pins
pins: props.pins,
}),
// 获取引脚位置
getPinPosition: (pinId: string) => {
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);
// 如果是自定义的引脚ID
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) {
console.log(`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`, { x: customPin.x, y: customPin.y });
console.log(`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`, {
x: customPin.x,
y: customPin.y,
});
// 考虑组件尺寸的缩放
// 这里的x和y是针对标准尺寸size=1的坐标需要根据实际size调整
const scaledX = customPin.x * props.size;
const scaledY = customPin.y * props.size;
console.log(`[MechanicalButton] 返回缩放后的坐标:`, { x: scaledX, y: scaledY });
console.log(`[MechanicalButton] 返回缩放后的坐标:`, {
x: scaledX,
y: scaledY,
});
return {
x: scaledX,
y: scaledY
y: scaledY,
};
} else {
console.log(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`);
@@ -249,7 +244,7 @@ defineExpose({
}
console.log(`[MechanicalButton] 返回null未找到引脚`);
return null;
}
},
});
</script>
@@ -258,15 +253,15 @@ defineExpose({
export function getDefaultProps() {
return {
size: 1,
bindKey: '',
bindKey: "",
pins: [
{
pinId: 'BTN',
constraint: '',
pinId: "BTN",
constraint: "",
x: 80,
y: 140
}
]
y: 140,
},
],
};
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="motherboard-container" :style="{
<div class="motherboard-container" v-bind="$attrs" :style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
@@ -9,35 +9,41 @@
<image href="../equipments/svg/motherboard.svg" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" />
</svg>
</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>
<script setup lang="tsx">
import { JtagClient, type FileParameter } from "@/APIClient";
import z from "zod";
import { useEquipments } from "@/stores/equipments";
import UploadCard from "@/components/UploadCard.vue";
import { useDialogStore } from "@/stores/dialog";
import { toNumber, toString } from "lodash";
import { computed, ref, defineComponent, watch } from "vue";
import MotherBoardCaps from "./MotherBoardCaps.vue";
import { ref, computed, watchEffect, inject } from "vue";
import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
import { toNumber } from "lodash";
// 主板特有属性
interface MotherBoardProps {
export interface MotherBoardProps {
size?: number;
jtagAddr?: string;
jtagPort?: string;
boardAddr?: string;
boardPort?: string;
componentId?: string;
}
const emit = defineEmits<{
(e: "pinClick", pinId: string): void;
}>();
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 height = computed(() => 600 * props.size);
const eqps = useEquipments();
watch([props.jtagAddr, props.jtagPort], () => {
eqps.jtagIPAddr = props.jtagAddr;
eqps.jtagPort = props.jtagPort;
});
// 向外暴露方法
defineExpose({
@@ -47,144 +53,17 @@ defineExpose({
}),
// 主板没有引脚但为了接口一致性提供一个空的getPinPosition方法
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 lang="tsx">
// 添加一个静态方法来获取默认props
export function getDefaultProps() {
export function getDefaultProps(): MotherBoardProps {
return {
size: 1,
jtagAddr: "127.0.0.1",
jtagPort: "1234",
boardAddr: "127.0.0.1",
boardPort: "1234",
componentId: "DefaultMotherBoardID",
};
}
</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>
<div class="chip-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 400 400"
class="fbg676-chip"
> <!-- 芯片外边框 - 用多段path代替rect这样可以实现缺角 -->
<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"
/>
<div class="chip-container" :style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}">
<svg xmlns="http://www.w3.org/2000/svg" :width="width" :height="height" viewBox="0 0 400 400" class="fbg676-chip">
<!-- 芯片外边框 - 用多段path代替rect这样可以实现缺角 -->
<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" />
<!-- 芯片内层 - 增大尺寸 -->
<rect width="280" height="280" x="60" y="60" fill="#0F1211" rx="2" ry="2" />
</svg>
<!-- 渲染芯片引脚 --> <div v-for="pin in computedPins" :key="pin.pinId"
:style="{
<!-- 渲染芯片引脚 -->
<div v-for="pin in computedPins" :key="pin.pinId" :style="{
position: 'absolute',
left: `${(pin.x || 0) * props.size * 0.37}px`,
top: `${(pin.y || 0) * props.size * 0.37}px`,
transform: 'translate(-50%, -50%)'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${(pin.x || 0) * props.size * 0.37}`"
:data-pin-y="`${(pin.y || 0) * props.size * 0.37}`"> <Pin
:ref="el => { if(el) pinRefs[pin.pinId] = el }"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
:size="0.35"
@pin-click="$emit('pin-click', $event)"
/>
transform: 'translate(-50%, -50%)',
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${(pin.x || 0) * props.size * 0.37}`"
:data-pin-y="`${(pin.y || 0) * props.size * 0.37}`">
<Pin :ref="(el) => {
if (el) pinRefs[pin.pinId] = el;
}
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" :size="0.35"
@pin-click="$emit('pin-click', $event)" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import Pin from './Pin.vue';
import { ref, computed } from "vue";
import Pin from "./Pin.vue";
// 存储多个Pin引用
const pinRefs = ref<Record<string, any>>({});
@@ -55,7 +48,7 @@ interface ChipProps {
const props = withDefaults(defineProps<ChipProps>(), {
size: 1,
pins: () => []
pins: () => [],
});
// 计算尺寸
@@ -104,9 +97,9 @@ const computedPins = computed(() => {
for (let col = 0; col < pinColumns; col++) {
pins.push({
pinId: `pin_${pinIndex++}`,
constraint: '',
constraint: "",
x: xStart + col * xStep,
y: yStart + row * yStep
y: yStart + row * yStep,
});
}
}
@@ -116,15 +109,15 @@ const computedPins = computed(() => {
defineExpose({
getInfo: () => ({
chipType: 'PG2L100H_FBG676',
direction: 'inout',
type: 'digital',
pins: computedPins.value
chipType: "PG2L100H_FBG676",
direction: "inout",
type: "digital",
pins: computedPins.value,
}),
getPinPosition: (pinId: string) => {
// 返回指定引脚ID的位置
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) {
// 考虑组件尺寸的缩放应用0.37的系数确保居中对齐
@@ -133,13 +126,13 @@ defineExpose({
return {
x: scaledX,
y: scaledY
y: scaledY,
};
}
return null;
}
return null;
}
},
});
</script>
@@ -148,7 +141,7 @@ defineExpose({
export function getDefaultProps() {
return {
size: 1,
pins: [] // 默认不提供引脚由computedPins计算生成
pins: [], // 默认不提供引脚由computedPins计算生成
};
}
</script>

View File

@@ -1,30 +1,14 @@
<template>
<svg
:width="width"
:height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component"
> <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<svg :width="width" :height="height" :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight" class="pin-component">
<g :transform="`translate(${viewBoxWidth / 2}, ${viewBoxHeight / 2})`">
<g>
<g transform="translate(-12.5, -12.5)"> <!-- 添加一个透明的更大区域来增强点击能力但比原来小一点 -->
<circle
cx="12.5"
cy="12.5"
r="5"
fill="transparent"
class="interactive"
@click.stop="handlePinClick"
@mousedown.stop
@touchstart.stop
:data-pin-element="`${props.pinId}`"
<g transform="translate(-12.5, -12.5)">
<!-- 添加一个透明的更大区域来增强点击能力但比原来小一点 -->
<circle cx="12.5" cy="12.5" r="5" fill="transparent" class="interactive" @click.stop="handlePinClick"
@mousedown.stop @touchstart.stop :data-pin-element="`${props.pinId}`"
:data-component-id="props.componentId" />
<!-- 实际可见的引脚圆点 -->
<circle
:style="{ fill: pinColor }"
cx="12.5"
cy="12.5"
r="3.75"
pointer-events="none"
<circle :style="{ fill: pinColor }" cx="12.5" cy="12.5" r="3.75" pointer-events="none"
:data-pin-visual="`${props.pinId}`" />
</g>
</g>
@@ -33,32 +17,33 @@
</template>
<script setup lang="ts">
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints';
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { useConstraintsStore } from "../../stores/constraints";
const { getConstraintColor, onConstraintStateChange } = useConstraintsStore();
interface Props {
size?: number;
label?: string;
constraint?: string;
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
direction?: "input" | "output" | "inout";
type?: "digital" | "analog";
pinId?: string; // 添加引脚ID属性用于唯一标识
componentId?: string; // 添加组件ID属性关联到父组件
}
const props = withDefaults(defineProps<Props>(), {
size: 1,
label: 'PIN',
constraint: '',
direction: 'input',
type: 'digital',
pinId: 'pin-default', // 默认ID
componentId: '' // 默认空字符串
label: "PIN",
constraint: "",
direction: "input",
type: "digital",
pinId: "pin-default", // 默认ID
componentId: "", // 默认空字符串
});
const emit = defineEmits([
'value-change',
'pin-click' // 新增Pin点击事件
"value-change",
"pin-click", // 新增Pin点击事件
]);
// 内部状态
@@ -71,15 +56,15 @@ function handlePinClick(event: MouseEvent) {
const rect = target.getBoundingClientRect();
const pinCenter = {
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,
constraint: props.constraint,
type: props.type,
direction: props.direction,
position: pinCenter,
originalEvent: event
originalEvent: event,
});
}
@@ -89,7 +74,7 @@ const viewBoxWidth = computed(() => 15);
const viewBoxHeight = computed(() => 15);
const getColorByType = computed(() => {
return props.type === 'analog' ? '#2a6099' : '#444';
return props.type === "analog" ? "#2a6099" : "#444";
});
// 根据约束电平状态计算引脚颜色
@@ -105,7 +90,7 @@ onMounted(() => {
if (props.constraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
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) {
unsubscribe();
unsubscribe = null;
@@ -125,19 +112,20 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
emit('value-change', { constraint, level });
emit("value-change", { constraint, level });
}
});
}
});
},
);
function updateAnalogValue(value: number) {
if (props.type !== 'analog') return;
if (props.type !== "analog") return;
analogValue.value = Math.max(0, Math.min(1, value));
emit('value-change', {
emit("value-change", {
label: props.label,
constraint: props.constraint,
value: analogValue.value
value: analogValue.value,
});
}
@@ -149,16 +137,16 @@ defineExpose({
constraint: props.constraint,
direction: props.direction,
type: props.type,
pinId: props.pinId
pinId: props.pinId,
}),
getPinPosition: () => {
// Pin组件自身的getPinPosition应该返回相对于父组件的位置
// 在MechanicalButton等组件中已经明确传递了position所以这里实际上可能不会被使用
return {
x: viewBoxWidth.value / 2,
y: viewBoxHeight.value / 2
y: viewBoxHeight.value / 2,
};
}
},
});
</script>
@@ -167,20 +155,20 @@ defineExpose({
export function getDefaultProps() {
return {
size: 1,
label: 'PIN',
constraint: '',
direction: 'input',
type: 'digital',
pinId: 'pin-default',
componentId: '',
label: "PIN",
constraint: "",
direction: "input",
type: "digital",
pinId: "pin-default",
componentId: "",
pins: [
{
pinId: 'PIN',
constraint: '',
pinId: "PIN",
constraint: "",
x: 0,
y: 0
}
]
y: 0,
},
],
};
}
</script>
@@ -190,18 +178,25 @@ export function getDefaultProps() {
display: block;
user-select: none;
position: relative;
z-index: 5; /* 提高引脚组件的z-index */
pointer-events: auto; /* 确保可以接收点击事件 */
overflow: visible; /* 确保可以看到引脚 */
z-index: 5;
/* 提高引脚组件的z-index */
pointer-events: auto;
/* 确保可以接收点击事件 */
overflow: visible;
/* 确保可以看到引脚 */
}
.interactive {
cursor: pointer;
transition: filter 0.2s;
pointer-events: auto; /* 确保可以接收点击事件 */
pointer-events: auto;
/* 确保可以接收点击事件 */
}
.interactive:hover {
filter: brightness(1.2);
stroke: rgba(255, 255, 255, 0.3); /* 添加边框以便更容易看到点击区域 */
stroke: rgba(255, 255, 255, 0.3);
/* 添加边框以便更容易看到点击区域 */
stroke-width: 1;
}
</style>

View File

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

View File

@@ -1,106 +1,75 @@
<template>
<div class="seven-segment-display" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 120 220"
class="display"
>
<div class="seven-segment-display" :style="{
width: width + 'px',
height: height + 'px',
position: 'relative',
}">
<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="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
<!-- 7 + 小数点每个段由多边形表示重新设计点位置使其更接近实际数码管 -->
<!-- a段 (顶部横线) -->
<polygon
:points="'30,20 90,20 98,28 82,36 38,36 22,28'"
<polygon :points="'30,20 90,20 98,28 82,36 38,36 22,28'"
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }" class="segment" />
<!-- b段 (右上竖线) -->
<polygon
:points="'100,30 108,38 108,82 100,90 92,82 92,38'"
<polygon :points="'100,30 108,38 108,82 100,90 92,82 92,38'"
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }" class="segment" />
<!-- c段 (右下竖线) -->
<polygon
:points="'100,90 108,98 108,142 100,150 92,142 92,98'"
<polygon :points="'100,90 108,98 108,142 100,150 92,142 92,98'"
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }" class="segment" />
<!-- d段 (底部横线) -->
<polygon
:points="'30,160 90,160 98,152 82,144 38,144 22,152'"
<polygon :points="'30,160 90,160 98,152 82,144 38,144 22,152'"
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }" class="segment" />
<!-- e段 (左下竖线) -->
<polygon
:points="'20,90 28,98 28,142 20,150 12,142 12,98'"
<polygon :points="'20,90 28,98 28,142 20,150 12,142 12,98'"
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }" class="segment" />
<!-- f段 (左上竖线) -->
<polygon
:points="'20,30 28,38 28,82 20,90 12,82 12,38'"
<polygon :points="'20,30 28,38 28,82 20,90 12,82 12,38'"
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }" class="segment" />
<!-- g段 (中间横线) -->
<polygon
:points="'30,90 38,82 82,82 90,90 82,98 38,98'"
<polygon :points="'30,90 38,82 82,82 90,90 82,98 38,98'"
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
class="segment"
/> <!-- dp段 (小数点) -->
<circle
cx="108"
cy="154"
r="6"
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
class="segment"
/>
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }" class="segment" />
<!-- dp段 (小数点) -->
<circle cx="108" cy="154" r="6" :fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }" class="segment" />
</svg>
<!-- 引脚 -->
<div v-for="pin in pins" :key="pin.pinId"
:style="{
<div v-for="pin in pins" :key="pin.pinId" :style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
transform: 'translate(-50%, -50%)',
}" :data-pin-wrapper="`${pin.pinId}`" :data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`">
<Pin
:ref="el => { if(el) pinRefs[pin.pinId] = el }"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)"
/>
<Pin :ref="(el) => {
if (el) pinRefs[pin.pinId] = el;
}
" :label="pin.pinId" :constraint="pin.constraint" :pinId="pin.pinId" @pin-click="$emit('pin-click', $event)" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import Pin from './Pin.vue';
import { ref, computed, watch, onMounted, onUnmounted } from "vue";
import { useConstraintsStore } from "../../stores/constraints";
import Pin from "./Pin.vue";
const { getConstraintState, onConstraintStateChange } = useConstraintsStore();
// 存储Pin引用
const pinRefs = ref<Record<string, any>>({});
@@ -115,41 +84,41 @@ interface SevenSegmentDisplayProps {
x: number;
y: number;
}[];
cathodeType?: 'common' | 'anode'; // 共阴极或共阳极
cathodeType?: "common" | "anode"; // 共阴极或共阳极
}
const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), {
size: 1,
color: 'red',
cathodeType: 'common', // 默认为共阴极
color: "red",
cathodeType: "common", // 默认为共阴极
pins: () => [
{ pinId: 'a', constraint: '', x: 10 , y: 170 }, // a段
{ pinId: 'b', constraint: '', x: 25-1 , y: 170 }, // b段
{ pinId: 'c', constraint: '', x: 40-2 , y: 170 }, // c段
{ pinId: 'd', constraint: '', x: 55-3 , y: 170 }, // d段
{ pinId: 'e', constraint: '', x: 70-4 , y: 170 }, // e段
{ pinId: 'f', constraint: '', x: 85-5 , y: 170 }, // f段
{ pinId: 'g', constraint: '', x: 100-6, y: 170 }, // g段
{ pinId: 'dp', constraint: '', x: 115-7, y: 170 }, // 小数点
{ pinId: 'COM', constraint: '', x: 60 , y: 10 } // 公共端,稍微低一点
]
{ pinId: "a", constraint: "", x: 10, y: 170 }, // a段
{ pinId: "b", constraint: "", x: 25 - 1, y: 170 }, // b段
{ pinId: "c", constraint: "", x: 40 - 2, y: 170 }, // c段
{ pinId: "d", constraint: "", x: 55 - 3, y: 170 }, // d段
{ pinId: "e", constraint: "", x: 70 - 4, y: 170 }, // e段
{ pinId: "f", constraint: "", x: 85 - 5, y: 170 }, // f段
{ pinId: "g", constraint: "", x: 100 - 6, y: 170 }, // g段
{ pinId: "dp", constraint: "", x: 115 - 7, y: 170 }, // 小数点
{ pinId: "COM", constraint: "", x: 60, y: 10 }, // 公共端,稍微低一点
],
});
const width = computed(() => 120 * props.size);
const height = computed(() => 220 * props.size);
// 计算段颜色和非激活状态颜色
const segmentColor = computed(() => props.color || 'red');
const inactiveColor = computed(() => '#FFFFFF');
const segmentColor = computed(() => props.color || "red");
const inactiveColor = computed(() => "#FFFFFF");
// 监听props变化
watch(
() => props,
(newProps) => {
console.log('SevenSegmentDisplay props changed:', newProps);
console.log("SevenSegmentDisplay props changed:", newProps);
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];
}
// 更新引脚状态的函数
function updateSegmentStates() {
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为空则默认为未激活状态
if (!pin.constraint) {
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] =
false;
continue;
}
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 {
// 共阳极: 低电平激活段
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) {
const affectedPin = props.pins.find(pin => pin.constraint === constraint);
if (affectedPin && ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'dp'].includes(affectedPin.pinId)) {
const affectedPin = props.pins.find((pin) => pin.constraint === constraint);
if (
affectedPin &&
["a", "b", "c", "d", "e", "f", "g", "dp"].includes(affectedPin.pinId)
) {
updateSegmentStates();
}
}
@@ -213,7 +190,7 @@ onUnmounted(() => {
// 暴露属性和方法
defineExpose({
updateSegmentStates
updateSegmentStates,
});
</script>
@@ -224,7 +201,9 @@ defineExpose({
}
.segment {
transition: opacity 0.2s, fill 0.2s;
transition:
opacity 0.2s,
fill 0.2s;
}
/* 数码管发光效果 */
@@ -238,19 +217,19 @@ defineExpose({
export function getDefaultProps() {
return {
size: 1,
color: 'red',
cathodeType: 'common',
color: "red",
cathodeType: "common",
pins: [
{ pinId: 'a', constraint: '', x: 10 , y: 170 },
{ pinId: 'b', constraint: '', x: 25-1 , y: 170 },
{ pinId: 'c', constraint: '', x: 40-2 , y: 170 },
{ pinId: 'd', constraint: '', x: 55-3 , y: 170 },
{ pinId: 'e', constraint: '', x: 70-4 , y: 170 },
{ pinId: 'f', constraint: '', x: 85-5 , y: 170 },
{ pinId: 'g', constraint: '', x: 100-6, y: 170 },
{ pinId: 'dp', constraint: '', x: 115-7, y: 170 },
{ pinId: 'COM', constraint: '', x: 60 , y: 10 }
]
{ pinId: "a", constraint: "", x: 10, y: 170 },
{ pinId: "b", constraint: "", x: 25 - 1, y: 170 },
{ pinId: "c", constraint: "", x: 40 - 2, y: 170 },
{ pinId: "d", constraint: "", x: 55 - 3, y: 170 },
{ pinId: "e", constraint: "", x: 70 - 4, y: 170 },
{ pinId: "f", constraint: "", x: 85 - 5, y: 170 },
{ pinId: "g", constraint: "", x: 100 - 6, y: 170 },
{ pinId: "dp", constraint: "", x: 115 - 7, y: 170 },
{ pinId: "COM", constraint: "", x: 60, y: 10 },
],
};
}
</script>

View File

@@ -1,28 +1,18 @@
<template>
<g :data-wire-id="props.id">
<path
:d="pathData"
fill="none"
:stroke="computedStroke"
:stroke-width="strokeWidth"
stroke-linecap="round"
stroke-linejoin="round"
:class="{ 'wire-active': props.isActive }"
@click="handleClick"
/>
<path :d="pathData" fill="none" :stroke="computedStroke" :stroke-width="strokeWidth" stroke-linecap="round"
stroke-linejoin="round" :class="{ 'wire-active': props.isActive }" @click="handleClick" />
<!-- 可选添加连线标签或状态指示器 -->
<text
v-if="showLabel"
:x="labelPosition.x"
:y="labelPosition.y"
class="wire-label"
>{{ props.constraint || '' }}</text>
<text v-if="showLabel" :x="labelPosition.x" :y="labelPosition.y" class="wire-label">{{ props.constraint || ""
}}</text>
</g>
</template>
<script setup lang="ts">
import { computed, defineEmits, reactive, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import { useConstraintsStore } from "@/stores/constraints";
import { computed, defineEmits, watch, onMounted, onUnmounted } from "vue";
const { getConstraintColor, onConstraintStateChange } = useConstraintsStore();
interface Props {
id: string;
@@ -33,7 +23,7 @@ interface Props {
strokeColor?: string;
strokeWidth?: number;
isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path';
routingMode?: "auto" | "orthogonal" | "direct" | "path";
// 针脚引用属性
startComponentId?: string;
startPinId?: string;
@@ -48,12 +38,12 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
strokeColor: '#4a5568',
strokeColor: "#4a5568",
strokeWidth: 2,
isActive: false,
routingMode: 'orthogonal',
routingMode: "orthogonal",
showLabel: false,
constraint: ''
constraint: "",
});
// 响应约束状态变化的颜色
@@ -64,37 +54,46 @@ const constraintColor = computed(() => {
// 计算实际使用的颜色isActive优先其次是constraint电平颜色最后是默认色
const computedStroke = computed(() => {
if (props.isActive) return '#ff9800';
if (props.isActive) return "#ff9800";
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) {
emit('click', { id: props.id, event });
emit("click", { id: props.id, event });
}
// 计算标签位置 - 放在连线中间
const labelPosition = computed(() => {
return {
x: (props.startX + props.endX) / 2,
y: (props.startY + props.endY) / 2 - 5
y: (props.startY + props.endY) / 2 - 5,
};
});
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(
props.startX,
props.startY,
props.endX,
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(
@@ -102,10 +101,10 @@ function calculatePathFromCommands(
startY: number,
endX: number,
endY: number,
commands: string[]
commands: string[],
) {
// 找到分隔符索引,通常是 "*"
const splitterIndex = commands.indexOf('*');
const splitterIndex = commands.indexOf("*");
if (splitterIndex === -1) {
// 如果没有分隔符,回退到正交路径
return calculateOrthogonalPath(startX, startY, endX, endY);
@@ -149,9 +148,12 @@ function calculatePathFromCommands(
// 将两部分路径连接起来
// 如果终点路径的第一个点与起点路径的最后一个点相同,则去掉重复点
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][1] === reversedEndPoints[0][1]) {
pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]
) {
combinedPoints = [...pathPoints, ...reversedEndPoints.slice(1)];
} else {
combinedPoints = [...pathPoints, ...reversedEndPoints];
@@ -160,19 +162,27 @@ function calculatePathFromCommands(
const allPoints = combinedPoints;
// 检查是否需要添加中间连接点
if (allPoints.length >= 2) {
const startPathEndPoint = pathPoints.length > 0 ? pathPoints[pathPoints.length - 1] : null;
const endPathStartPoint = reversedEndPoints.length > 0 ? reversedEndPoints[0] : null;
const startPathEndPoint =
pathPoints.length > 0 ? pathPoints[pathPoints.length - 1] : null;
const endPathStartPoint =
reversedEndPoints.length > 0 ? reversedEndPoints[0] : null;
// 只有当起点路径和终点路径不相连时才添加连接线
if (startPathEndPoint && endPathStartPoint &&
(startPathEndPoint[0] !== endPathStartPoint[0] || startPathEndPoint[1] !== endPathStartPoint[1])) {
if (
startPathEndPoint &&
endPathStartPoint &&
(startPathEndPoint[0] !== endPathStartPoint[0] ||
startPathEndPoint[1] !== endPathStartPoint[1])
) {
// 使用正交连接或直接连接
let middlePoints: [number, number][] = [];
// 正交连接,添加额外点以确保路径是正交的
middlePoints = generateOrthogonalConnection(
startPathEndPoint[0], startPathEndPoint[1],
endPathStartPoint[0], endPathStartPoint[1]
startPathEndPoint[0],
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" 等
let newX = x;
let newY = y;
if (command.startsWith('right')) {
if (command.startsWith("right")) {
const distance = parseInt(command.substring(5), 10) || 10;
newX = x + distance;
newY = y;
} else if (command.startsWith('left')) {
} else if (command.startsWith("left")) {
const distance = parseInt(command.substring(4), 10) || 10;
newX = x - distance;
newY = y;
} else if (command.startsWith('down')) {
if (command.startsWith('downright')) {
} else if (command.startsWith("down")) {
if (command.startsWith("downright")) {
const distance = parseInt(command.substring(9), 10) || 10;
newX = x + distance;
newY = y + distance;
} else if (command.startsWith('downleft')) {
} else if (command.startsWith("downleft")) {
const distance = parseInt(command.substring(8), 10) || 10;
newX = x - distance;
newY = y + distance;
@@ -221,12 +235,12 @@ function executePathCommand(x: number, y: number, command: string): { newX: numb
newX = x;
newY = y + distance;
}
} else if (command.startsWith('up')) {
if (command.startsWith('upright')) {
} else if (command.startsWith("up")) {
if (command.startsWith("upright")) {
const distance = parseInt(command.substring(7), 10) || 10;
newX = x + distance;
newY = y - distance;
} else if (command.startsWith('upleft')) {
} else if (command.startsWith("upleft")) {
const distance = parseInt(command.substring(6), 10) || 10;
newX = x - 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 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 dy = endY - startY;
@@ -298,32 +322,26 @@ function reversePathCommand(command: string): string {
// 根据命令类型返回反向命令
// 水平方向反转
if (command.startsWith('right')) {
if (command.startsWith("right")) {
return `left${distance}`;
}
else if (command.startsWith('left')) {
} else if (command.startsWith("left")) {
return `right${distance}`;
}
// 垂直和斜向反转
else if (command.startsWith('down')) {
if (command.startsWith('downright')) {
else if (command.startsWith("down")) {
if (command.startsWith("downright")) {
return `upleft${distance}`;
}
else if (command.startsWith('downleft')) {
} else if (command.startsWith("downleft")) {
return `upright${distance}`;
}
else {
} else {
return `up${distance}`;
}
}
else if (command.startsWith('up')) {
if (command.startsWith('upright')) {
} else if (command.startsWith("up")) {
if (command.startsWith("upright")) {
return `downleft${distance}`;
}
else if (command.startsWith('upleft')) {
} else if (command.startsWith("upleft")) {
return `downright${distance}`;
}
else {
} else {
return `down${distance}`;
}
}
@@ -354,7 +372,9 @@ onUnmounted(() => {
});
// 监听约束属性变化
watch(() => props.constraint, (newConstraint, oldConstraint) => {
watch(
() => props.constraint,
(newConstraint, oldConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
@@ -367,26 +387,34 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
}
});
}
});
},
);
// 暴露方法,用于获取这条连线的信息
defineExpose({ id: props.id, getInfo: () => ({
defineExpose({
id: props.id,
getInfo: () => ({
id: props.id,
startComponentId: props.startComponentId,
startPinId: props.startPinId,
endComponentId: props.endComponentId,
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 是只读的,我们只能通过事件通知父组件更新
emit('update:position', {
emit("update:position", {
id: props.id,
startX: newStartX,
startY: newStartY,
endX: newEndX,
endY: newEndY
endY: newEndY,
});
},
// 获取连线的针脚情况
@@ -395,8 +423,8 @@ defineExpose({ id: props.id, getInfo: () => ({
getRoutingMode: () => props.routingMode,
// 设置连线状态(如高亮等)
setActive: (active: boolean) => {
emit('update:active', active);
}
emit("update:active", active);
},
});
</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 const useConstraintsStore = defineStore('constraints', () => {
// 约束状态存储
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 约束颜色映射
export const constraintColors = {
const constraintColors = {
high: '#ff3333', // 高电平为红色
low: '#3333ff', // 低电平为蓝色
undefined: '#999999' // 未定义为灰色
};
// 获取约束状态
export function getConstraintState(constraint: string): ConstraintLevel {
function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined';
return constraintStates[constraint] || 'undefined';
}
// 设置约束状态
export function setConstraintState(constraint: string, level: ConstraintLevel) {
function setConstraintState(constraint: string, level: ConstraintLevel) {
if (!constraint) return;
constraintStates[constraint] = level;
}
// 批量设置约束状态
export function batchSetConstraintStates(states: Record<string, ConstraintLevel>) {
function batchSetConstraintStates(states: Record<string, ConstraintLevel> | Record<string, boolean>) {
// 收集发生变化的约束
const changedConstraints: [string, ConstraintLevel][] = [];
// 更新状态并收集变化
Object.entries(states).forEach(([constraint, level]) => {
if (isBoolean(level)) {
level = level ? "high" : "low";
}
if (constraintStates[constraint] !== level) {
constraintStates[constraint] = level;
changedConstraints.push([constraint, level]);
@@ -44,27 +53,27 @@ export function batchSetConstraintStates(states: Record<string, ConstraintLevel>
}
// 获取约束对应的颜色
export function getConstraintColor(constraint: string): string {
function getConstraintColor(constraint: string): string {
const state = getConstraintState(constraint);
return constraintColors[state];
}
// 清除所有约束状态
export function clearAllConstraintStates() {
function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => {
delete constraintStates[key];
});
}
// 获取所有约束状态
export function getAllConstraintStates(): Record<string, ConstraintLevel> {
function getAllConstraintStates(): Record<string, ConstraintLevel> {
return { ...constraintStates };
}
// 注册约束状态变化回调
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);
return () => {
const index = stateChangeCallbacks.indexOf(callback);
@@ -75,7 +84,20 @@ export function onConstraintStateChange(callback: (constraint: string, level: Co
}
// 触发约束变化
export function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
setConstraintState(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 { 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', () => {
const jtagIPAddr = ref("127.0.0.1")
const jtagPort = ref("1234")
const jtagBitstream = ref<File | undefined>()
const remoteUpdateIPAddr = ref("127.0.0.1")
const remoteUpdatePort = ref("1234")
const remoteUpdateBitstream = ref<File | undefined>()
// Global Stores
const constrainsts = useConstraintsStore();
const dialog = useDialogStore();
const boardAddr = ref("127.0.0.1");
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 {
jtagIPAddr,
jtagPort,
boardAddr,
boardPort,
setAddr,
setPort,
jtagBitstream,
remoteUpdateIPAddr,
remoteUpdatePort,
remoteUpdateBitstream,
jtagBoundaryScanFreq,
jtagClientMutex,
jtagClient,
jtagUploadBitstream,
jtagDownloadBitstream,
jtagGetIDCode,
jtagSetSpeed,
enableJtagBoundaryScan,
}
})

View File

@@ -1,23 +1,35 @@
<template>
<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>
<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-body">
<div class="flex flex-row justify-between items-center">
<h2 class="card-title mb-4">IP 地址列表</h2>
<button class="btn btn-ghost" @click="">刷新</button>
</div>
<div class="overflow-x-auto">
<table class="table w-full">
<!-- 表头 -->
<thead>
<tr class="bg-base-300">
<th>IP 地址</th>
<th>版本号</th>
<th>默认启动位流</th>
<th>黄金位流</th>
<th>应用位流1</th>
<th>应用位流2</th>
<th>应用位流3</th>
<th class="w-50">IP 地址</th>
<th class="w-30">版本号</th>
<th class="w-50">默认启动位流</th>
<th class="w-80">黄金位流</th>
<th class="w-80">应用位流1</th>
<th class="w-80">应用位流2</th>
<th class="w-80">应用位流3</th>
<th>操作</th>
</tr>
</thead>
@@ -25,10 +37,13 @@
<!-- 表格内容 -->
<tbody>
<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>
<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>应用位流1</option>
<option>应用位流2</option>
@@ -38,56 +53,46 @@
<!-- 黄金位流上传区 -->
<td>
<div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-primary">
<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>
<input type="file" class="file-input file-input-primary" @change="handleFileChange($event, 0)" />
</div>
</td>
<!-- 应用位流1上传区 -->
<td>
<div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-secondary">
<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>
<input type="file" class="file-input file-input-secondary" @change="handleFileChange($event, 1)" />
</div>
</td>
<!-- 应用位流2上传区 -->
<td>
<div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-accent">
<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>
<input type="file" class="file-input file-input-accent" @change="handleFileChange($event, 2)" />
</div>
</td>
<!-- 应用位流3上传区 -->
<td>
<div class="flex flex-col items-center gap-2">
<label class="btn btn-outline btn-info">
<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>
<input type="file" class="file-input file-input-info" @change="handleFileChange($event, 3)" />
</div>
</td>
<!-- 操作按钮 -->
<td class="flex gap-2">
<button class="btn btn-sm btn-warning">固化</button>
<button class="btn btn-sm btn-success">切换并热启动</button>
<button class="btn grow btn-warning" @click="
uploadAndDownloadBitstreams(
devAddr,
goldBitstreamFile,
appBitstream1File,
appBitstream2File,
appBitstream3File,
)
">
固化
</button>
<button class="btn grow btn-success" @click="
hotresetBitstream(devAddr, getSelectedBitstreamNum())
">
切换并热启动
</button>
</td>
</tr>
</tbody>
@@ -96,7 +101,7 @@
<div class="mt-6 bg-base-300 p-4 rounded-lg">
<p class="text-sm opacity-80">
<span class="font-semibold text-warning">提示</span>
<span class="font-semibold text-error">提示</span>
请谨慎操作FPGA固化和热启动功能确保上传的位流文件无误以避免设备损坏
</p>
</div>
@@ -106,21 +111,139 @@
</template>
<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 appBitstream1File = ref<string>('');
const appBitstream2File = ref<string>('');
const appBitstream3File = ref<string>('');
const goldBitstreamFile = ref<File>();
const appBitstream1File = ref<File>();
const appBitstream2File = ref<File>();
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 file = target.files?.[0];
const file = target.files?.[0]; // 获取选中的第一个文件
if (file) {
fileRef.value = file.name;
if (!file) {
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>

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">
<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">
<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" />
<div
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定位限制覆盖层只在图片容器内 -->
<div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
</div>
@@ -14,64 +17,72 @@
Welcome to
</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>
<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>
<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">
<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">
<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">
<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="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>
进入工程界面
</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">
<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">
<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">
<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>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
登录
</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">
<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">
<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">
<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>
<circle cx="12" cy="7" r="4"></circle>
</svg>
用户中心
</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">
<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">
<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">
<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>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
测试功能
</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">
<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>
<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">
<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>
</svg>
管理控制台
</router-link>
</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">
<span class="font-semibold text-primary">提示</span> 您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计
<span class="font-semibold text-primary">提示</span>
您可以在工程界面中创建编辑和测试您的FPGA项目使用我们简洁直观的界面轻松进行硬件设计
</p>
</div>
</div>

View File

@@ -379,7 +379,6 @@ async function handleAddComponent(componentData: {
x: Math.round(position.x + offsetX),
y: Math.round(position.y + offsetY),
attrs: componentData.props,
capsPage: capsPage, // 设置组件的能力页面
rotate: 0,
group: "",
positionlock: false,
@@ -572,36 +571,58 @@ async function handleComponentSelected(componentData: DiagramPart | null) {
if (moduleRef) {
try {
// 使用组件配置工具创建配置项
// 创建属性配置数组
const propConfigs: PropertyConfig[] = [];
// 1. 自动获取组件属性信息 - 利用DiagramPart接口结构
const directPropConfigs = generatePropertyConfigs(componentData);
propConfigs.push(...directPropConfigs);
// 创建一个映射来跟踪已添加的属性名
const addedProps = new Set<string>();
// 2. 尝试使用组件导出的getDefaultProps方法获取配置
// 1. 首先从getDefaultProps方法获取默认配置
if (typeof moduleRef.getDefaultProps === "function") {
// 从getDefaultProps方法构建配置
const defaultProps = moduleRef.getDefaultProps();
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 };
console.log(
`Built config for ${componentData.type} from getDefaultProps:`,
`Built config for ${componentData.type}:`,
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) {
console.error(
`Error building config for ${componentData.type}:`,

View File

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