Merge branch 'csharp'

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

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ lerna-debug.log*
node_modules
.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>

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

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

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

View File

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

View File

@ -1,5 +1,6 @@
using System.Collections;
using System.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>
@ -54,17 +58,14 @@
<div v-if="activeTab === 'components'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredComponents.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(component, index) in filteredComponents" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(component)">
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>
@ -91,20 +94,26 @@
<div v-if="activeTab === 'templates'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredTemplates.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(template, index) in filteredTemplates" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addTemplate(template)">
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>
@ -115,21 +124,18 @@
</button>
</div>
</div>
<!-- 虚拟外设列表 (虚拟外设选项卡) -->
<!-- 虚拟外设列表 (虚拟外设选项卡) -->
<div v-if="activeTab === 'virtual'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredVirtualDevices.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(device, index) in filteredVirtualDevices" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(device)">
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");
}
//
@ -316,23 +329,26 @@ async function addComponent(componentTemplate: { type: string; name: string }) {
let defaultProps: Record<string, any> = {};
// getDefaultProps
if(moduleRef){
if (typeof moduleRef.getDefaultProps === 'function') {
if (moduleRef) {
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}`);
}
} else{
} else {
console.log(`Failed to load module for ${componentTemplate.type}`);
}
//
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 =>
component.name.toLowerCase().includes(query) ||
component.type.toLowerCase().includes(query)
return availableComponents.filter(
(component) =>
component.name.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 =>
template.name.toLowerCase().includes(query) ||
(template.description && template.description.toLowerCase().includes(query))
return availableTemplates.value.filter(
(template) =>
template.name.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 =>
device.name.toLowerCase().includes(query) ||
device.type.toLowerCase().includes(query)
return availableVirtualDevices.filter(
(device) =>
device.name.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"; // VueAPI
import { isNull, isUndefined } from "lodash";
import type { JSX } from "vue/jsx-runtime";
import {
ref,
computed,
watch,
useTemplateRef,
} from "vue"; // VueAPI
//
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">
{{ buttonText }}
<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.valuebitstream
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;
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);
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 = '';
// 使
if (currentWaveform === 'custom') {
let path = "";
// 使
if (currentWaveform === "custom") {
//
if (drawPoints.value.length > 0) {
path = `M${xOffset + drawPoints.value[0][0]},${yOffset + drawPoints.value[0][1]}`;
@ -308,32 +347,34 @@ 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)) {
path += ` L${x + xOffset},${yOffset + height/2 - y}`;
if (typeof y === "number" && isFinite(y)) {
path += ` L${x + xOffset},${yOffset + height / 2 - y}`;
} else {
//
path += ` L${x + xOffset},${yOffset + height/2}`;
path += ` L${x + xOffset},${yOffset + height / 2}`;
}
}
}
} else {
// 使
const waveFunction = waveformFunctions[currentWaveform as keyof typeof waveformFunctions];
const waveFunction =
waveformFunctions[currentWaveform as keyof typeof waveformFunctions];
//
path = `M${xOffset},${yOffset + height/2}`;
path = `M${xOffset},${yOffset + height / 2}`;
for (let x = 0; x <= width; x++) {
// - x
const scaledX = x * scaleFactor;
const y = waveFunction(scaledX, width, height, phaseRadians);
path += ` L${x + xOffset},${yOffset + height/2 - y}`;
path += ` L${x + xOffset},${yOffset + height / 2 - y}`;
}
}
@ -346,6 +387,56 @@ function selectWaveform(index: number) {
updateModelValue();
}
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,72 +557,90 @@ function createCustomWaveformFunction() {
const expression = customWaveformExpression.value;
// mathjs
import('mathjs').then((math) => {
try {
//
const compiledExpression = math.compile(expression);
import("mathjs")
.then((math) => {
try {
//
const compiledExpression = math.compile(expression);
//
waveformFunctions.custom = (x: number, width: number, height: number, phaseRad: number): number => {
try {
// - x
const phaseShift = phaseRad / (2 * Math.PI);
//
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;
// 使x 0-1
let normalizedX = (x / width + phaseShift) % 1;
// x[-1.5,1.5]
// [-1,1]
const scaledX = (normalizedX * 3) - 1.5;
// x[-1.5,1.5]
// [-1,1]
const scaledX = normalizedX * 3 - 1.5;
// 使
const scope = {
x: scaledX, // x
t: normalizedX * 2 * Math.PI, // (0-2π)
phase: phaseRad, //
PI: Math.PI, // PI
a: 7.8, //
};
// 使
const scope = {
x: scaledX, // x
t: normalizedX * 2 * Math.PI, // (0-2π)
phase: phaseRad, //
PI: Math.PI, // PI
a: 7.8, //
};
//
let result = compiledExpression.evaluate(scope);
let result = compiledExpression.evaluate(scope);
//
if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) {
result = 0;
//
if (
typeof result !== "number" ||
isNaN(result) ||
!isFinite(result)
) {
result = 0;
}
//
//
result = Math.max(-1, Math.min(1, result));
//
return (height / 2) * result;
} catch (e) {
console.error("Error evaluating expression:", e);
return 0;
}
};
//
//
result = Math.max(-1, Math.min(1, result));
//
return height/2 * result;
} catch (e) {
console.error('Error evaluating expression:', e);
return 0;
}
};
//
updateModelValue();
} catch (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);
};
}
}).catch(error => {
console.error('Error loading mathjs:', error);
});
};
//
updateModelValue();
} catch (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)
);
};
}
})
.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')
? [...drawPoints.value]
: null
data:
drawPoints.value.length > 0 &&
currentWaveformIndex.value === waveforms.indexOf("custom")
? [...drawPoints.value]
: 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,28 +910,40 @@ onMounted(() => {
});
// model
watch(() => props.modelValue, (newVal) => {
if (newVal && newVal.frequency !== undefined && newVal.frequency !== frequency.value) {
frequency.value = newVal.frequency;
frequencyInput.value = formatFrequency(frequency.value);
}
if (newVal && newVal.phase !== undefined && newVal.phase !== phase.value) {
phase.value = newVal.phase;
phaseInput.value = phase.value.toString();
}
if (newVal && newVal.timebase !== undefined && newVal.timebase !== timebase.value) {
timebase.value = newVal.timebase;
}
if (newVal && newVal.waveform) {
const index = waveforms.indexOf(newVal.waveform);
if (index !== -1 && index !== currentWaveformIndex.value) {
currentWaveformIndex.value = index;
watch(
() => props.modelValue,
(newVal) => {
if (
newVal &&
newVal.frequency !== undefined &&
newVal.frequency !== frequency.value
) {
frequency.value = newVal.frequency;
frequencyInput.value = formatFrequency(frequency.value);
}
}
}, { deep: true });
if (newVal && newVal.phase !== undefined && newVal.phase !== phase.value) {
phase.value = newVal.phase;
phaseInput.value = phase.value.toString();
}
if (
newVal &&
newVal.timebase !== undefined &&
newVal.timebase !== timebase.value
) {
timebase.value = newVal.timebase;
}
if (newVal && newVal.waveform) {
const index = waveforms.indexOf(newVal.waveform);
if (index !== -1 && index !== currentWaveformIndex.value) {
currentWaveformIndex.value = index;
}
}
},
{ 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="{
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}`"
: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"
/>
<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}`"
: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" />
</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,
});
//
// xysize=1size
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="{
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)"
/>
<!-- 渲染芯片引脚 -->
<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)" />
</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>>({});
@ -48,14 +41,14 @@ interface ChipProps {
pins?: {
pinId: string;
constraint: string;
x?: number; // x
y?: number; // y
x?: number; // x
y?: number; // y
}[];
}
const props = withDefaults(defineProps<ChipProps>(), {
size: 1,
pins: () => []
pins: () => [],
});
//
@ -99,14 +92,14 @@ const computedPins = computed(() => {
// 676BGA
const pins = [];
let pinIndex = 0;
// BGA
// BGA
for (let row = 0; row < pinRows; row++) {
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,27 +102,30 @@ onUnmounted(() => {
}
});
watch(() => props.constraint, (newConstraint, oldConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
emit('value-change', { constraint, level });
}
});
}
});
watch(
() => props.constraint,
(newConstraint, oldConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
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: () => {
// PingetPinPosition
// MechanicalButtonposition使
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 + 小数点每个段由多边形表示重新设计点位置使其更接近实际数码管 -->
<!-- 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="{
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}`"
: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)"
/>
<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}`"
: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)" />
</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(() => {
// 使isActiveconstraint
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);
@ -129,7 +128,7 @@ function calculatePathFromCommands(
currentY = newY;
pathPoints.push([currentX, currentY]);
}
//
//
let endCurrentX = endX;
let endCurrentY = endY;
@ -143,15 +142,18 @@ function calculatePathFromCommands(
endCurrentX = newX;
endCurrentY = newY;
endPathPoints.push([endCurrentX, endCurrentY]);
} //
} //
const reversedEndPoints = [...endPathPoints].reverse();
//
//
let combinedPoints;
if (pathPoints.length > 0 && reversedEndPoints.length > 0 &&
pathPoints[pathPoints.length - 1][0] === reversedEndPoints[0][0] &&
pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]) {
if (
pathPoints.length > 0 &&
reversedEndPoints.length > 0 &&
pathPoints[pathPoints.length - 1][0] === reversedEndPoints[0][0] &&
pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]
) {
combinedPoints = [...pathPoints, ...reversedEndPoints.slice(1)];
} else {
combinedPoints = [...pathPoints, ...reversedEndPoints];
@ -160,25 +162,33 @@ 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],
);
//
allPoints.splice(pathPoints.length, 0, ...middlePoints);
}
} // SVG
} // SVG
if (allPoints.length < 2) {
// 线
return `M ${startX} ${startY} L ${endX} ${endY}`;
@ -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,39 +372,49 @@ onUnmounted(() => {
});
//
watch(() => props.constraint, (newConstraint, oldConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
watch(
() => props.constraint,
(newConstraint, oldConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
}
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
//
}
});
}
});
if (newConstraint) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === newConstraint) {
//
}
});
}
},
);
// 线
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,81 +1,103 @@
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';
// 约束状态存储
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
export const useConstraintsStore = defineStore('constraints', () => {
// 约束颜色映射
export const constraintColors = {
high: '#ff3333', // 高电平为红色
low: '#3333ff', // 低电平为蓝色
undefined: '#999999' // 未定义为灰色
};
// 约束状态存储
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
// 获取约束状态
export function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined';
return constraintStates[constraint] || 'undefined';
}
// 设置约束状态
export function setConstraintState(constraint: string, level: ConstraintLevel) {
if (!constraint) return;
constraintStates[constraint] = level;
}
// 批量设置约束状态
export function batchSetConstraintStates(states: Record<string, ConstraintLevel>) {
// 收集发生变化的约束
const changedConstraints: [string, ConstraintLevel][] = [];
// 更新状态并收集变化
Object.entries(states).forEach(([constraint, level]) => {
if (constraintStates[constraint] !== level) {
constraintStates[constraint] = level;
changedConstraints.push([constraint, level]);
}
});
// 通知所有变化
changedConstraints.forEach(([constraint, level]) => {
stateChangeCallbacks.forEach(callback => callback(constraint, level));
});
}
// 获取约束对应的颜色
export function getConstraintColor(constraint: string): string {
const state = getConstraintState(constraint);
return constraintColors[state];
}
// 清除所有约束状态
export function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => {
delete constraintStates[key];
});
}
// 获取所有约束状态
export function getAllConstraintStates(): Record<string, ConstraintLevel> {
return { ...constraintStates };
}
// 注册约束状态变化回调
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = [];
export function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) {
stateChangeCallbacks.push(callback);
return () => {
const index = stateChangeCallbacks.indexOf(callback);
if (index > -1) {
stateChangeCallbacks.splice(index, 1);
}
// 约束颜色映射
const constraintColors = {
high: '#ff3333', // 高电平为红色
low: '#3333ff', // 低电平为蓝色
undefined: '#999999' // 未定义为灰色
};
}
// 触发约束变化
export function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
setConstraintState(constraint, level);
stateChangeCallbacks.forEach(callback => callback(constraint, level));
}
// 获取约束状态
function getConstraintState(constraint: string): ConstraintLevel {
if (!constraint) return 'undefined';
return constraintStates[constraint] || 'undefined';
}
// 设置约束状态
function setConstraintState(constraint: string, level: ConstraintLevel) {
if (!constraint) return;
constraintStates[constraint] = level;
}
// 批量设置约束状态
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]);
}
});
// 通知所有变化
changedConstraints.forEach(([constraint, level]) => {
stateChangeCallbacks.forEach(callback => callback(constraint, level));
});
}
// 获取约束对应的颜色
function getConstraintColor(constraint: string): string {
const state = getConstraintState(constraint);
return constraintColors[state];
}
// 清除所有约束状态
function clearAllConstraintStates() {
Object.keys(constraintStates).forEach(key => {
delete constraintStates[key];
});
}
// 获取所有约束状态
function getAllConstraintStates(): Record<string, ConstraintLevel> {
return { ...constraintStates };
}
// 注册约束状态变化回调
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = [];
function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) {
stateChangeCallbacks.push(callback);
return () => {
const index = stateChangeCallbacks.indexOf(callback);
if (index > -1) {
stateChangeCallbacks.splice(index, 1);
}
};
}
// 触发约束变化
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">
<h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
<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">
<h2 class="card-title mb-4">IP 地址列表</h2>
<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,69 +37,62 @@
<!-- 表格内容 -->
<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>
<option>应用位流3</option>
</select>
</td>
<!-- 黄金位流上传区 -->
<!-- 黄金位流上传区 -->
<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上传区 -->
<!-- 应用位流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上传区 -->
<!-- 应用位流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上传区 -->
<!-- 应用位流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,77 +1,88 @@
<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>
<!-- 内容容器 -->
<!-- 内容容器 -->
<div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
<h1 class="text-4xl md:text-5xl font-bold mb-3 relative group">
<span class="relative z-10 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
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">
<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">
<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);
selectedComponentConfig.value = { props: propConfigs };
console.log(
`Built config for ${componentData.type} from getDefaultProps:`,
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 };
//
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}:`,
selectedComponentConfig.value,
);
} 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>