5 Commits

Author SHA1 Message Date
alivender
51b39cee07 add: 添加了HDMI视频流Client 2025-08-04 11:54:58 +08:00
alivender
0bd1ad8a0e add: 添加了960*540分辨率 2025-08-02 21:07:08 +08:00
alivender
f2c7c78b64 feat: JtaggetDR可以一次全部获取到 2025-08-02 16:01:07 +08:00
alivender
2f23ffe482 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-08-02 13:15:07 +08:00
alivender
9904fecbee feat: 统一资源管理 2025-08-02 13:14:01 +08:00
22 changed files with 1331 additions and 851 deletions

View File

@@ -303,8 +303,11 @@ async function generateApiClient(): Promise<void> {
async function generateSignalRClient(): Promise<void> { async function generateSignalRClient(): Promise<void> {
console.log("Generating SignalR TypeScript client..."); console.log("Generating SignalR TypeScript client...");
try { try {
// TypedSignalR.Client.TypeScript.Analyzer 会在编译时自动生成客户端
// 我们只需要确保服务器项目构建一次即可生成 TypeScript 客户端
const { stdout, stderr } = await execAsync( const { stdout, stderr } = await execAsync(
"dotnet tsrts --project ./server/server.csproj --output ./src", "dotnet build --configuration Release",
{ cwd: "./server" }
); );
if (stdout) console.log(stdout); if (stdout) console.log(stdout);
if (stderr) console.error(stderr); if (stderr) console.error(stderr);

View File

@@ -101,22 +101,6 @@ public class ExamController : ControllerBase
public bool IsVisibleToUsers { get; set; } = true; public bool IsVisibleToUsers { get; set; } = true;
} }
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
}
/// <summary> /// <summary>
/// 创建实验请求类 /// 创建实验请求类
/// </summary> /// </summary>
@@ -304,151 +288,4 @@ public class ExamController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}"); return StatusCode(StatusCodes.Status500InternalServerError, $"创建实验失败: {ex.Message}");
} }
} }
/// <summary>
/// 添加实验资源
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize("Admin")]
[HttpPost("{examId}/resources/{resourceType}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddExamResource(string examId, string resourceType, IFormFile file)
{
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType) || file == null)
return BadRequest("实验ID、资源类型和文件不能为空");
try
{
using var db = new Database.AppDataConnection();
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddExamResource(examId, resourceType, file.FileName, fileData);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
if (result.Error.Message.Contains("已存在"))
return Conflict(result.Error.Message);
logger.Error($"添加实验资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName
};
logger.Info($"成功添加实验资源: {examId}/{resourceType}/{file.FileName}");
return CreatedAtAction(nameof(GetExamResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加实验资源 {examId}/{resourceType}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加实验资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet("{examId}/resources/{resourceType}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamResourceList(string examId, string resourceType)
{
if (string.IsNullOrWhiteSpace(examId) || string.IsNullOrWhiteSpace(resourceType))
return BadRequest("实验ID和资源类型不能为空");
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamResourceList(examId, resourceType);
if (!result.IsSuccessful)
{
logger.Error($"获取实验资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.Name
}).ToArray();
logger.Info($"成功获取实验资源列表: {examId}/{resourceType},共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取实验资源列表 {examId}/{resourceType} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取实验资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("resources/{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetExamResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetExamResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType, resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
} }

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Database;
namespace server.Controllers; namespace server.Controllers;
@@ -14,8 +15,6 @@ public class JtagController : ControllerBase
{ {
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private const string BITSTREAM_PATH = "bitstream/Jtag";
/// <summary> /// <summary>
/// 控制器首页信息 /// 控制器首页信息
/// </summary> /// </summary>
@@ -112,64 +111,12 @@ public class JtagController : ControllerBase
} }
} }
/// <summary>
/// 上传比特流文件到服务器
/// </summary>
/// <param name="address">目标设备地址</param>
/// <param name="file">比特流文件</param>
/// <returns>上传结果</returns>
[HttpPost("UploadBitstream")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async ValueTask<IResult> UploadBitstream(string address, IFormFile file)
{
logger.Info($"User {User.Identity?.Name} uploading bitstream for device {address}");
if (file == null || file.Length == 0)
{
logger.Warn($"User {User.Identity?.Name} attempted to upload empty file for device {address}");
return TypedResults.BadRequest("未选择文件");
}
try
{
// 生成安全的文件名(避免路径遍历攻击)
var fileName = Path.GetRandomFileName();
var uploadsFolder = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
// 如果存在文件,则删除原文件再上传
if (Directory.Exists(uploadsFolder))
{
Directory.Delete(uploadsFolder, true);
logger.Info($"User {User.Identity?.Name} removed existing bitstream folder for device {address}");
}
Directory.CreateDirectory(uploadsFolder);
var filePath = Path.Combine(uploadsFolder, fileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
logger.Info($"User {User.Identity?.Name} successfully uploaded bitstream for device {address}, file size: {file.Length} bytes");
return TypedResults.Ok(true);
}
catch (Exception ex)
{
logger.Error(ex, $"User {User.Identity?.Name} failed to upload bitstream for device {address}");
return TypedResults.InternalServerError(ex);
}
}
/// <summary> /// <summary>
/// 通过 JTAG 下载比特流文件到 FPGA 设备 /// 通过 JTAG 下载比特流文件到 FPGA 设备
/// </summary> /// </summary>
/// <param name="address">JTAG 设备地址</param> /// <param name="address">JTAG 设备地址</param>
/// <param name="port">JTAG 设备端口</param> /// <param name="port">JTAG 设备端口</param>
/// <param name="bitstreamId">比特流ID</param>
/// <returns>下载结果</returns> /// <returns>下载结果</returns>
[HttpPost("DownloadBitstream")] [HttpPost("DownloadBitstream")]
[EnableCors("Users")] [EnableCors("Users")]
@@ -177,87 +124,111 @@ public class JtagController : ControllerBase
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)] [ProducesResponseType(typeof(Exception), StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async ValueTask<IResult> DownloadBitstream(string address, int port) public async ValueTask<IResult> DownloadBitstream(string address, int port, int bitstreamId)
{ {
logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port}"); logger.Info($"User {User.Identity?.Name} initiating bitstream download to device {address}:{port} using bitstream ID: {bitstreamId}");
// 检查文件
var fileDir = Path.Combine(Environment.CurrentDirectory, $"{BITSTREAM_PATH}/{address}");
if (!Directory.Exists(fileDir))
{
logger.Warn($"User {User.Identity?.Name} attempted to download non-existent bitstream for device {address}");
return TypedResults.BadRequest("Empty bitstream, Please upload it first");
}
try try
{ {
// 读取文件 // 获取当前用户名
var filePath = Directory.GetFiles(fileDir)[0]; var username = User.Identity?.Name;
logger.Info($"User {User.Identity?.Name} reading bitstream file: {filePath}"); if (string.IsNullOrEmpty(username))
using (var fileStream = System.IO.File.Open(filePath, System.IO.FileMode.Open))
{ {
if (fileStream is null || fileStream.Length <= 0) logger.Warn("Anonymous user attempted to download bitstream");
return TypedResults.Unauthorized();
}
// 从数据库获取用户信息
using var db = new Database.AppDataConnection();
var userResult = db.GetUserByName(username);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
{
logger.Error($"User {username} not found in database");
return TypedResults.BadRequest("用户不存在");
}
var user = userResult.Value.Value;
// 从数据库获取比特流
var bitstreamResult = db.GetResourceById(bitstreamId);
if (!bitstreamResult.IsSuccessful)
{
logger.Error($"User {username} failed to get bitstream from database: {bitstreamResult.Error}");
return TypedResults.InternalServerError($"数据库查询失败: {bitstreamResult.Error?.Message}");
}
if (!bitstreamResult.Value.HasValue)
{
logger.Warn($"User {username} attempted to download non-existent bitstream ID: {bitstreamId}");
return TypedResults.BadRequest("比特流不存在");
}
var bitstream = bitstreamResult.Value.Value;
// 处理比特流数据
var fileBytes = bitstream.Data;
if (fileBytes == null || fileBytes.Length == 0)
{
logger.Warn($"User {username} found empty bitstream data for ID: {bitstreamId}");
return TypedResults.BadRequest("比特流数据为空,请重新上传");
}
logger.Info($"User {username} processing bitstream file of size: {fileBytes.Length} bytes");
// 定义缓冲区大小: 32KB
byte[] buffer = new byte[32 * 1024];
byte[] revBuffer = new byte[32 * 1024];
long totalBytesProcessed = 0;
// 使用内存流处理文件
using (var inputStream = new MemoryStream(fileBytes))
using (var outputStream = new MemoryStream())
{
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{ {
logger.Warn($"User {User.Identity?.Name} found invalid bitstream file for device {address}"); // 反转 32bits
return TypedResults.BadRequest("Wrong bitstream, Please upload it again"); var retBuffer = Common.Number.ReverseBytes(buffer, 4);
if (!retBuffer.IsSuccessful)
{
logger.Error($"User {username} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await outputStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesProcessed += bytesRead;
} }
logger.Info($"User {User.Identity?.Name} processing bitstream file of size: {fileStream.Length} bytes"); // 获取处理后的数据
var processedBytes = outputStream.ToArray();
logger.Info($"User {username} processed {totalBytesProcessed} bytes for device {address}");
// 定义缓冲区大小: 32KB // 下载比特流
byte[] buffer = new byte[32 * 1024]; var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
byte[] revBuffer = new byte[32 * 1024]; var ret = await jtagCtrl.DownloadBitstream(processedBytes);
long totalBytesRead = 0;
// 使用异步流读取文件 if (ret.IsSuccessful)
using (var memoryStream = new MemoryStream())
{ {
int bytesRead; logger.Info($"User {username} successfully downloaded bitstream '{bitstream.ResourceName}' to device {address}");
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0) return TypedResults.Ok(ret.Value);
{ }
// 反转 32bits else
var retBuffer = Common.Number.ReverseBytes(buffer, 4); {
if (!retBuffer.IsSuccessful) logger.Error($"User {username} failed to download bitstream to device {address}: {ret.Error}");
{ return TypedResults.InternalServerError(ret.Error);
logger.Error($"User {User.Identity?.Name} failed to reverse bytes: {retBuffer.Error}");
return TypedResults.InternalServerError(retBuffer.Error);
}
revBuffer = retBuffer.Value;
for (int i = 0; i < revBuffer.Length; i++)
{
revBuffer[i] = Common.Number.ReverseBits(revBuffer[i]);
}
await memoryStream.WriteAsync(revBuffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
// 将所有数据转换为字节数组(注意:如果文件非常大,可能不适合完全加载到内存)
var fileBytes = memoryStream.ToArray();
logger.Info($"User {User.Identity?.Name} processed {totalBytesRead} bytes for device {address}");
// 下载比特流
var jtagCtrl = new Peripherals.JtagClient.Jtag(address, port);
var ret = await jtagCtrl.DownloadBitstream(fileBytes);
if (ret.IsSuccessful)
{
logger.Info($"User {User.Identity?.Name} successfully downloaded bitstream to device {address}");
return TypedResults.Ok(ret.Value);
}
else
{
logger.Error($"User {User.Identity?.Name} failed to download bitstream to device {address}: {ret.Error}");
return TypedResults.InternalServerError(ret.Error);
}
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error(ex, $"User {User.Identity?.Name} encountered exception while downloading bitstream to device {address}"); logger.Error(ex, $"User encountered exception while downloading bitstream to device {address}");
return TypedResults.InternalServerError(ex); return TypedResults.InternalServerError(ex);
} }
} }

View File

@@ -0,0 +1,377 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using DotNext;
using Database;
namespace server.Controllers;
/// <summary>
/// 资源控制器 - 提供统一的资源管理API
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ResourceController : ControllerBase
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
/// <summary>
/// 资源信息类
/// </summary>
public class ResourceInfo
{
/// <summary>
/// 资源ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// 资源名称
/// </summary>
public required string Name { get; set; }
/// <summary>
/// 资源类型
/// </summary>
public required string Type { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string Purpose { get; set; }
/// <summary>
/// 上传时间
/// </summary>
public DateTime UploadTime { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
/// <summary>
/// MIME类型
/// </summary>
public string? MimeType { get; set; }
}
/// <summary>
/// 添加资源请求类
/// </summary>
public class AddResourceRequest
{
/// <summary>
/// 资源类型
/// </summary>
public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template/user
/// </summary>
public required string ResourcePurpose { get; set; }
/// <summary>
/// 所属实验ID可选
/// </summary>
public string? ExamID { get; set; }
}
/// <summary>
/// 添加资源(文件上传)
/// </summary>
/// <param name="request">添加资源请求</param>
/// <param name="file">资源文件</param>
/// <returns>添加结果</returns>
[Authorize]
[HttpPost]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> AddResource([FromForm] AddResourceRequest request, IFormFile file)
{
if (string.IsNullOrWhiteSpace(request.ResourceType) || string.IsNullOrWhiteSpace(request.ResourcePurpose) || file == null)
return BadRequest("资源类型、资源用途和文件不能为空");
// 验证资源用途
if (request.ResourcePurpose != Resource.ResourcePurposes.Template && request.ResourcePurpose != Resource.ResourcePurposes.User)
return BadRequest($"无效的资源用途: {request.ResourcePurpose}");
// 模板资源需要管理员权限
if (request.ResourcePurpose == Resource.ResourcePurposes.Template && !User.IsInRole("Admin"))
return Forbid("只有管理员可以添加模板资源");
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 读取文件数据
using var memoryStream = new MemoryStream();
await file.CopyToAsync(memoryStream);
var fileData = memoryStream.ToArray();
var result = db.AddResource(user.ID, request.ResourceType, request.ResourcePurpose, file.FileName, fileData, request.ExamID);
if (!result.IsSuccessful)
{
if (result.Error.Message.Contains("不存在"))
return NotFound(result.Error.Message);
logger.Error($"添加资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {result.Error.Message}");
}
var resource = result.Value;
var resourceInfo = new ResourceInfo
{
ID = resource.ID,
Name = resource.ResourceName,
Type = resource.ResourceType,
Purpose = resource.ResourcePurpose,
UploadTime = resource.UploadTime,
ExamID = resource.ExamID,
MimeType = resource.MimeType
};
logger.Info($"成功添加资源: {request.ResourceType}/{request.ResourcePurpose}/{file.FileName}");
return CreatedAtAction(nameof(GetResourceById), new { resourceId = resource.ID }, resourceInfo);
}
catch (Exception ex)
{
logger.Error($"添加资源 {request.ResourceType}/{request.ResourcePurpose}/{file.FileName} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"添加资源失败: {ex.Message}");
}
}
/// <summary>
/// 获取资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <returns>资源列表</returns>
[Authorize]
[HttpGet]
[EnableCors("Users")]
[ProducesResponseType(typeof(ResourceInfo[]), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceList([FromQuery] string? examId = null, [FromQuery] string? resourceType = null, [FromQuery] string? resourcePurpose = null)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户ID
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 普通用户只能查看自己的资源和模板资源
Guid? userId = null;
if (!User.IsInRole("Admin"))
{
// 如果指定了用户资源用途,则只查看自己的资源
if (resourcePurpose == Resource.ResourcePurposes.User)
{
userId = user.ID;
}
// 如果指定了模板资源用途则不限制用户ID
else if (resourcePurpose == Resource.ResourcePurposes.Template)
{
userId = null;
}
// 如果没有指定用途,则查看自己的用户资源和所有模板资源
else
{
// 这种情况下需要分别查询并合并结果
var userResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.User, user.ID);
var templateResourcesResult = db.GetFullResourceList(examId, resourceType, Resource.ResourcePurposes.Template, null);
if (!userResourcesResult.IsSuccessful || !templateResourcesResult.IsSuccessful)
{
logger.Error($"获取资源列表时出错");
return StatusCode(StatusCodes.Status500InternalServerError, "获取资源列表失败");
}
var allResources = userResourcesResult.Value.Concat(templateResourcesResult.Value)
.OrderByDescending(r => r.UploadTime);
var mergedResourceInfos = allResources.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {mergedResourceInfos.Length} 个资源");
return Ok(mergedResourceInfos);
}
}
var result = db.GetFullResourceList(examId, resourceType, resourcePurpose, userId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源列表时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {result.Error.Message}");
}
var resources = result.Value.Select(r => new ResourceInfo
{
ID = r.ID,
Name = r.ResourceName,
Type = r.ResourceType,
Purpose = r.ResourcePurpose,
UploadTime = r.UploadTime,
ExamID = r.ExamID,
MimeType = r.MimeType
}).ToArray();
logger.Info($"成功获取资源列表,共 {resources.Length} 个资源");
return Ok(resources);
}
catch (Exception ex)
{
logger.Error($"获取资源列表时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源列表失败: {ex.Message}");
}
}
/// <summary>
/// 根据资源ID下载资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>资源文件</returns>
[HttpGet("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(typeof(FileResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult GetResourceById(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
var result = db.GetResourceById(resourceId);
if (!result.IsSuccessful)
{
logger.Error($"获取资源时出错: {result.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {result.Error.Message}");
}
if (!result.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = result.Value.Value;
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
return File(resource.Data, resource.MimeType ?? "application/octet-stream", resource.ResourceName);
}
catch (Exception ex)
{
logger.Error($"获取资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {ex.Message}");
}
}
/// <summary>
/// 删除资源
/// </summary>
/// <param name="resourceId">资源ID</param>
/// <returns>删除结果</returns>
[Authorize]
[HttpDelete("{resourceId}")]
[EnableCors("Users")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult DeleteResource(int resourceId)
{
try
{
using var db = new Database.AppDataConnection();
// 获取当前用户信息
var userName = User.Identity?.Name;
if (string.IsNullOrEmpty(userName))
return Unauthorized("无法获取用户信息");
var userResult = db.GetUserByName(userName);
if (!userResult.IsSuccessful || !userResult.Value.HasValue)
return Unauthorized("用户不存在");
var user = userResult.Value.Value;
// 先获取资源信息以验证权限
var resourceResult = db.GetResourceById(resourceId);
if (!resourceResult.IsSuccessful)
{
logger.Error($"获取资源时出错: {resourceResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"获取资源失败: {resourceResult.Error.Message}");
}
if (!resourceResult.Value.HasValue)
{
logger.Warn($"资源不存在: {resourceId}");
return NotFound($"资源 {resourceId} 不存在");
}
var resource = resourceResult.Value.Value;
// 权限检查:管理员可以删除所有资源,普通用户只能删除自己的用户资源
if (!User.IsInRole("Admin"))
{
if (resource.ResourcePurpose == Resource.ResourcePurposes.Template)
return Forbid("普通用户不能删除模板资源");
if (resource.UserID != user.ID)
return Forbid("只能删除自己的资源");
}
var deleteResult = db.DeleteResource(resourceId);
if (!deleteResult.IsSuccessful)
{
logger.Error($"删除资源时出错: {deleteResult.Error.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {deleteResult.Error.Message}");
}
logger.Info($"成功删除资源: {resourceId} ({resource.ResourceName})");
return NoContent();
}
catch (Exception ex)
{
logger.Error($"删除资源 {resourceId} 时出错: {ex.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"删除资源失败: {ex.Message}");
}
}
}

View File

@@ -229,9 +229,9 @@ public class Exam
} }
/// <summary> /// <summary>
/// 实验资源表(图片等) /// 资源类,统一管理实验资源、用户比特流等各类资源
/// </summary> /// </summary>
public class ExamResource public class Resource
{ {
/// <summary> /// <summary>
/// 资源的唯一标识符 /// 资源的唯一标识符
@@ -240,17 +240,29 @@ public class ExamResource
public int ID { get; set; } public int ID { get; set; }
/// <summary> /// <summary>
/// 所属实验ID /// 上传资源的用户ID
/// </summary> /// </summary>
[NotNull] [NotNull]
public required string ExamID { get; set; } public required Guid UserID { get; set; }
/// <summary> /// <summary>
/// 资源类型images, markdown, bitstream, diagram, project /// 所属实验ID可选如果不属于特定实验则为空
/// </summary>
[Nullable]
public string? ExamID { get; set; }
/// <summary>
/// 资源类型images, markdown, bitstream, diagram, project等
/// </summary> /// </summary>
[NotNull] [NotNull]
public required string ResourceType { get; set; } public required string ResourceType { get; set; }
/// <summary>
/// 资源用途template模板或 user用户上传
/// </summary>
[NotNull]
public required string ResourcePurpose { get; set; }
/// <summary> /// <summary>
/// 资源名称(包含文件扩展名) /// 资源名称(包含文件扩展名)
/// </summary> /// </summary>
@@ -264,10 +276,10 @@ public class ExamResource
public required byte[] Data { get; set; } public required byte[] Data { get; set; }
/// <summary> /// <summary>
/// 资源创建时间 /// 资源创建/上传时间
/// </summary> /// </summary>
[NotNull] [NotNull]
public DateTime CreatedTime { get; set; } = DateTime.Now; public DateTime UploadTime { get; set; } = DateTime.Now;
/// <summary> /// <summary>
/// 资源的MIME类型 /// 资源的MIME类型
@@ -305,6 +317,22 @@ public class ExamResource
/// </summary> /// </summary>
public const string Project = "project"; public const string Project = "project";
} }
/// <summary>
/// 资源用途枚举
/// </summary>
public static class ResourcePurposes
{
/// <summary>
/// 模板资源,通常由管理员上传,供用户参考
/// </summary>
public const string Template = "template";
/// <summary>
/// 用户上传的资源
/// </summary>
public const string User = "user";
}
} }
/// <summary> /// <summary>
@@ -355,7 +383,7 @@ public class AppDataConnection : DataConnection
this.CreateTable<User>(); this.CreateTable<User>();
this.CreateTable<Board>(); this.CreateTable<Board>();
this.CreateTable<Exam>(); this.CreateTable<Exam>();
this.CreateTable<ExamResource>(); this.CreateTable<Resource>();
logger.Info("数据库表创建完成"); logger.Info("数据库表创建完成");
} }
@@ -368,7 +396,7 @@ public class AppDataConnection : DataConnection
this.DropTable<User>(); this.DropTable<User>();
this.DropTable<Board>(); this.DropTable<Board>();
this.DropTable<Exam>(); this.DropTable<Exam>();
this.DropTable<ExamResource>(); this.DropTable<Resource>();
logger.Warn("所有数据库表已删除"); logger.Warn("所有数据库表已删除");
} }
@@ -828,9 +856,9 @@ public class AppDataConnection : DataConnection
public ITable<Exam> ExamTable => this.GetTable<Exam>(); public ITable<Exam> ExamTable => this.GetTable<Exam>();
/// <summary> /// <summary>
/// 实验资源表 /// 资源表(统一管理实验资源、用户比特流等)
/// </summary> /// </summary>
public ITable<ExamResource> ExamResourceTable => this.GetTable<ExamResource>(); public ITable<Resource> ResourceTable => this.GetTable<Resource>();
/// <summary> /// <summary>
/// 创建新实验 /// 创建新实验
@@ -933,34 +961,44 @@ public class AppDataConnection : DataConnection
} }
/// <summary> /// <summary>
/// 添加实验资源 /// 添加资源
/// </summary> /// </summary>
/// <param name="examId">所属实验ID</param> /// <param name="userId">上传用户ID</param>
/// <param name="resourceType">资源类型</param> /// <param name="resourceType">资源类型</param>
/// <param name="resourcePurpose">资源用途template 或 user</param>
/// <param name="resourceName">资源名称</param> /// <param name="resourceName">资源名称</param>
/// <param name="data">资源二进制数据</param> /// <param name="data">资源二进制数据</param>
/// <param name="examId">所属实验ID可选</param>
/// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param> /// <param name="mimeType">MIME类型可选将根据文件扩展名自动确定</param>
/// <returns>创建的资源</returns> /// <returns>创建的资源</returns>
public Result<ExamResource> AddExamResource(string examId, string resourceType, string resourceName, byte[] data, string? mimeType = null) public Result<Resource> AddResource(Guid userId, string resourceType, string resourcePurpose, string resourceName, byte[] data, string? examId = null, string? mimeType = null)
{ {
try try
{ {
// 验证实验是否存在 // 验证用户是否存在
var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault(); var user = this.UserTable.Where(u => u.ID == userId).FirstOrDefault();
if (exam == null) if (user == null)
{ {
logger.Error($"实验不存在: {examId}"); logger.Error($"用户不存在: {userId}");
return new(new Exception($"实验不存在: {examId}")); return new(new Exception($"用户不存在: {userId}"));
} }
// 检查资源是否存在 // 如果指定了实验ID验证实验是否存在
var existingResource = this.ExamResourceTable if (!string.IsNullOrEmpty(examId))
.Where(r => r.ExamID == examId && r.ResourceType == resourceType && r.ResourceName == resourceName)
.FirstOrDefault();
if (existingResource != null)
{ {
logger.Error($"资源已存在: {examId}/{resourceType}/{resourceName}"); var exam = this.ExamTable.Where(e => e.ID == examId).FirstOrDefault();
return new(new Exception($"资源已存在: {examId}/{resourceType}/{resourceName}")); if (exam == null)
{
logger.Error($"实验不存在: {examId}");
return new(new Exception($"实验不存在: {examId}"));
}
}
// 验证资源用途
if (resourcePurpose != Resource.ResourcePurposes.Template && resourcePurpose != Resource.ResourcePurposes.User)
{
logger.Error($"无效的资源用途: {resourcePurpose}");
return new(new Exception($"无效的资源用途: {resourcePurpose}"));
} }
// 如果未指定MIME类型根据文件扩展名自动确定 // 如果未指定MIME类型根据文件扩展名自动确定
@@ -970,49 +1008,126 @@ public class AppDataConnection : DataConnection
mimeType = GetMimeTypeFromExtension(extension, resourceName); mimeType = GetMimeTypeFromExtension(extension, resourceName);
} }
var resource = new ExamResource var resource = new Resource
{ {
UserID = userId,
ExamID = examId, ExamID = examId,
ResourceType = resourceType, ResourceType = resourceType,
ResourcePurpose = resourcePurpose,
ResourceName = resourceName, ResourceName = resourceName,
Data = data, Data = data,
MimeType = mimeType, MimeType = mimeType,
CreatedTime = DateTime.Now UploadTime = DateTime.Now
}; };
this.Insert(resource); var insertedId = this.InsertWithIdentity(resource);
logger.Info($"新资源已添加: {examId}/{resourceType}/{resourceName} ({data.Length} bytes)"); resource.ID = Convert.ToInt32(insertedId);
logger.Info($"新资源已添加: {userId}/{resourceType}/{resourcePurpose}/{resourceName} ({data.Length} bytes)" +
(examId != null ? $" [实验: {examId}]" : "") + $" [ID: {resource.ID}]");
return new(resource); return new(resource);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error($"添加实验资源时出错: {ex.Message}"); logger.Error($"添加资源时出错: {ex.Message}");
return new(ex); return new(ex);
} }
} }
/// <summary> /// <summary>
/// 获取指定实验ID的指定资源类型的所有资源的ID和名称 /// 获取资源信息列表(返回ID和名称
/// </summary>
/// <param name="examId">实验ID</param>
/// <param name="resourceType">资源类型</param> /// <param name="resourceType">资源类型</param>
/// <param name="examId">实验ID可选</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// </summary>
/// <returns>资源信息列表</returns> /// <returns>资源信息列表</returns>
public Result<(int ID, string Name)[]> GetExamResourceList(string examId, string resourceType) public Result<(int ID, string Name)[]> GetResourceList(string resourceType, string? examId = null, string? resourcePurpose = null, Guid? userId = null)
{ {
try try
{ {
var resources = this.ExamResourceTable var query = this.ResourceTable.Where(r => r.ResourceType == resourceType);
.Where(r => r.ExamID == examId && r.ResourceType == resourceType)
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query
.Select(r => new { r.ID, r.ResourceName }) .Select(r => new { r.ID, r.ResourceName })
.ToArray(); .ToArray();
var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray(); var result = resources.Select(r => (r.ID, r.ResourceName)).ToArray();
logger.Info($"获取实验资源列表: {examId}/{resourceType},共 {result.Length} 个资源"); logger.Info($"获取资源列表: {resourceType}" +
(examId != null ? $"/{examId}" : "") +
(resourcePurpose != null ? $"/{resourcePurpose}" : "") +
(userId != null ? $"/{userId}" : "") +
$",共 {result.Length} 个资源");
return new(result); return new(result);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.Error($"获取实验资源列表时出错: {ex.Message}"); logger.Error($"获取资源列表时出错: {ex.Message}");
return new(ex);
}
}
/// <summary>
/// 获取完整的资源列表
/// </summary>
/// <param name="examId">实验ID可选</param>
/// <param name="resourceType">资源类型(可选)</param>
/// <param name="resourcePurpose">资源用途(可选)</param>
/// <param name="userId">用户ID可选</param>
/// <returns>完整的资源对象列表</returns>
public Result<List<Resource>> GetFullResourceList(string? examId = null, string? resourceType = null, string? resourcePurpose = null, Guid? userId = null)
{
try
{
var query = this.ResourceTable.AsQueryable();
if (examId != null)
{
query = query.Where(r => r.ExamID == examId);
}
if (resourceType != null)
{
query = query.Where(r => r.ResourceType == resourceType);
}
if (resourcePurpose != null)
{
query = query.Where(r => r.ResourcePurpose == resourcePurpose);
}
if (userId != null)
{
query = query.Where(r => r.UserID == userId);
}
var resources = query.OrderByDescending(r => r.UploadTime).ToList();
logger.Info($"获取完整资源列表" +
(examId != null ? $" [实验: {examId}]" : "") +
(resourceType != null ? $" [类型: {resourceType}]" : "") +
(resourcePurpose != null ? $" [用途: {resourcePurpose}]" : "") +
(userId != null ? $" [用户: {userId}]" : "") +
$",共 {resources.Count} 个资源");
return new(resources);
}
catch (Exception ex)
{
logger.Error($"获取完整资源列表时出错: {ex.Message}");
return new(ex); return new(ex);
} }
} }
@@ -1022,16 +1137,16 @@ public class AppDataConnection : DataConnection
/// </summary> /// </summary>
/// <param name="resourceId">资源ID</param> /// <param name="resourceId">资源ID</param>
/// <returns>资源数据</returns> /// <returns>资源数据</returns>
public Result<Optional<ExamResource>> GetExamResourceById(int resourceId) public Result<Optional<Resource>> GetResourceById(int resourceId)
{ {
try try
{ {
var resource = this.ExamResourceTable.Where(r => r.ID == resourceId).FirstOrDefault(); var resource = this.ResourceTable.Where(r => r.ID == resourceId).FirstOrDefault();
if (resource == null) if (resource == null)
{ {
logger.Info($"未找到资源: {resourceId}"); logger.Info($"未找到资源: {resourceId}");
return new(Optional<ExamResource>.None); return new(Optional<Resource>.None);
} }
logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})"); logger.Info($"成功获取资源: {resourceId} ({resource.ResourceName})");
@@ -1045,15 +1160,15 @@ public class AppDataConnection : DataConnection
} }
/// <summary> /// <summary>
/// 删除实验资源 /// 删除资源
/// </summary> /// </summary>
/// <param name="resourceId">资源ID</param> /// <param name="resourceId">资源ID</param>
/// <returns>删除的记录数</returns> /// <returns>删除的记录数</returns>
public Result<int> DeleteExamResource(int resourceId) public Result<int> DeleteResource(int resourceId)
{ {
try try
{ {
var result = this.ExamResourceTable.Where(r => r.ID == resourceId).Delete(); var result = this.ResourceTable.Where(r => r.ID == resourceId).Delete();
logger.Info($"资源已删除: {resourceId},删除记录数: {result}"); logger.Info($"资源已删除: {resourceId},删除记录数: {result}");
return new(result); return new(result);
} }
@@ -1132,29 +1247,20 @@ public class AppDataConnection : DataConnection
} }
/// <summary> /// <summary>
/// 删除所有实验 /// 根据文件扩展名获取比特流MIME类型
/// </summary> /// </summary>
/// <returns>删除的实验数量</returns> /// <param name="extension">文件扩展名</param>
public int DeleteAllExams() /// <returns>MIME类型</returns>
private string GetBitstreamMimeType(string extension)
{ {
// 先删除所有实验资源 return extension.ToLowerInvariant() switch
var resourceDeleteCount = this.DeleteAllExamResources(); {
logger.Info($"已删除所有实验资源,共删除 {resourceDeleteCount} 个资源"); ".bit" => "application/octet-stream",
".sbit" => "application/octet-stream",
// 再删除所有实验 ".bin" => "application/octet-stream",
var examDeleteCount = this.ExamTable.Delete(); ".mcs" => "application/octet-stream",
logger.Info($"已删除所有实验,共删除 {examDeleteCount} 个实验"); ".hex" => "text/plain",
return examDeleteCount; _ => "application/octet-stream"
} };
/// <summary>
/// 删除所有实验资源
/// </summary>
/// <returns>删除的资源数量</returns>
public int DeleteAllExamResources()
{
var deleteCount = this.ExamResourceTable.Delete();
logger.Info($"已删除所有实验资源,共删除 {deleteCount} 个资源");
return deleteCount;
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using DotNext; using DotNext;
using Peripherals.PowerClient; using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.CameraClient; namespace Peripherals.CameraClient;
@@ -19,7 +20,7 @@ class Camera
{ {
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 2000; readonly int timeout = 500;
readonly int taskID; readonly int taskID;
readonly int port; readonly int port;
readonly string address; readonly string address;
@@ -43,7 +44,7 @@ class Camera
/// <param name="address">摄像头设备IP地址</param> /// <param name="address">摄像头设备IP地址</param>
/// <param name="port">摄像头设备端口</param> /// <param name="port">摄像头设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
public Camera(string address, int port, int timeout = 2000) public Camera(string address, int port, int timeout = 500)
{ {
if (timeout < 0) if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout)); throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -225,6 +226,7 @@ class Camera
this.taskID, // taskID this.taskID, // taskID
FrameAddr, FrameAddr,
(int)_currentFrameLength, // 使用当前分辨率的动态大小 (int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.ExtendBurst,
this.timeout); this.timeout);
if (!result.IsSuccessful) if (!result.IsSuccessful)
@@ -462,6 +464,20 @@ class Camera
); );
} }
/// <summary>
/// 配置为960x540分辨率
/// </summary>
/// <returns>配置结果</returns>
public async ValueTask<Result<bool>> ConfigureResolution960x540()
{
return await ConfigureResolution(
hStart: 0, vStart: 0,
dvpHo: 960, dvpVo: 540,
hts: 1700, vts: 1500,
hOffset: 16, vOffset: 4
);
}
/// <summary> /// <summary>
/// 配置为320x240分辨率 /// 配置为320x240分辨率
/// </summary> /// </summary>
@@ -543,6 +559,9 @@ class Camera
case "640x480": case "640x480":
result = await ConfigureResolution640x480(); result = await ConfigureResolution640x480();
break; break;
case "960x540":
result = await ConfigureResolution960x540();
break;
case "1280x720": case "1280x720":
result = await ConfigureResolution1280x720(); result = await ConfigureResolution1280x720();
break; break;

View File

@@ -0,0 +1,118 @@
using System.Net;
using DotNext;
using Peripherals.PowerClient;
using WebProtocol;
namespace Peripherals.HdmiInClient;
static class HdmiInAddr
{
public const UInt32 BASE = 0xA000_0000;
public const UInt32 HdmiIn_CTRL = BASE + 0x0; //[0]: rstn, 0 is reset.
public const UInt32 HdmiIn_READFIFO = BASE + 0x1;
}
class HdmiIn
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
readonly int timeout = 500;
readonly int taskID;
readonly int port;
readonly string address;
private IPEndPoint ep;
// 动态分辨率参数
private UInt16 _currentWidth = 960;
private UInt16 _currentHeight = 540;
private UInt32 _currentFrameLength = 960 * 540 * 2 / 4; // RGB565格式2字节/像素按4字节对齐
/// <summary>
/// 初始化HDMI输入客户端
/// </summary>
/// <param name="address">HDMI输入设备IP地址</param>
/// <param name="port">HDMI输入设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param>
public HdmiIn(string address, int port, int timeout = 500)
{
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;
}
public async ValueTask<Result<bool>> EnableTrans(bool isEnable)
{
var ret = await UDPClientPool.WriteAddr(this.ep, this.taskID, HdmiInAddr.HdmiIn_CTRL, (isEnable ? 0x00000001u : 0x00000000u));
if (!ret.IsSuccessful)
{
logger.Error($"Failed to write HdmiIn_CTRL to HdmiIn at {this.address}:{this.port}, error: {ret.Error}");
return new(ret.Error);
}
if (!ret.Value)
{
logger.Error($"HdmiIn_CTRL write returned false for HdmiIn at {this.address}:{this.port}");
return false;
}
return true;
}
/// <summary>
/// 读取一帧图像数据
/// </summary>
/// <returns>包含图像数据的字节数组</returns>
public async ValueTask<Result<byte[]>> ReadFrame()
{
// 只在第一次或出错时清除UDP缓冲区避免每帧都清除造成延迟
// MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
logger.Trace($"Reading frame from HdmiIn {this.address}");
// 使用UDPClientPool读取图像帧数据
var result = await UDPClientPool.ReadAddr4BytesAsync(
this.ep,
this.taskID, // taskID
HdmiInAddr.HdmiIn_READFIFO,
(int)_currentFrameLength, // 使用当前分辨率的动态大小
BurstType.FixedBurst,
this.timeout);
if (!result.IsSuccessful)
{
logger.Error($"Failed to read frame from HdmiIn {this.address}:{this.port}, error: {result.Error}");
// 读取失败时清除缓冲区,为下次读取做准备
try
{
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
}
catch (Exception ex)
{
logger.Warn($"Failed to clear UDP data after read error: {ex.Message}");
}
return new(result.Error);
}
logger.Trace($"Successfully read frame from HdmiIn {this.address}:{this.port}, data length: {result.Value.Length} bytes");
return result.Value;
}
/// <summary>
/// 获取当前分辨率
/// </summary>
/// <returns>当前分辨率(宽度, 高度)</returns>
public (int Width, int Height) GetCurrentResolution()
{
return (_currentWidth, _currentHeight);
}
/// <summary>
/// 获取当前帧长度
/// </summary>
/// <returns>当前帧长度</returns>
public UInt32 GetCurrentFrameLength()
{
return _currentFrameLength;
}
}

View File

@@ -612,13 +612,10 @@ public class Jtag
if (ret.Value) if (ret.Value)
{ {
var array = new UInt32[UInt32Num]; var array = new UInt32[UInt32Num];
for (int i = 0; i < UInt32Num; i++) var retData = await UDPClientPool.ReadAddr4Bytes(ep, 0, JtagAddr.READ_DATA, (int)UInt32Num);
{ if (!retData.IsSuccessful)
var retData = await ReadFIFO(JtagAddr.READ_DATA); return new(new Exception("Read FIFO failed when Load DR"));
if (!retData.IsSuccessful) Buffer.BlockCopy(retData.Value, 0, array, 0, (int)UInt32Num * 4);
return new(new Exception("Read FIFO failed when Load DR"));
array[i] = retData.Value;
}
return array; return array;
} }
else else
@@ -788,7 +785,7 @@ public class Jtag
{ {
var paser = new BsdlParser.Parser(); var paser = new BsdlParser.Parser();
var portNum = paser.GetBoundaryRegsNum().Value; var portNum = paser.GetBoundaryRegsNum().Value;
logger.Debug($"Get boundar scan registers number: {portNum}"); logger.Debug($"Get boundary scan registers number: {portNum}");
// Clear Data // Clear Data
MsgBus.UDPServer.ClearUDPData(this.address, 0); MsgBus.UDPServer.ClearUDPData(this.address, 0);

View File

@@ -2,6 +2,7 @@ using System.Collections;
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.LogicAnalyzerClient; namespace Peripherals.LogicAnalyzerClient;
@@ -475,6 +476,7 @@ public class Analyzer
this.taskID, this.taskID,
AnalyzerAddr.STORE_OFFSET_ADDR, AnalyzerAddr.STORE_OFFSET_ADDR,
capture_length, capture_length,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using Common; using Common;
using DotNext; using DotNext;
using WebProtocol;
namespace Peripherals.OscilloscopeClient; namespace Peripherals.OscilloscopeClient;
@@ -319,6 +320,7 @@ class Oscilloscope
this.taskID, this.taskID,
OscilloscopeAddr.RD_DATA_ADDR, OscilloscopeAddr.RD_DATA_ADDR,
(int)OscilloscopeAddr.RD_DATA_LENGTH / 32, (int)OscilloscopeAddr.RD_DATA_LENGTH / 32,
BurstType.ExtendBurst, // 使用扩展突发读取
this.timeout this.timeout
); );
if (!ret.IsSuccessful) if (!ret.IsSuccessful)

View File

@@ -1109,6 +1109,7 @@ public class HttpVideoStreamService : BackgroundService
return new List<(int, int, string)> return new List<(int, int, string)>
{ {
(640, 480, "640x480 (VGA)"), (640, 480, "640x480 (VGA)"),
(960, 540, "960x540 (qHD)"),
(1280, 720, "1280x720 (HD)"), (1280, 720, "1280x720 (HD)"),
(1280, 960, "1280x960 (SXGA)"), (1280, 960, "1280x960 (SXGA)"),
(1920, 1080, "1920x1080 (Full HD)") (1920, 1080, "1920x1080 (Full HD)")

View File

@@ -433,11 +433,12 @@ public class UDPClientPool
/// <param name="endPoint">IP端点IP地址与端口</param> /// <param name="endPoint">IP端点IP地址与端口</param>
/// <param name="taskID">任务ID</param> /// <param name="taskID">任务ID</param>
/// <param name="devAddr">设备地址</param> /// <param name="devAddr">设备地址</param>
/// <param name="burstType">突发类型</param>
/// <param name="dataLength">要读取的数据长度4字节</param> /// <param name="dataLength">要读取的数据长度4字节</param>
/// <param name="timeout">超时时间(毫秒)</param> /// <param name="timeout">超时时间(毫秒)</param>
/// <returns>读取结果,包含接收到的字节数组</returns> /// <returns>读取结果,包含接收到的字节数组</returns>
public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync( public static async ValueTask<Result<byte[]>> ReadAddr4BytesAsync(
IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, int timeout = 1000) IPEndPoint endPoint, int taskID, UInt32 devAddr, int dataLength, BurstType burstType, int timeout = 1000)
{ {
var pkgList = new List<SendAddrPackage>(); var pkgList = new List<SendAddrPackage>();
var resultData = new List<byte>(); var resultData = new List<byte>();
@@ -460,7 +461,7 @@ public class UDPClientPool
var opts = new SendAddrPackOptions var opts = new SendAddrPackOptions
{ {
BurstType = BurstType.FixedBurst, BurstType = burstType,
CommandID = Convert.ToByte(taskID), CommandID = Convert.ToByte(taskID),
IsWrite = false, IsWrite = false,
BurstLength = (byte)(currentSegmentSize - 1), BurstLength = (byte)(currentSegmentSize - 1),

View File

@@ -2942,277 +2942,6 @@ export class ExamClient {
} }
return Promise.resolve<ExamInfo>(null as any); return Promise.resolve<ExamInfo>(null as any);
} }
/**
* 添加实验资源
* @param examId 实验ID
* @param resourceType 资源类型
* @param file (optional) 资源文件
* @return 添加结果
*/
addExamResource(examId: string, resourceType: string, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<ResourceInfo> {
let url_ = this.baseUrl + "/api/Exam/{examId}/resources/{resourceType}";
if (examId === undefined || examId === null)
throw new Error("The parameter 'examId' must be defined.");
url_ = url_.replace("{examId}", encodeURIComponent("" + examId));
if (resourceType === undefined || resourceType === null)
throw new Error("The parameter 'resourceType' must be defined.");
url_ = url_.replace("{resourceType}", encodeURIComponent("" + resourceType));
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (file === null || file === undefined)
throw new Error("The parameter 'file' cannot be null.");
else
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processAddExamResource(_response);
});
}
protected processAddExamResource(response: AxiosResponse): Promise<ResourceInfo> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 201) {
const _responseText = response.data;
let result201: any = null;
let resultData201 = _responseText;
result201 = ResourceInfo.fromJS(resultData201);
return Promise.resolve<ResourceInfo>(result201);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 403) {
const _responseText = response.data;
let result403: any = null;
let resultData403 = _responseText;
result403 = ProblemDetails.fromJS(resultData403);
return throwException("A server side error occurred.", status, _responseText, _headers, result403);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 409) {
const _responseText = response.data;
let result409: any = null;
let resultData409 = _responseText;
result409 = ProblemDetails.fromJS(resultData409);
return throwException("A server side error occurred.", status, _responseText, _headers, result409);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo>(null as any);
}
/**
* 获取指定实验ID的指定资源类型的所有资源的ID和名称
* @param examId 实验ID
* @param resourceType 资源类型
* @return 资源列表
*/
getExamResourceList(examId: string, resourceType: string, cancelToken?: CancelToken): Promise<ResourceInfo[]> {
let url_ = this.baseUrl + "/api/Exam/{examId}/resources/{resourceType}";
if (examId === undefined || examId === null)
throw new Error("The parameter 'examId' must be defined.");
url_ = url_.replace("{examId}", encodeURIComponent("" + examId));
if (resourceType === undefined || resourceType === null)
throw new Error("The parameter 'resourceType' must be defined.");
url_ = url_.replace("{resourceType}", encodeURIComponent("" + resourceType));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetExamResourceList(_response);
});
}
protected processGetExamResourceList(response: AxiosResponse): Promise<ResourceInfo[]> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
if (Array.isArray(resultData200)) {
result200 = [] as any;
for (let item of resultData200)
result200!.push(ResourceInfo.fromJS(item));
}
else {
result200 = <any>null;
}
return Promise.resolve<ResourceInfo[]>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo[]>(null as any);
}
/**
* 根据资源ID下载资源
* @param resourceId 资源ID
* @return 资源文件
*/
getExamResourceById(resourceId: number, cancelToken?: CancelToken): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Exam/resources/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
responseType: "blob",
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetExamResourceById(_response);
});
}
protected processGetExamResourceById(response: AxiosResponse): Promise<FileResponse> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined;
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
if (fileName) {
fileName = decodeURIComponent(fileName);
} else {
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
}
return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<FileResponse>(null as any);
}
} }
export class JtagClient { export class JtagClient {
@@ -3427,98 +3156,14 @@ export class JtagClient {
return Promise.resolve<void>(null as any); return Promise.resolve<void>(null as any);
} }
/**
* 上传比特流文件到服务器
* @param address (optional) 目标设备地址
* @param file (optional) 比特流文件
* @return 上传结果
*/
uploadBitstream(address: string | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/UploadBitstream?";
if (address === null)
throw new Error("The parameter 'address' cannot be null.");
else if (address !== undefined)
url_ += "address=" + encodeURIComponent("" + address) + "&";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (file === null || file === undefined)
throw new Error("The parameter 'file' cannot be null.");
else
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processUploadBitstream(_response);
});
}
protected processUploadBitstream(response: AxiosResponse): Promise<boolean> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return Promise.resolve<boolean>(result200);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<boolean>(null as any);
}
/** /**
* 通过 JTAG 下载比特流文件到 FPGA 设备 * 通过 JTAG 下载比特流文件到 FPGA 设备
* @param address (optional) JTAG 设备地址 * @param address (optional) JTAG 设备地址
* @param port (optional) JTAG 设备端口 * @param port (optional) JTAG 设备端口
* @param bitstreamId (optional) 比特流ID
* @return 下载结果 * @return 下载结果
*/ */
downloadBitstream(address: string | undefined, port: number | undefined, cancelToken?: CancelToken): Promise<boolean> { downloadBitstream(address: string | undefined, port: number | undefined, bitstreamId: number | undefined, cancelToken?: CancelToken): Promise<boolean> {
let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?"; let url_ = this.baseUrl + "/api/Jtag/DownloadBitstream?";
if (address === null) if (address === null)
throw new Error("The parameter 'address' cannot be null."); throw new Error("The parameter 'address' cannot be null.");
@@ -3528,6 +3173,10 @@ export class JtagClient {
throw new Error("The parameter 'port' cannot be null."); throw new Error("The parameter 'port' cannot be null.");
else if (port !== undefined) else if (port !== undefined)
url_ += "port=" + encodeURIComponent("" + port) + "&"; url_ += "port=" + encodeURIComponent("" + port) + "&";
if (bitstreamId === null)
throw new Error("The parameter 'bitstreamId' cannot be null.");
else if (bitstreamId !== undefined)
url_ += "bitstreamId=" + encodeURIComponent("" + bitstreamId) + "&";
url_ = url_.replace(/[?&]$/, ""); url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = { let options_: AxiosRequestConfig = {
@@ -6565,6 +6214,353 @@ export class RemoteUpdateClient {
} }
} }
export class ResourceClient {
protected instance: AxiosInstance;
protected baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(baseUrl?: string, instance?: AxiosInstance) {
this.instance = instance || axios.create();
this.baseUrl = baseUrl ?? "http://127.0.0.1:5000";
}
/**
* 添加资源(文件上传)
* @param resourceType (optional) 资源类型
* @param resourcePurpose (optional) 资源用途template/user
* @param examID (optional) 所属实验ID可选
* @param file (optional) 资源文件
* @return 添加结果
*/
addResource(resourceType: string | undefined, resourcePurpose: string | undefined, examID: string | null | undefined, file: FileParameter | undefined, cancelToken?: CancelToken): Promise<ResourceInfo> {
let url_ = this.baseUrl + "/api/Resource";
url_ = url_.replace(/[?&]$/, "");
const content_ = new FormData();
if (resourceType === null || resourceType === undefined)
throw new Error("The parameter 'resourceType' cannot be null.");
else
content_.append("ResourceType", resourceType.toString());
if (resourcePurpose === null || resourcePurpose === undefined)
throw new Error("The parameter 'resourcePurpose' cannot be null.");
else
content_.append("ResourcePurpose", resourcePurpose.toString());
if (examID !== null && examID !== undefined)
content_.append("ExamID", examID.toString());
if (file === null || file === undefined)
throw new Error("The parameter 'file' cannot be null.");
else
content_.append("file", file.data, file.fileName ? file.fileName : "file");
let options_: AxiosRequestConfig = {
data: content_,
method: "POST",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processAddResource(_response);
});
}
protected processAddResource(response: AxiosResponse): Promise<ResourceInfo> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 201) {
const _responseText = response.data;
let result201: any = null;
let resultData201 = _responseText;
result201 = ResourceInfo.fromJS(resultData201);
return Promise.resolve<ResourceInfo>(result201);
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo>(null as any);
}
/**
* 获取资源列表
* @param examId (optional) 实验ID可选
* @param resourceType (optional) 资源类型(可选)
* @param resourcePurpose (optional) 资源用途(可选)
* @return 资源列表
*/
getResourceList(examId: string | null | undefined, resourceType: string | null | undefined, resourcePurpose: string | null | undefined, cancelToken?: CancelToken): Promise<ResourceInfo[]> {
let url_ = this.baseUrl + "/api/Resource?";
if (examId !== undefined && examId !== null)
url_ += "examId=" + encodeURIComponent("" + examId) + "&";
if (resourceType !== undefined && resourceType !== null)
url_ += "resourceType=" + encodeURIComponent("" + resourceType) + "&";
if (resourcePurpose !== undefined && resourcePurpose !== null)
url_ += "resourcePurpose=" + encodeURIComponent("" + resourcePurpose) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetResourceList(_response);
});
}
protected processGetResourceList(response: AxiosResponse): Promise<ResourceInfo[]> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200) {
const _responseText = response.data;
let result200: any = null;
let resultData200 = _responseText;
if (Array.isArray(resultData200)) {
result200 = [] as any;
for (let item of resultData200)
result200!.push(ResourceInfo.fromJS(item));
}
else {
result200 = <any>null;
}
return Promise.resolve<ResourceInfo[]>(result200);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<ResourceInfo[]>(null as any);
}
/**
* 根据资源ID下载资源
* @param resourceId 资源ID
* @return 资源文件
*/
getResourceById(resourceId: number, cancelToken?: CancelToken): Promise<FileResponse> {
let url_ = this.baseUrl + "/api/Resource/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
responseType: "blob",
method: "GET",
url: url_,
headers: {
"Accept": "application/json"
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processGetResourceById(_response);
});
}
protected processGetResourceById(response: AxiosResponse): Promise<FileResponse> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 200 || status === 206) {
const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined;
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
if (fileName) {
fileName = decodeURIComponent(fileName);
} else {
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
}
return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers });
} else if (status === 400) {
const _responseText = response.data;
let result400: any = null;
let resultData400 = _responseText;
result400 = ProblemDetails.fromJS(resultData400);
return throwException("A server side error occurred.", status, _responseText, _headers, result400);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<FileResponse>(null as any);
}
/**
* 删除资源
* @param resourceId 资源ID
* @return 删除结果
*/
deleteResource(resourceId: number, cancelToken?: CancelToken): Promise<void> {
let url_ = this.baseUrl + "/api/Resource/{resourceId}";
if (resourceId === undefined || resourceId === null)
throw new Error("The parameter 'resourceId' must be defined.");
url_ = url_.replace("{resourceId}", encodeURIComponent("" + resourceId));
url_ = url_.replace(/[?&]$/, "");
let options_: AxiosRequestConfig = {
method: "DELETE",
url: url_,
headers: {
},
cancelToken
};
return this.instance.request(options_).catch((_error: any) => {
if (isAxiosError(_error) && _error.response) {
return _error.response;
} else {
throw _error;
}
}).then((_response: AxiosResponse) => {
return this.processDeleteResource(_response);
});
}
protected processDeleteResource(response: AxiosResponse): Promise<void> {
const status = response.status;
let _headers: any = {};
if (response.headers && typeof response.headers === "object") {
for (const k in response.headers) {
if (response.headers.hasOwnProperty(k)) {
_headers[k] = response.headers[k];
}
}
}
if (status === 204) {
const _responseText = response.data;
return Promise.resolve<void>(null as any);
} else if (status === 401) {
const _responseText = response.data;
let result401: any = null;
let resultData401 = _responseText;
result401 = ProblemDetails.fromJS(resultData401);
return throwException("A server side error occurred.", status, _responseText, _headers, result401);
} else if (status === 403) {
const _responseText = response.data;
let result403: any = null;
let resultData403 = _responseText;
result403 = ProblemDetails.fromJS(resultData403);
return throwException("A server side error occurred.", status, _responseText, _headers, result403);
} else if (status === 404) {
const _responseText = response.data;
let result404: any = null;
let resultData404 = _responseText;
result404 = ProblemDetails.fromJS(resultData404);
return throwException("A server side error occurred.", status, _responseText, _headers, result404);
} else if (status === 500) {
const _responseText = response.data;
return throwException("A server side error occurred.", status, _responseText, _headers);
} else if (status !== 200 && status !== 204) {
const _responseText = response.data;
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}
return Promise.resolve<void>(null as any);
}
}
export class TutorialClient { export class TutorialClient {
protected instance: AxiosInstance; protected instance: AxiosInstance;
protected baseUrl: string; protected baseUrl: string;
@@ -8023,52 +8019,6 @@ export interface ICreateExamRequest {
isVisibleToUsers: boolean; isVisibleToUsers: boolean;
} }
/** 资源信息类 */
export class ResourceInfo implements IResourceInfo {
/** 资源ID */
id!: number;
/** 资源名称 */
name!: string;
constructor(data?: IResourceInfo) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.id = _data["id"];
this.name = _data["name"];
}
}
static fromJS(data: any): ResourceInfo {
data = typeof data === 'object' ? data : {};
let result = new ResourceInfo();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["id"] = this.id;
data["name"] = this.name;
return data;
}
}
/** 资源信息类 */
export interface IResourceInfo {
/** 资源ID */
id: number;
/** 资源名称 */
name: string;
}
/** 逻辑分析仪运行状态枚举 */ /** 逻辑分析仪运行状态枚举 */
export enum CaptureStatus { export enum CaptureStatus {
None = 0, None = 0,
@@ -8518,6 +8468,82 @@ export interface IOscilloscopeDataResponse {
waveformData: string; waveformData: string;
} }
/** 资源信息类 */
export class ResourceInfo implements IResourceInfo {
/** 资源ID */
id!: number;
/** 资源名称 */
name!: string;
/** 资源类型 */
type!: string;
/** 资源用途template/user */
purpose!: string;
/** 上传时间 */
uploadTime!: Date;
/** 所属实验ID可选 */
examID?: string | undefined;
/** MIME类型 */
mimeType?: string | undefined;
constructor(data?: IResourceInfo) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.id = _data["id"];
this.name = _data["name"];
this.type = _data["type"];
this.purpose = _data["purpose"];
this.uploadTime = _data["uploadTime"] ? new Date(_data["uploadTime"].toString()) : <any>undefined;
this.examID = _data["examID"];
this.mimeType = _data["mimeType"];
}
}
static fromJS(data: any): ResourceInfo {
data = typeof data === 'object' ? data : {};
let result = new ResourceInfo();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["id"] = this.id;
data["name"] = this.name;
data["type"] = this.type;
data["purpose"] = this.purpose;
data["uploadTime"] = this.uploadTime ? this.uploadTime.toISOString() : <any>undefined;
data["examID"] = this.examID;
data["mimeType"] = this.mimeType;
return data;
}
}
/** 资源信息类 */
export interface IResourceInfo {
/** 资源ID */
id: number;
/** 资源名称 */
name: string;
/** 资源类型 */
type: string;
/** 资源用途template/user */
purpose: string;
/** 上传时间 */
uploadTime: Date;
/** 所属实验ID可选 */
examID?: string | undefined;
/** MIME类型 */
mimeType?: string | undefined;
}
/** Package options which to send address to read or write */ /** Package options which to send address to read or write */
export class SendAddrPackOptions implements ISendAddrPackOptions { export class SendAddrPackOptions implements ISendAddrPackOptions {
/** 突发类型 */ /** 突发类型 */

View File

@@ -88,17 +88,17 @@ export async function loadDiagramData(examId?: string): Promise<DiagramData> {
// 如果提供了examId优先从API加载实验的diagram // 如果提供了examId优先从API加载实验的diagram
if (examId) { if (examId) {
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 获取diagram类型的资源列表 // 获取diagram类型的资源列表
const resources = await examClient.getExamResourceList(examId, 'canvas'); const resources = await resourceClient.getResourceList(examId, 'canvas', 'template');
if (resources && resources.length > 0) { if (resources && resources.length > 0) {
// 获取第一个diagram资源 // 获取第一个diagram资源
const diagramResource = resources[0]; const diagramResource = resources[0];
// 使用动态API获取资源文件内容 // 使用动态API获取资源文件内容
const response = await examClient.getExamResourceById(diagramResource.id); const response = await resourceClient.getResourceById(diagramResource.id);
if (response && response.data) { if (response && response.data) {
const text = await response.data.text(); const text = await response.data.text();

View File

@@ -6,8 +6,6 @@ import hljs from 'highlight.js';
import 'highlight.js/styles/github.css'; // 亮色主题 import 'highlight.js/styles/github.css'; // 亮色主题
// 导入主题存储 // 导入主题存储
import { useThemeStore } from '@/stores/theme'; import { useThemeStore } from '@/stores/theme';
// 导入ExamClient用于获取图片资源
import { ExamClient } from '@/APIClient';
import { AuthManager } from '@/utils/AuthManager'; import { AuthManager } from '@/utils/AuthManager';
const props = defineProps({ const props = defineProps({
@@ -36,8 +34,8 @@ const imageResourceCache = ref<Map<string, string>>(new Map());
// 获取图片资源ID的函数 // 获取图片资源ID的函数
async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> { async function getImageResourceId(examId: string, imagePath: string): Promise<string | null> {
try { try {
const client = AuthManager.createAuthenticatedExamClient(); const client = AuthManager.createAuthenticatedResourceClient();
const resources = await client.getExamResourceList(examId, 'images'); const resources = await client.getResourceList(examId, 'images', 'template');
// 查找匹配的图片资源 // 查找匹配的图片资源
const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath)); const imageResource = resources.find(r => r.name === imagePath || r.name.endsWith(imagePath));
@@ -52,8 +50,8 @@ async function getImageResourceId(examId: string, imagePath: string): Promise<st
// 通过资源ID获取图片数据URL // 通过资源ID获取图片数据URL
async function getImageDataUrl(resourceId: string): Promise<string | null> { async function getImageDataUrl(resourceId: string): Promise<string | null> {
try { try {
const client = AuthManager.createAuthenticatedExamClient(); const client = AuthManager.createAuthenticatedResourceClient();
const response = await client.getExamResourceById(parseInt(resourceId)); const response = await client.getResourceById(parseInt(resourceId));
if (response && response.data) { if (response && response.data) {
return URL.createObjectURL(response.data); return URL.createObjectURL(response.data);

View File

@@ -127,12 +127,13 @@ onMounted(async () => {
let thumbnail: string | undefined; let thumbnail: string | undefined;
try { try {
// 获取实验的封面资源 // 获取实验的封面资源(模板资源)
const resourceList = await client.getExamResourceList(exam.id, 'cover'); const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resourceList = await resourceClient.getResourceList(exam.id, 'cover', 'template');
if (resourceList && resourceList.length > 0) { if (resourceList && resourceList.length > 0) {
// 使用第一个封面资源 // 使用第一个封面资源
const coverResource = resourceList[0]; const coverResource = resourceList[0];
const fileResponse = await client.getExamResourceById(coverResource.id); const fileResponse = await resourceClient.getResourceById(coverResource.id);
// 创建Blob URL作为缩略图 // 创建Blob URL作为缩略图
thumbnail = URL.createObjectURL(fileResponse.data); thumbnail = URL.createObjectURL(fileResponse.data);
} }

View File

@@ -8,7 +8,7 @@
<fieldset class="fieldset w-full"> <fieldset class="fieldset w-full">
<legend class="fieldset-legend text-sm">示例比特流文件</legend> <legend class="fieldset-legend text-sm">示例比特流文件</legend>
<div class="space-y-2"> <div class="space-y-2">
<div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 bg-base-200 rounded"> <div v-for="bitstream in availableBitstreams" :key="bitstream.id" class="flex items-center justify-between p-2 border-2 border-base-300 rounded-lg">
<span class="text-sm">{{ bitstream.name }}</span> <span class="text-sm">{{ bitstream.name }}</span>
<div class="flex gap-2"> <div class="flex gap-2">
<button <button
@@ -75,8 +75,8 @@ import { useDialogStore } from "@/stores/dialog";
import { isNull, isUndefined } from "lodash"; import { isNull, isUndefined } from "lodash";
interface Props { interface Props {
uploadEvent?: (file: File) => Promise<boolean>; uploadEvent?: (file: File, examId: string) => Promise<number | null>;
downloadEvent?: () => Promise<boolean>; downloadEvent?: (bitstreamId: number) => Promise<boolean>;
maxMemory?: number; maxMemory?: number;
examId?: string; // 新增examId属性 examId?: string; // 新增examId属性
} }
@@ -127,9 +127,9 @@ async function loadAvailableBitstreams() {
} }
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用新的API获取比特流资源列表 // 使用新的ResourceClient API获取比特流模板资源列表
const resources = await examClient.getExamResourceList(props.examId, 'bitstream'); const resources = await resourceClient.getResourceList(props.examId, 'bitstream', 'template');
availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || []; availableBitstreams.value = resources.map(r => ({ id: r.id, name: r.name })) || [];
} catch (error) { } catch (error) {
console.error('加载比特流列表失败:', error); console.error('加载比特流列表失败:', error);
@@ -143,10 +143,10 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
isDownloading.value = true; isDownloading.value = true;
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用动态API获取资源文件 // 使用新的ResourceClient API获取资源文件
const response = await examClient.getExamResourceById(bitstream.id); const response = await resourceClient.getResourceById(bitstream.id);
if (response && response.data) { if (response && response.data) {
// 创建下载链接 // 创建下载链接
@@ -173,37 +173,21 @@ async function downloadExampleBitstream(bitstream: {id: number, name: string}) {
// 直接烧录示例比特流 // 直接烧录示例比特流
async function programExampleBitstream(bitstream: {id: number, name: string}) { async function programExampleBitstream(bitstream: {id: number, name: string}) {
if (isProgramming.value || !props.uploadEvent) return; if (isProgramming.value) return;
isProgramming.value = true; isProgramming.value = true;
try { try {
const examClient = AuthManager.createAuthenticatedExamClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
// 使用动态API获取比特流文件数据 if (props.downloadEvent) {
const response = await examClient.getExamResourceById(bitstream.id); const downloadSuccess = await props.downloadEvent(bitstream.id);
if (downloadSuccess) {
if (!response || !response.data) { dialog.info("示例比特流烧录成功");
throw new Error('获取比特流文件失败');
}
const file = new File([response.data], response.fileName || bitstream.name, { type: response.data.type });
// 调用上传事件
const uploadSuccess = await props.uploadEvent(file);
if (uploadSuccess) {
// 如果有下载事件(烧录),则执行
if (props.downloadEvent) {
const downloadSuccess = await props.downloadEvent();
if (downloadSuccess) {
dialog.info("示例比特流烧录成功");
} else {
dialog.error("烧录失败");
}
} else { } else {
dialog.info("示例比特流上传成功"); dialog.error("烧录失败");
} }
} else { } else {
dialog.error("上传失败"); dialog.info("示例比特流props.downloadEvent未定义 无法烧录");
} }
} catch (error) { } catch (error) {
console.error('烧录示例比特流失败:', error); console.error('烧录示例比特流失败:', error);
@@ -234,6 +218,7 @@ function checkFile(file: File): boolean {
} }
async function handleClick(event: Event): Promise<void> { async function handleClick(event: Event): Promise<void> {
console.log("上传按钮被点击");
if (isNull(bitstream.value) || isUndefined(bitstream.value)) { if (isNull(bitstream.value) || isUndefined(bitstream.value)) {
dialog.error(`未选择文件`); dialog.error(`未选择文件`);
return; return;
@@ -246,19 +231,21 @@ async function handleClick(event: Event): Promise<void> {
} }
isUploading.value = true; isUploading.value = true;
let uploadedBitstreamId: number | null = null;
try { try {
const ret = await props.uploadEvent(bitstream.value); console.log("开始上传比特流文件:", bitstream.value.name);
const bitstreamId = await props.uploadEvent(bitstream.value, props.examId || '');
console.log("上传结果ID:", bitstreamId);
if (isUndefined(props.downloadEvent)) { if (isUndefined(props.downloadEvent)) {
if (ret) { console.log("上传成功,下载未定义");
dialog.info("上传成功");
emits("finishedUpload", bitstream.value);
} else dialog.error("上传失败");
return;
}
if (!ret) {
isUploading.value = false; isUploading.value = false;
return; return;
} }
if (bitstreamId === null || bitstreamId === undefined) {
isUploading.value = false;
return;
}
uploadedBitstreamId = bitstreamId;
} catch (e) { } catch (e) {
dialog.error("上传失败"); dialog.error("上传失败");
console.error(e); console.error(e);
@@ -267,9 +254,14 @@ async function handleClick(event: Event): Promise<void> {
// Download // Download
try { try {
const ret = await props.downloadEvent(); console.log("开始下载比特流ID:", uploadedBitstreamId);
if (ret) dialog.info("下载成功"); if (uploadedBitstreamId === null || uploadedBitstreamId === undefined) {
else dialog.error("下载失败"); dialog.error("uploadedBitstreamId is null or undefined");
} else {
const ret = await props.downloadEvent(uploadedBitstreamId);
if (ret) dialog.info("下载成功");
else dialog.error("下载失败");
}
} catch (e) { } catch (e) {
dialog.error("下载失败"); dialog.error("下载失败");
console.error(e); console.error(e);

View File

@@ -22,8 +22,9 @@
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<UploadCard <UploadCard
:exam-id="props.examId"
:upload-event="eqps.jtagUploadBitstream" :upload-event="eqps.jtagUploadBitstream"
:download-event="eqps.jtagDownloadBitstream" :download-event="handleDownloadBitstream"
:bitstream-file="eqps.jtagBitstream" :bitstream-file="eqps.jtagBitstream"
@update:bitstream-file="handleBitstreamChange" @update:bitstream-file="handleBitstreamChange"
> >
@@ -127,6 +128,11 @@ function handleBitstreamChange(file: File | undefined) {
eqps.jtagBitstream = file; eqps.jtagBitstream = file;
} }
async function handleDownloadBitstream(bitstreamId: number): Promise<boolean> {
console.log("开始下载比特流ID:", bitstreamId);
return await eqps.jtagDownloadBitstream(bitstreamId);
}
function handleSelectJtagSpeed(event: Event) { function handleSelectJtagSpeed(event: Event) {
const target = event.target as HTMLSelectElement; const target = event.target as HTMLSelectElement;
eqps.jtagSetSpeed(target.selectedIndex); eqps.jtagSetSpeed(target.selectedIndex);

View File

@@ -11,6 +11,7 @@ import { toFileParameterOrUndefined } from "@/utils/Common";
import { AuthManager } from "@/utils/AuthManager"; import { AuthManager } from "@/utils/AuthManager";
import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr"; import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr";
import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client"; import { getHubProxyFactory, getReceiverRegister } from "@/TypedSignalR.Client";
import type { ResourceInfo } from "@/APIClient";
import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs.JtagHub"; import type { IJtagHub } from "@/TypedSignalR.Client/server.Hubs.JtagHub";
export const useEquipments = defineStore("equipments", () => { export const useEquipments = defineStore("equipments", () => {
@@ -24,6 +25,7 @@ export const useEquipments = defineStore("equipments", () => {
// Jtag // Jtag
const jtagBitstream = ref<File>(); const jtagBitstream = ref<File>();
const jtagBoundaryScanFreq = ref(100); const jtagBoundaryScanFreq = ref(100);
const jtagUserBitstreams = ref<ResourceInfo[]>([]);
const jtagClientMutex = withTimeout( const jtagClientMutex = withTimeout(
new Mutex(), new Mutex(),
1000, 1000,
@@ -121,25 +123,38 @@ export const useEquipments = defineStore("equipments", () => {
enableJtagBoundaryScan.value = enable; enableJtagBoundaryScan.value = enable;
} }
async function jtagUploadBitstream(bitstream: File): Promise<boolean> { async function jtagUploadBitstream(bitstream: File, examId?: string): Promise<number | null> {
try { try {
// 自动开启电源 // 自动开启电源
await powerSetOnOff(true); await powerSetOnOff(true);
const jtagClient = AuthManager.createAuthenticatedJtagClient(); const resourceClient = AuthManager.createAuthenticatedResourceClient();
const resp = await jtagClient.uploadBitstream( const resp = await resourceClient.addResource(
boardAddr.value, 'bitstream',
'user',
examId || null,
toFileParameterOrUndefined(bitstream), toFileParameterOrUndefined(bitstream),
); );
return resp;
// 如果上传成功,设置为当前选中的比特流
if (resp && resp.id !== undefined && resp.id !== null) {
return resp.id;
}
return null;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("上传错误");
console.error(e); console.error(e);
return false; return null;
} }
} }
async function jtagDownloadBitstream(): Promise<boolean> { async function jtagDownloadBitstream(bitstreamId?: number): Promise<boolean> {
if (bitstreamId === null || bitstreamId === undefined) {
dialog.error("请先选择要下载的比特流");
return false;
}
const release = await jtagClientMutex.acquire(); const release = await jtagClientMutex.acquire();
try { try {
// 自动开启电源 // 自动开启电源
@@ -149,10 +164,11 @@ export const useEquipments = defineStore("equipments", () => {
const resp = await jtagClient.downloadBitstream( const resp = await jtagClient.downloadBitstream(
boardAddr.value, boardAddr.value,
boardPort.value, boardPort.value,
bitstreamId,
); );
return resp; return resp;
} catch (e) { } catch (e) {
dialog.error("上传错误"); dialog.error("下载错误");
console.error(e); console.error(e);
return false; return false;
} finally { } finally {
@@ -281,6 +297,7 @@ export const useEquipments = defineStore("equipments", () => {
jtagBoundaryScanSetOnOff, jtagBoundaryScanSetOnOff,
jtagBitstream, jtagBitstream,
jtagBoundaryScanFreq, jtagBoundaryScanFreq,
jtagUserBitstreams,
jtagUploadBitstream, jtagUploadBitstream,
jtagDownloadBitstream, jtagDownloadBitstream,
jtagGetIDCode, jtagGetIDCode,

View File

@@ -14,6 +14,7 @@ import {
OscilloscopeApiClient, OscilloscopeApiClient,
DebuggerClient, DebuggerClient,
ExamClient, ExamClient,
ResourceClient,
} from "@/APIClient"; } from "@/APIClient";
import router from "@/router"; import router from "@/router";
import { HubConnectionBuilder } from "@microsoft/signalr"; import { HubConnectionBuilder } from "@microsoft/signalr";
@@ -36,7 +37,8 @@ type SupportedClient =
| NetConfigClient | NetConfigClient
| OscilloscopeApiClient | OscilloscopeApiClient
| DebuggerClient | DebuggerClient
| ExamClient; | ExamClient
| ResourceClient;
export class AuthManager { export class AuthManager {
// 存储token到localStorage // 存储token到localStorage
@@ -199,6 +201,10 @@ export class AuthManager {
return AuthManager.createAuthenticatedClient(ExamClient); return AuthManager.createAuthenticatedClient(ExamClient);
} }
public static createAuthenticatedResourceClient(): ResourceClient {
return AuthManager.createAuthenticatedClient(ResourceClient);
}
public static createAuthenticatedJtagHubConnection() { public static createAuthenticatedJtagHubConnection() {
const token = this.getToken(); const token = this.getToken();
if (isNull(token)) { if (isNull(token)) {

View File

@@ -679,15 +679,15 @@ const downloadResources = async () => {
downloadingResources.value = true downloadingResources.value = true
try { try {
const client = AuthManager.createAuthenticatedExamClient() const resourceClient = AuthManager.createAuthenticatedResourceClient()
// 获取资源包列表 // 获取资源包列表(模板资源)
const resourceList = await client.getExamResourceList(selectedExam.value.id, 'resource') const resourceList = await resourceClient.getResourceList(selectedExam.value.id, 'resource', 'template')
if (resourceList && resourceList.length > 0) { if (resourceList && resourceList.length > 0) {
// 使用动态API获取第一个资源包 // 使用新的ResourceClient API获取第一个资源包
const resourceId = resourceList[0].id const resourceId = resourceList[0].id
const fileResponse = await client.getExamResourceById(resourceId) const fileResponse = await resourceClient.getResourceById(resourceId)
// 创建Blob URL // 创建Blob URL
const blobUrl = URL.createObjectURL(fileResponse.data) const blobUrl = URL.createObjectURL(fileResponse.data)
@@ -925,7 +925,7 @@ const submitCreateExam = async () => {
// 上传实验资源 // 上传实验资源
const uploadExamResources = async (examId: string) => { const uploadExamResources = async (examId: string) => {
const client = AuthManager.createAuthenticatedExamClient() const client = AuthManager.createAuthenticatedResourceClient()
try { try {
// 上传MD文档 // 上传MD文档
@@ -934,7 +934,7 @@ const uploadExamResources = async (examId: string) => {
data: uploadFiles.value.mdFile, data: uploadFiles.value.mdFile,
fileName: uploadFiles.value.mdFile.name fileName: uploadFiles.value.mdFile.name
} }
await client.addExamResource(examId, 'doc', mdFileParam) await client.addResource('doc', 'template', examId, mdFileParam)
console.log('MD文档上传成功') console.log('MD文档上传成功')
} }
@@ -944,7 +944,7 @@ const uploadExamResources = async (examId: string) => {
data: imageFile, data: imageFile,
fileName: imageFile.name fileName: imageFile.name
} }
await client.addExamResource(examId, 'image', imageFileParam) await client.addResource('image', 'template', examId, imageFileParam)
console.log('图片上传成功:', imageFile.name) console.log('图片上传成功:', imageFile.name)
} }
@@ -954,7 +954,7 @@ const uploadExamResources = async (examId: string) => {
data: bitstreamFile, data: bitstreamFile,
fileName: bitstreamFile.name fileName: bitstreamFile.name
} }
await client.addExamResource(examId, 'bitstream', bitstreamFileParam) await client.addResource('bitstream', 'template', examId, bitstreamFileParam)
console.log('比特流文件上传成功:', bitstreamFile.name) console.log('比特流文件上传成功:', bitstreamFile.name)
} }
@@ -964,7 +964,7 @@ const uploadExamResources = async (examId: string) => {
data: canvasFile, data: canvasFile,
fileName: canvasFile.name fileName: canvasFile.name
} }
await client.addExamResource(examId, 'canvas', canvasFileParam) await client.addResource('canvas', 'template', examId, canvasFileParam)
console.log('画布模板上传成功:', canvasFile.name) console.log('画布模板上传成功:', canvasFile.name)
} }
@@ -974,7 +974,7 @@ const uploadExamResources = async (examId: string) => {
data: uploadFiles.value.resourceFile, data: uploadFiles.value.resourceFile,
fileName: uploadFiles.value.resourceFile.name fileName: uploadFiles.value.resourceFile.name
} }
await client.addExamResource(examId, 'resource', resourceFileParam) await client.addResource('resource', 'template', examId, resourceFileParam)
console.log('资源包上传成功') console.log('资源包上传成功')
} }

View File

@@ -37,7 +37,7 @@
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
<SplitterResizeHandle <SplitterResizeHandle
id="splitter-group-h-resize-handle" id="splitter-group-h-resize-handle"
class="w-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="w-1 bg-base-300"
/> />
<!-- 右侧编辑区域 --> <!-- 右侧编辑区域 -->
<SplitterPanel <SplitterPanel
@@ -74,7 +74,7 @@
<SplitterResizeHandle <SplitterResizeHandle
v-show="!isBottomBarFullscreen" v-show="!isBottomBarFullscreen"
id="splitter-group-v-resize-handle" id="splitter-group-v-resize-handle"
class="h-2 bg-base-100 hover:bg-primary hover:opacity-70 transition-colors" class="h-1 bg-base-300"
/> />
<!-- 功能底栏 --> <!-- 功能底栏 -->
@@ -217,17 +217,17 @@ async function loadDocumentContent() {
if (examId) { if (examId) {
// 如果有实验ID从API加载实验文档 // 如果有实验ID从API加载实验文档
console.log('加载实验文档:', examId); console.log('加载实验文档:', examId);
const client = AuthManager.createAuthenticatedExamClient(); const client = AuthManager.createAuthenticatedResourceClient();
// 获取markdown类型的资源列表 // 获取markdown类型的模板资源列表
const resources = await client.getExamResourceList(examId, 'doc'); const resources = await client.getResourceList(examId, 'doc', 'template');
if (resources && resources.length > 0) { if (resources && resources.length > 0) {
// 获取第一个markdown资源 // 获取第一个markdown资源
const markdownResource = resources[0]; const markdownResource = resources[0];
// 使用动态API获取资源文件内容 // 使用新的ResourceClient API获取资源文件内容
const response = await client.getExamResourceById(markdownResource.id); const response = await client.getResourceById(markdownResource.id);
if (!response || !response.data) { if (!response || !response.data) {
throw new Error('获取markdown文件失败'); throw new Error('获取markdown文件失败');