✨ feat(server): 添加静态状态页与调试入口
- 将状态页、调试页改为 `wwwroot` 静态资源 - 补充调试配置接口与前端脚本 - 为兼容层、规划层和运行时补充日志 - 更新集成测试覆盖新入口
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,14 +12,17 @@ namespace Flyshot.Server.Host.Controllers;
|
||||
public sealed class LegacyHttpApiController : ControllerBase
|
||||
{
|
||||
private readonly IControllerClientCompatService _compatService;
|
||||
private readonly ILogger<LegacyHttpApiController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化旧 HTTP 兼容控制器。
|
||||
/// </summary>
|
||||
/// <param name="compatService">ControllerClient 兼容服务。</param>
|
||||
public LegacyHttpApiController(IControllerClientCompatService compatService)
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public LegacyHttpApiController(IControllerClientCompatService compatService, ILogger<LegacyHttpApiController> logger)
|
||||
{
|
||||
_compatService = compatService ?? throw new ArgumentNullException(nameof(compatService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,13 +44,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/connect_server/")]
|
||||
public IActionResult ConnectServer([FromQuery] string server_ip, [FromQuery] int port)
|
||||
{
|
||||
_logger.LogInformation("ConnectServer 调用: server_ip={ServerIp}, port={Port}", server_ip, port);
|
||||
try
|
||||
{
|
||||
_compatService.ConnectServer(server_ip, port);
|
||||
_logger.LogInformation("ConnectServer 成功: server_ip={ServerIp}, port={Port}", server_ip, port);
|
||||
return Ok(new { status = "connected" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "ConnectServer 失败: server_ip={ServerIp}, port={Port}", server_ip, port);
|
||||
return LegacyBadRequest("Connect Server failed");
|
||||
}
|
||||
}
|
||||
@@ -80,13 +86,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/setup_robot/")]
|
||||
public IActionResult SetupRobot([FromQuery] string robot_name)
|
||||
{
|
||||
_logger.LogInformation("SetupRobot 调用: robot_name={RobotName}", robot_name);
|
||||
try
|
||||
{
|
||||
_compatService.SetUpRobot(robot_name);
|
||||
_logger.LogInformation("SetupRobot 成功: robot_name={RobotName}", robot_name);
|
||||
return Ok(new { status = "robot setup" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "SetupRobot 失败: robot_name={RobotName}", robot_name);
|
||||
return LegacyBadRequest("SetUpRobot failed");
|
||||
}
|
||||
}
|
||||
@@ -152,13 +161,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpGet("/enable_robot/")]
|
||||
public IActionResult EnableRobot([FromQuery] int buffer_size = 2)
|
||||
{
|
||||
_logger.LogInformation("EnableRobot 调用: buffer_size={BufferSize}", buffer_size);
|
||||
try
|
||||
{
|
||||
_compatService.EnableRobot(buffer_size);
|
||||
_logger.LogInformation("EnableRobot 成功");
|
||||
return Ok(new { enable_robot = true });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "EnableRobot 失败");
|
||||
return LegacyBadRequest("EnableRobot failed");
|
||||
}
|
||||
}
|
||||
@@ -170,13 +182,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpGet("/disable_robot/")]
|
||||
public IActionResult DisableRobot()
|
||||
{
|
||||
_logger.LogInformation("DisableRobot 调用");
|
||||
try
|
||||
{
|
||||
_compatService.DisableRobot();
|
||||
_logger.LogInformation("DisableRobot 成功");
|
||||
return Ok(new { disable_robot = true });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "DisableRobot 失败");
|
||||
return LegacyBadRequest("DisableRobot failed");
|
||||
}
|
||||
}
|
||||
@@ -188,13 +203,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpGet("/stop_move/")]
|
||||
public IActionResult StopMove()
|
||||
{
|
||||
_logger.LogInformation("StopMove 调用");
|
||||
try
|
||||
{
|
||||
_compatService.StopMove();
|
||||
_logger.LogInformation("StopMove 成功");
|
||||
return Ok(new { status = "move stopped" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "StopMove 失败");
|
||||
return LegacyBadRequest("StopMove failed");
|
||||
}
|
||||
}
|
||||
@@ -207,13 +225,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/set_active_controller/")]
|
||||
public IActionResult SetActiveController([FromQuery] bool sim)
|
||||
{
|
||||
_logger.LogInformation("SetActiveController 调用: sim={Sim}", sim);
|
||||
try
|
||||
{
|
||||
_compatService.SetActiveController(sim);
|
||||
_logger.LogInformation("SetActiveController 成功: sim={Sim}", sim);
|
||||
return Ok(new { status = "active controller set" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "SetActiveController 失败: sim={Sim}", sim);
|
||||
return LegacyBadRequest("SetActiveController failed");
|
||||
}
|
||||
}
|
||||
@@ -226,13 +247,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/connect_robot/")]
|
||||
public IActionResult ConnectRobot([FromQuery] string ip)
|
||||
{
|
||||
_logger.LogInformation("ConnectRobot 调用: ip={Ip}", ip);
|
||||
try
|
||||
{
|
||||
_compatService.Connect(ip);
|
||||
_logger.LogInformation("ConnectRobot 成功: ip={Ip}", ip);
|
||||
return Ok(new { status = "robot connected" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "ConnectRobot 失败: ip={Ip}", ip);
|
||||
return LegacyBadRequest("Connect failed");
|
||||
}
|
||||
}
|
||||
@@ -244,13 +268,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/disconnect_robot/")]
|
||||
public IActionResult DisconnectRobot()
|
||||
{
|
||||
_logger.LogInformation("DisconnectRobot 调用");
|
||||
try
|
||||
{
|
||||
_compatService.Disconnect();
|
||||
_logger.LogInformation("DisconnectRobot 成功");
|
||||
return Ok(new { status = "robot disconnected" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "DisconnectRobot 失败");
|
||||
return LegacyBadRequest("Disconnect failed");
|
||||
}
|
||||
}
|
||||
@@ -286,13 +313,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/set_tcp/")]
|
||||
public IActionResult SetTcp([FromBody] LegacyTcpRequest tcp_data)
|
||||
{
|
||||
_logger.LogInformation("SetTcp 调用: x={X}, y={Y}, z={Z}", tcp_data.x, tcp_data.y, tcp_data.z);
|
||||
try
|
||||
{
|
||||
_compatService.SetTcp(tcp_data.x, tcp_data.y, tcp_data.z);
|
||||
_logger.LogInformation("SetTcp 成功");
|
||||
return Ok(new { status = "TCP set" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "SetTcp 失败");
|
||||
return LegacyBadRequest("SetTCP failed");
|
||||
}
|
||||
}
|
||||
@@ -324,13 +354,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/set_io/")]
|
||||
public IActionResult SetIo([FromQuery] int port, [FromQuery] bool value, [FromQuery] string io_type)
|
||||
{
|
||||
_logger.LogInformation("SetIo 调用: port={Port}, value={Value}, io_type={IoType}", port, value, io_type);
|
||||
try
|
||||
{
|
||||
_compatService.SetIo(port, value, io_type);
|
||||
_logger.LogInformation("SetIo 成功: port={Port}, value={Value}", port, value);
|
||||
return Ok(new { status = "IO set" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "SetIo 失败: port={Port}, value={Value}", port, value);
|
||||
return LegacyBadRequest("SetDigitalOutput failed");
|
||||
}
|
||||
}
|
||||
@@ -344,12 +377,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpGet("/get_io/")]
|
||||
public IActionResult GetIo([FromQuery] int port, [FromQuery] string io_type)
|
||||
{
|
||||
_logger.LogInformation("GetIo 调用: port={Port}, io_type={IoType}", port, io_type);
|
||||
try
|
||||
{
|
||||
return Ok(new { value = _compatService.GetIo(port, io_type) });
|
||||
var value = _compatService.GetIo(port, io_type);
|
||||
_logger.LogInformation("GetIo 成功: port={Port}, value={Value}", port, value);
|
||||
return Ok(new { value });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "GetIo 失败: port={Port}", port);
|
||||
return LegacyBadRequest("GetDigitalOutput failed");
|
||||
}
|
||||
}
|
||||
@@ -379,13 +416,17 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/move_joint/")]
|
||||
public IActionResult MoveJoint([FromBody] LegacyJointPositionRequest joint_data)
|
||||
{
|
||||
_logger.LogInformation("MoveJoint 调用: 关节数={JointCount}", joint_data.joints.Count);
|
||||
_logger.LogDebug("MoveJoint 路点: {Joints}", string.Join(", ", joint_data.joints.Select(j => j.ToString("F4"))));
|
||||
try
|
||||
{
|
||||
_compatService.MoveJoint(joint_data.joints);
|
||||
_logger.LogInformation("MoveJoint 成功");
|
||||
return Ok(new { status = "robot moved" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "MoveJoint 失败");
|
||||
return LegacyBadRequest("MoveJoint failed");
|
||||
}
|
||||
}
|
||||
@@ -441,16 +482,20 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[FromQuery] string? method = null,
|
||||
[FromQuery] bool? save_traj = null)
|
||||
{
|
||||
_logger.LogInformation("ExecuteTrajectory 调用: method={Method}, save_traj={SaveTraj}", method ?? "icsp", save_traj ?? false);
|
||||
try
|
||||
{
|
||||
var request = ParseExecuteTrajectoryRequest(waypoints, method, save_traj);
|
||||
_logger.LogDebug("ExecuteTrajectory 路点数={WaypointCount}, method={Method}", request.Waypoints.Count, request.Method);
|
||||
_compatService.ExecuteTrajectory(
|
||||
request.Waypoints,
|
||||
new TrajectoryExecutionOptions(request.Method, request.SaveTrajectory));
|
||||
_logger.LogInformation("ExecuteTrajectory 成功: method={Method}", request.Method);
|
||||
return Ok(new { status = "trajectory executed" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "ExecuteTrajectory 失败");
|
||||
return LegacyBadRequest("ExecuteTrajectory failed");
|
||||
}
|
||||
}
|
||||
@@ -463,18 +508,30 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/upload_flyshot/")]
|
||||
public IActionResult UploadFlyshot([FromBody] LegacyFlightTrajectoryRequest trajectory_data)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"UploadFlyshot 调用: name={Name}, waypoints={WaypointCount}, shot_flags={ShotCount}",
|
||||
trajectory_data.name,
|
||||
trajectory_data.waypoints.Count,
|
||||
trajectory_data.shot_flags.Count(static f => f));
|
||||
|
||||
if (trajectory_data.shot_flags.Count != trajectory_data.waypoints.Count)
|
||||
{
|
||||
_logger.LogWarning("UploadFlyshot 校验失败: shot_flags长度({ShotFlagsCount}) != 路点数({WaypointCount})",
|
||||
trajectory_data.shot_flags.Count, trajectory_data.waypoints.Count);
|
||||
return LegacyValidationError("shot_flags长度必须与路点数量相同");
|
||||
}
|
||||
|
||||
if (trajectory_data.offset_values.Count != trajectory_data.waypoints.Count)
|
||||
{
|
||||
_logger.LogWarning("UploadFlyshot 校验失败: offset_values长度({OffsetCount}) != 路点数({WaypointCount})",
|
||||
trajectory_data.offset_values.Count, trajectory_data.waypoints.Count);
|
||||
return LegacyValidationError("offset_values长度必须与路点数量相同");
|
||||
}
|
||||
|
||||
if (trajectory_data.addrs.Count != trajectory_data.waypoints.Count)
|
||||
{
|
||||
_logger.LogWarning("UploadFlyshot 校验失败: addrs长度({AddrCount}) != 路点数({WaypointCount})",
|
||||
trajectory_data.addrs.Count, trajectory_data.waypoints.Count);
|
||||
return LegacyValidationError("addrs长度必须与路点数量相同");
|
||||
}
|
||||
|
||||
@@ -488,10 +545,12 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
addressGroups: trajectory_data.addrs);
|
||||
|
||||
_compatService.UploadTrajectory(trajectory);
|
||||
_logger.LogInformation("UploadFlyshot 成功: name={Name}", trajectory_data.name);
|
||||
return Ok(new { status = "FlyShot uploaded" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "UploadFlyshot 失败: name={Name}", trajectory_data.name);
|
||||
return LegacyBadRequest("UploadFlyShotTraj failed");
|
||||
}
|
||||
}
|
||||
@@ -504,6 +563,9 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/execute_flyshot/")]
|
||||
public IActionResult ExecuteFlyshot([FromBody] LegacyExecuteFlyshotRequest data)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"ExecuteFlyshot 调用: name={Name}, method={Method}, move_to_start={MoveToStart}, use_cache={UseCache}",
|
||||
data.name, data.method, data.move_to_start, data.use_cache);
|
||||
try
|
||||
{
|
||||
_compatService.ExecuteTrajectoryByName(
|
||||
@@ -513,10 +575,12 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
method: data.method,
|
||||
saveTrajectory: data.save_traj,
|
||||
useCache: data.use_cache));
|
||||
_logger.LogInformation("ExecuteFlyshot 成功: name={Name}", data.name);
|
||||
return Ok(new { status = "FlyShot executed", success = true });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "ExecuteFlyshot 失败: name={Name}", data.name);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { detail = exception.Message });
|
||||
}
|
||||
}
|
||||
@@ -529,17 +593,21 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/save_traj_info/")]
|
||||
public IActionResult SaveTrajectoryInfo([FromBody] LegacyTrajectoryInfoRequest request)
|
||||
{
|
||||
_logger.LogInformation("SaveTrajectoryInfo 调用: name={Name}, method={Method}", request.name, request.method);
|
||||
try
|
||||
{
|
||||
_compatService.SaveTrajectoryInfo(request.name, request.method);
|
||||
_logger.LogInformation("SaveTrajectoryInfo 成功: name={Name}", request.name);
|
||||
return Ok(new { status = "trajectory info saved", success = true });
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "SaveTrajectoryInfo 不支持: name={Name}", request.name);
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { detail = exception.Message });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "SaveTrajectoryInfo 失败: name={Name}", request.name);
|
||||
return LegacyBadRequest("SaveTrajInfo failed");
|
||||
}
|
||||
}
|
||||
@@ -552,6 +620,7 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/is_flyShotTrajValid/")]
|
||||
public IActionResult IsFlyshotTrajectoryValid([FromBody] LegacyFlyshotValidationRequest request)
|
||||
{
|
||||
_logger.LogInformation("IsFlyshotTrajectoryValid 调用: name={Name}, method={Method}", request.name, request.method);
|
||||
try
|
||||
{
|
||||
var isValid = _compatService.IsFlyshotTrajectoryValid(
|
||||
@@ -560,14 +629,17 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
request.method,
|
||||
request.save_traj);
|
||||
|
||||
_logger.LogInformation("IsFlyshotTrajectoryValid 结果: name={Name}, valid={Valid}, duration={Duration}s", request.name, isValid, duration.TotalSeconds);
|
||||
return Ok(new { success = isValid, valid = isValid, time = duration.TotalSeconds });
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "IsFlyshotTrajectoryValid 不支持: name={Name}", request.name);
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { detail = exception.Message });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "IsFlyshotTrajectoryValid 失败: name={Name}", request.name);
|
||||
return LegacyBadRequest("IsFlyShotTrajValid failed");
|
||||
}
|
||||
}
|
||||
@@ -580,13 +652,23 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/set_speedRatio/")]
|
||||
public IActionResult SetSpeedRatio([FromBody] LegacySpeedRatioRequest data)
|
||||
{
|
||||
_logger.LogInformation("SetSpeedRatio 调用: speed={Speed}", data.speed);
|
||||
try
|
||||
{
|
||||
// 验证数值 范围符合预期(例如 0.01到 1.0),以避免对控制器造成潜在风险
|
||||
if (data.speed < 0.01 || data.speed > 1.0)
|
||||
{
|
||||
_logger.LogWarning("SetSpeedRatio 参数无效: speed={Speed}", data.speed);
|
||||
return BadRequest(new { detail = "Speed ratio must be between 0.01 and 1.0." });
|
||||
}
|
||||
|
||||
_compatService.SetSpeedRatio(data.speed);
|
||||
_logger.LogInformation("SetSpeedRatio 成功: speed={Speed}", data.speed);
|
||||
return Ok(new { message = "set_speedRatio executed", returnCode = 0 });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "SetSpeedRatio 失败: speed={Speed}", data.speed);
|
||||
return LegacyBadRequest("set_speedRatio failed");
|
||||
}
|
||||
}
|
||||
@@ -599,13 +681,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/delete_flyshot/")]
|
||||
public IActionResult DeleteFlyshot([FromBody] LegacyNameRequest request)
|
||||
{
|
||||
_logger.LogInformation("DeleteFlyshot 调用: name={Name}", request.name);
|
||||
try
|
||||
{
|
||||
_compatService.DeleteTrajectory(request.name);
|
||||
_logger.LogInformation("DeleteFlyshot 成功: name={Name}", request.name);
|
||||
return Ok(new { status = "FlyShot deleted" });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "DeleteFlyshot 失败: name={Name}", request.name);
|
||||
return LegacyBadRequest("DeleteFlyShotTraj failed");
|
||||
}
|
||||
}
|
||||
@@ -618,22 +703,28 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
[HttpPost("/init_mpc_robt")]
|
||||
public IActionResult InitMpcRobot([FromBody] LegacyInitMpcRobotRequest data)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"InitMpcRobot 调用: robot_name={RobotName}, robot_ip={RobotIp}, sim={Sim}, server={ServerIp}:{Port}",
|
||||
data.robot_name, data.robot_ip, data.sim, data.server_ip, data.port);
|
||||
try
|
||||
{
|
||||
_compatService.ConnectServer(data.server_ip, data.port);
|
||||
_compatService.SetUpRobot(data.robot_name);
|
||||
if (!_compatService.IsSetUp)
|
||||
{
|
||||
_logger.LogWarning("InitMpcRobot 失败: Robot not setup");
|
||||
return LegacyBadRequest("Robot not setup");
|
||||
}
|
||||
|
||||
_compatService.SetActiveController(data.sim);
|
||||
_compatService.Connect(data.robot_ip);
|
||||
_compatService.EnableRobot(2);
|
||||
_logger.LogInformation("InitMpcRobot 成功: robot_name={RobotName}", data.robot_name);
|
||||
return Ok(new { message = "init_Success", returnCode = 0 });
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "InitMpcRobot 失败");
|
||||
return LegacyBadRequest("Connect Server failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,390 +4,12 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace Flyshot.Server.Host.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 提供只读状态监控页面和控制器状态快照 API。
|
||||
/// 提供控制器状态快照 API,状态监控页面由 wwwroot 静态资源承载。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Tags("基础与状态")]
|
||||
public sealed class StatusController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 浏览器端状态监控页面,页面逻辑只读取状态快照,不触发控制动作。
|
||||
/// </summary>
|
||||
private const string StatusPageHtml = """
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Flyshot Replacement 状态监控</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f5f7fb;
|
||||
--surface: #ffffff;
|
||||
--line: #d8dee9;
|
||||
--text: #172033;
|
||||
--muted: #5b667a;
|
||||
--accent: #007c89;
|
||||
--good: #12805c;
|
||||
--warn: #b7791f;
|
||||
--bad: #b42318;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: "Segoe UI", "Microsoft YaHei", Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
padding: 18px 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
color: #ffffff;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 顶部操作区按钮和外链按钮共用同一组视觉样式,便于现场顺手跳转。 */
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
font: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-button:hover {
|
||||
background: rgba(0, 124, 137, 0.08);
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(1180px, calc(100% - 32px));
|
||||
margin: 22px auto;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.metric,
|
||||
section {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.metric {
|
||||
min-height: 86px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin-top: 8px;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 24px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex: 0 0 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--warn);
|
||||
}
|
||||
|
||||
.dot.good {
|
||||
background: var(--good);
|
||||
}
|
||||
|
||||
.dot.bad {
|
||||
background: var(--bad);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: 160px minmax(0, 1fr);
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 4px 16px 12px;
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
min-height: 36px;
|
||||
margin: 0;
|
||||
padding: 9px 0;
|
||||
border-bottom: 1px solid #edf1f7;
|
||||
}
|
||||
|
||||
dt {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
dd {
|
||||
overflow-wrap: anywhere;
|
||||
font-family: Consolas, "Cascadia Mono", monospace;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--muted);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.topbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary,
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
dl {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
dt {
|
||||
border-bottom: 0;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
dd {
|
||||
padding-top: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="topbar">
|
||||
<h1>Flyshot Replacement 状态监控</h1>
|
||||
<div class="actions">
|
||||
<a class="link-button" href="/debug" target="_blank" rel="noopener">调试接口</a>
|
||||
<button id="refresh" type="button">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="summary">
|
||||
<div class="metric">
|
||||
<div class="label">连接状态</div>
|
||||
<div class="value status-row"><span id="state-dot" class="dot"></span><span id="connection-state">--</span></div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">机器人</div>
|
||||
<div id="robot-name" class="value">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">速度倍率</div>
|
||||
<div id="speed-ratio" class="value">--</div>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="label">运动中</div>
|
||||
<div id="motion-state" class="value">--</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<section>
|
||||
<h2>控制器</h2>
|
||||
<dl>
|
||||
<dt>服务端版本</dt><dd id="server-version">--</dd>
|
||||
<dt>客户端版本</dt><dd id="client-version">--</dd>
|
||||
<dt>已初始化</dt><dd id="setup-state">--</dd>
|
||||
<dt>已使能</dt><dd id="enabled-state">--</dd>
|
||||
<dt>J519 状态</dt><dd id="j519-status">--</dd>
|
||||
<dt>J519 序号</dt><dd id="j519-sequence">--</dd>
|
||||
<dt>采样时间</dt><dd id="captured-at">--</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section>
|
||||
<h2>机器人</h2>
|
||||
<dl>
|
||||
<dt>自由度</dt><dd id="dof">--</dd>
|
||||
<dt>关节位置</dt><dd id="joints">--</dd>
|
||||
<dt>TCP 位姿</dt><dd id="pose">--</dd>
|
||||
<dt>已上传轨迹</dt><dd id="trajectories" class="empty">--</dd>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
const fields = {
|
||||
connectionState: document.getElementById("connection-state"),
|
||||
stateDot: document.getElementById("state-dot"),
|
||||
robotName: document.getElementById("robot-name"),
|
||||
speedRatio: document.getElementById("speed-ratio"),
|
||||
motionState: document.getElementById("motion-state"),
|
||||
serverVersion: document.getElementById("server-version"),
|
||||
clientVersion: document.getElementById("client-version"),
|
||||
setupState: document.getElementById("setup-state"),
|
||||
enabledState: document.getElementById("enabled-state"),
|
||||
j519Status: document.getElementById("j519-status"),
|
||||
j519Sequence: document.getElementById("j519-sequence"),
|
||||
capturedAt: document.getElementById("captured-at"),
|
||||
dof: document.getElementById("dof"),
|
||||
joints: document.getElementById("joints"),
|
||||
pose: document.getElementById("pose"),
|
||||
trajectories: document.getElementById("trajectories"),
|
||||
refresh: document.getElementById("refresh")
|
||||
};
|
||||
|
||||
function formatArray(values) {
|
||||
if (!Array.isArray(values) || values.length === 0) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return values.map(value => Number(value).toFixed(4)).join(", ");
|
||||
}
|
||||
|
||||
function formatNullableBool(value) {
|
||||
if (value === true) {
|
||||
return "是";
|
||||
}
|
||||
|
||||
if (value === false) {
|
||||
return "否";
|
||||
}
|
||||
|
||||
return "--";
|
||||
}
|
||||
|
||||
function formatJ519Status(snapshot) {
|
||||
if (snapshot.j519Status === null || snapshot.j519Status === undefined) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
const status = Number(snapshot.j519Status).toString(16).padStart(2, "0").toUpperCase();
|
||||
return `0x${status} accept=${formatNullableBool(snapshot.j519AcceptsCommand)} received=${formatNullableBool(snapshot.j519ReceivedCommand)} sysrdy=${formatNullableBool(snapshot.j519SystemReady)} motion=${formatNullableBool(snapshot.j519RobotInMotion)}`;
|
||||
}
|
||||
|
||||
function setDot(connectionState) {
|
||||
fields.stateDot.className = "dot";
|
||||
if (connectionState === "Connected") {
|
||||
fields.stateDot.classList.add("good");
|
||||
} else if (connectionState === "NotConfigured") {
|
||||
fields.stateDot.classList.add("bad");
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
fields.refresh.disabled = true;
|
||||
try {
|
||||
const response = await fetch("/api/status/snapshot", { cache: "no-store" });
|
||||
const payload = await response.json();
|
||||
const snapshot = payload.snapshot;
|
||||
|
||||
fields.connectionState.textContent = snapshot.connectionState;
|
||||
fields.robotName.textContent = payload.robotName || "--";
|
||||
fields.speedRatio.textContent = Number(snapshot.speedRatio).toFixed(2);
|
||||
fields.motionState.textContent = snapshot.isInMotion ? "是" : "否";
|
||||
fields.serverVersion.textContent = payload.serverVersion;
|
||||
fields.clientVersion.textContent = payload.clientVersion;
|
||||
fields.setupState.textContent = payload.isSetup ? "是" : "否";
|
||||
fields.enabledState.textContent = snapshot.isEnabled ? "是" : "否";
|
||||
fields.j519Status.textContent = formatJ519Status(snapshot);
|
||||
fields.j519Sequence.textContent = snapshot.j519Sequence ?? "--";
|
||||
fields.capturedAt.textContent = new Date(snapshot.capturedAt).toLocaleString();
|
||||
fields.dof.textContent = payload.degreesOfFreedom;
|
||||
fields.joints.textContent = formatArray(snapshot.jointPositions);
|
||||
fields.pose.textContent = formatArray(snapshot.cartesianPose);
|
||||
fields.trajectories.textContent = payload.uploadedTrajectories.length > 0
|
||||
? payload.uploadedTrajectories.join(", ")
|
||||
: "--";
|
||||
fields.trajectories.classList.toggle("empty", payload.uploadedTrajectories.length === 0);
|
||||
setDot(snapshot.connectionState);
|
||||
} finally {
|
||||
fields.refresh.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
fields.refresh.addEventListener("click", refreshStatus);
|
||||
refreshStatus();
|
||||
window.setInterval(refreshStatus, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
private readonly IControllerClientCompatService _compatService;
|
||||
|
||||
/// <summary>
|
||||
@@ -400,13 +22,23 @@ public sealed class StatusController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回浏览器可直接打开的状态监控页面。
|
||||
/// 提供短路由 `/status`,跳转到静态状态页。
|
||||
/// </summary>
|
||||
/// <returns>HTML 状态页面。</returns>
|
||||
/// <returns>重定向到 <c>/status.html</c>。</returns>
|
||||
[HttpGet("/status")]
|
||||
public ContentResult GetStatusPage()
|
||||
public IActionResult StatusPage()
|
||||
{
|
||||
return Content(StatusPageHtml, "text/html; charset=utf-8");
|
||||
return Redirect("/status.html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供短路由 `/debug`,跳转到静态调试页。
|
||||
/// </summary>
|
||||
/// <returns>重定向到 <c>/debug.html</c>。</returns>
|
||||
[HttpGet("/debug")]
|
||||
public IActionResult DebugPage()
|
||||
{
|
||||
return Redirect("/debug.html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user