✨ feat(fanuc): 添加协议编解码与状态页" -m "* 固化 10010 状态帧、10012 命令帧和 60015 J519 包编解码
* 扩展 ControllerClient 兼容层的执行参数和运行时编排 * 新增 /status 页面与 /api/status/snapshot 状态快照接口 * 补充 FANUC 协议、客户端和状态接口的最小验证测试 * 更新 README、兼容要求和真机 Socket 通信实现计划
This commit is contained in:
@@ -51,6 +51,26 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `GetServerVersion` 版本查询语义。
|
||||
/// </summary>
|
||||
/// <returns>服务端版本号。</returns>
|
||||
[HttpGet("/get_server_version/")]
|
||||
public IActionResult GetServerVersion()
|
||||
{
|
||||
return Ok(new { server_version = _compatService.GetServerVersion() });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `GetClientVersion` 版本查询语义。
|
||||
/// </summary>
|
||||
/// <returns>客户端版本号。</returns>
|
||||
[HttpGet("/get_client_version/")]
|
||||
public IActionResult GetClientVersion()
|
||||
{
|
||||
return Ok(new { client_version = _compatService.GetClientVersion() });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/setup_robot/` 路由。
|
||||
/// </summary>
|
||||
@@ -70,6 +90,49 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `SetUpRobotFromEnv(env_file)` 参数形状。
|
||||
/// </summary>
|
||||
/// <param name="env_file">环境文件路径。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/setup_robot_from_env/")]
|
||||
public IActionResult SetupRobotFromEnv([FromQuery] string env_file)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetUpRobotFromEnv(env_file);
|
||||
return Ok(new { status = "robot setup" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SetUpRobotFromEnv failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `SetShowTCP(is_show, axis_length, axis_size)` 参数形状。
|
||||
/// </summary>
|
||||
/// <param name="is_show">是否显示 TCP。</param>
|
||||
/// <param name="axis_length">坐标轴长度。</param>
|
||||
/// <param name="axis_size">坐标轴线宽。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/set_show_tcp/")]
|
||||
public IActionResult SetShowTcp(
|
||||
[FromQuery] bool is_show = true,
|
||||
[FromQuery] double axis_length = 0.1,
|
||||
[FromQuery] int axis_size = 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SetShowTcp(is_show, axis_length, axis_size);
|
||||
return Ok(new { status = "show TCP set" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SetShowTCP failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/is_setup/` 路由。
|
||||
/// </summary>
|
||||
@@ -81,15 +144,16 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/enable_robot/` 路由;保持原 Python 服务固定传 `8` 的行为。
|
||||
/// 兼容旧 `EnableRobot(buffer_size=2)` 参数形状。
|
||||
/// </summary>
|
||||
/// <param name="buffer_size">控制器执行缓冲区大小。</param>
|
||||
/// <returns>旧 FastAPI 层风格的布尔状态响应。</returns>
|
||||
[HttpGet("/enable_robot/")]
|
||||
public IActionResult EnableRobot()
|
||||
public IActionResult EnableRobot([FromQuery] int buffer_size = 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.EnableRobot(8);
|
||||
_compatService.EnableRobot(buffer_size);
|
||||
return Ok(new { enable_robot = true });
|
||||
}
|
||||
catch
|
||||
@@ -116,6 +180,24 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供与旧客户端 <c>StopMove</c> 语义对应的 HTTP 端点。
|
||||
/// </summary>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpGet("/stop_move/")]
|
||||
public IActionResult StopMove()
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.StopMove();
|
||||
return Ok(new { status = "move stopped" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("StopMove failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/set_active_controller/` 路由。
|
||||
/// </summary>
|
||||
@@ -154,6 +236,24 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供与旧客户端 <c>Disconnect</c> 语义对应的 HTTP 端点。
|
||||
/// </summary>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/disconnect_robot/")]
|
||||
public IActionResult DisconnectRobot()
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.Disconnect();
|
||||
return Ok(new { status = "robot disconnected" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("Disconnect failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/robot_info/` 路由。
|
||||
/// </summary>
|
||||
@@ -234,6 +334,25 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/get_io/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="port">IO 端口号。</param>
|
||||
/// <param name="io_type">IO 类型字符串。</param>
|
||||
/// <returns>当前 IO 值。</returns>
|
||||
[HttpGet("/get_io/")]
|
||||
public IActionResult GetIo([FromQuery] int port, [FromQuery] string io_type)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(new { value = _compatService.GetIo(port, io_type) });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("GetDigitalOutput failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/get_joint_position/` 路由。
|
||||
/// </summary>
|
||||
@@ -270,6 +389,28 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `GetNearestIK(pose, seed, ik)` 参数形状。
|
||||
/// </summary>
|
||||
/// <param name="request">IK 请求体。</param>
|
||||
/// <returns>IK 结果。</returns>
|
||||
[HttpPost("/get_nearest_ik/")]
|
||||
public IActionResult GetNearestIk([FromBody] LegacyNearestIkRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Ok(new { success = true, ik = _compatService.GetNearestIk(request.pose, request.seed) });
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { detail = exception.Message });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("GetNearestIK failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/list_flyShotTraj/` 路由。
|
||||
/// </summary>
|
||||
@@ -292,11 +433,17 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
/// <param name="waypoints">轨迹请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/execute_trajectory/")]
|
||||
public IActionResult ExecuteTrajectory([FromBody] JsonElement waypoints)
|
||||
public IActionResult ExecuteTrajectory(
|
||||
[FromBody] JsonElement waypoints,
|
||||
[FromQuery] string? method = null,
|
||||
[FromQuery] bool? save_traj = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.ExecuteTrajectory(ParseLegacyTrajectoryWaypoints(waypoints));
|
||||
var request = ParseExecuteTrajectoryRequest(waypoints, method, save_traj);
|
||||
_compatService.ExecuteTrajectory(
|
||||
request.Waypoints,
|
||||
new TrajectoryExecutionOptions(request.Method, request.SaveTrajectory));
|
||||
return Ok(new { status = "trajectory executed" });
|
||||
}
|
||||
catch
|
||||
@@ -349,14 +496,20 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
/// <summary>
|
||||
/// 兼容旧 `/execute_flyshot/` 路由。
|
||||
/// </summary>
|
||||
/// <param name="data">包含轨迹名称的请求体。</param>
|
||||
/// <param name="data">包含轨迹名称和执行参数的请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/execute_flyshot/")]
|
||||
public IActionResult ExecuteFlyshot([FromBody] LegacyNameRequest data)
|
||||
public IActionResult ExecuteFlyshot([FromBody] LegacyExecuteFlyshotRequest data)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.ExecuteTrajectoryByName(data.name);
|
||||
_compatService.ExecuteTrajectoryByName(
|
||||
data.name,
|
||||
new FlyshotExecutionOptions(
|
||||
moveToStart: data.move_to_start,
|
||||
method: data.method,
|
||||
saveTrajectory: data.save_traj,
|
||||
useCache: data.use_cache));
|
||||
return Ok(new { status = "FlyShot executed", success = true });
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -365,6 +518,57 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `SaveTrajInfo(name, method)` 参数形状。
|
||||
/// </summary>
|
||||
/// <param name="request">轨迹保存请求体。</param>
|
||||
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
|
||||
[HttpPost("/save_traj_info/")]
|
||||
public IActionResult SaveTrajectoryInfo([FromBody] LegacyTrajectoryInfoRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
_compatService.SaveTrajectoryInfo(request.name, request.method);
|
||||
return Ok(new { status = "trajectory info saved", success = true });
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { detail = exception.Message });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("SaveTrajInfo failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `IsFlyShotTrajValid(time, name, method, save_traj)` 参数形状。
|
||||
/// </summary>
|
||||
/// <param name="request">轨迹有效性检查请求体。</param>
|
||||
/// <returns>有效性和轨迹时长。</returns>
|
||||
[HttpPost("/is_flyShotTrajValid/")]
|
||||
public IActionResult IsFlyshotTrajectoryValid([FromBody] LegacyFlyshotValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var isValid = _compatService.IsFlyshotTrajectoryValid(
|
||||
out var duration,
|
||||
request.name,
|
||||
request.method,
|
||||
request.save_traj);
|
||||
|
||||
return Ok(new { success = isValid, valid = isValid, time = duration.TotalSeconds });
|
||||
}
|
||||
catch (NotSupportedException exception)
|
||||
{
|
||||
return StatusCode(StatusCodes.Status501NotImplemented, new { detail = exception.Message });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return LegacyBadRequest("IsFlyShotTrajValid failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧 `/set_speedRatio/` 路由。
|
||||
/// </summary>
|
||||
@@ -420,7 +624,7 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
return LegacyBadRequest("Robot not setup");
|
||||
}
|
||||
|
||||
_compatService.SetActiveController(sim: false);
|
||||
_compatService.SetActiveController(data.sim);
|
||||
_compatService.Connect(data.robot_ip);
|
||||
_compatService.EnableRobot(2);
|
||||
return Ok(new { message = "init_Success", returnCode = 0 });
|
||||
@@ -448,6 +652,47 @@ public sealed class LegacyHttpApiController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析旧 `/execute_trajectory/` 的完整参数形状。
|
||||
/// </summary>
|
||||
/// <param name="payload">原始 JSON 请求体。</param>
|
||||
/// <param name="queryMethod">查询字符串中的 method 覆盖值。</param>
|
||||
/// <param name="querySaveTrajectory">查询字符串中的 save_traj 覆盖值。</param>
|
||||
/// <returns>统一后的路点和执行参数。</returns>
|
||||
private static (
|
||||
IReadOnlyList<IReadOnlyList<double>> Waypoints,
|
||||
string Method,
|
||||
bool SaveTrajectory) ParseExecuteTrajectoryRequest(
|
||||
JsonElement payload,
|
||||
string? queryMethod,
|
||||
bool? querySaveTrajectory)
|
||||
{
|
||||
string method = queryMethod ?? "icsp";
|
||||
bool saveTrajectory = querySaveTrajectory ?? false;
|
||||
|
||||
if (payload.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (payload.TryGetProperty("method", out var methodElement) && methodElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
method = methodElement.GetString() ?? method;
|
||||
}
|
||||
|
||||
if (payload.TryGetProperty("save_traj", out var saveTrajectoryElement))
|
||||
{
|
||||
saveTrajectory = saveTrajectoryElement.GetBoolean();
|
||||
}
|
||||
|
||||
if (!payload.TryGetProperty("waypoints", out var waypointElement))
|
||||
{
|
||||
throw new InvalidOperationException("ExecuteTrajectory request body must include waypoints.");
|
||||
}
|
||||
|
||||
return (ParseLegacyTrajectoryWaypoints(waypointElement), method, saveTrajectory);
|
||||
}
|
||||
|
||||
return (ParseLegacyTrajectoryWaypoints(payload), method, saveTrajectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析旧 `/execute_trajectory/` 可能出现的两种历史请求体形状。
|
||||
/// </summary>
|
||||
@@ -576,6 +821,90 @@ public sealed class LegacyNameRequest
|
||||
public string name { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/execute_flyshot/` 路由使用的完整执行请求体。
|
||||
/// </summary>
|
||||
public sealed class LegacyExecuteFlyshotRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹名称。
|
||||
/// </summary>
|
||||
public string name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否先移动到轨迹起点。
|
||||
/// </summary>
|
||||
public bool move_to_start { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹生成方法。
|
||||
/// </summary>
|
||||
public string method { get; init; } = "icsp";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否保存轨迹信息。
|
||||
/// </summary>
|
||||
public bool save_traj { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否复用轨迹缓存。
|
||||
/// </summary>
|
||||
public bool use_cache { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `SaveTrajInfo` 参数形状。
|
||||
/// </summary>
|
||||
public sealed class LegacyTrajectoryInfoRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹名称。
|
||||
/// </summary>
|
||||
public string name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹生成方法。
|
||||
/// </summary>
|
||||
public string method { get; init; } = "icsp";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `IsFlyShotTrajValid` 参数形状。
|
||||
/// </summary>
|
||||
public sealed class LegacyFlyshotValidationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹名称。
|
||||
/// </summary>
|
||||
public string name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置轨迹生成方法。
|
||||
/// </summary>
|
||||
public string method { get; init; } = "icsp";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否保存轨迹信息。
|
||||
/// </summary>
|
||||
public bool save_traj { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `GetNearestIK` 参数形状。
|
||||
/// </summary>
|
||||
public sealed class LegacyNearestIkRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置目标位姿 `[x,y,z,qx,qy,qz,qw]`。
|
||||
/// </summary>
|
||||
public List<double> pose { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置 IK seed 关节数组。
|
||||
/// </summary>
|
||||
public List<double> seed { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示旧 `/set_speedRatio/` 路由使用的速度倍率请求体。
|
||||
/// </summary>
|
||||
@@ -611,4 +940,9 @@ public sealed class LegacyInitMpcRobotRequest
|
||||
/// 获取或设置机器人控制器 IP。
|
||||
/// </summary>
|
||||
public string robot_ip { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置是否使用仿真控制器;默认 false 连接真机。
|
||||
/// </summary>
|
||||
public bool sim { get; init; }
|
||||
}
|
||||
|
||||
385
src/Flyshot.Server.Host/Controllers/StatusController.cs
Normal file
385
src/Flyshot.Server.Host/Controllers/StatusController.cs
Normal file
@@ -0,0 +1,385 @@
|
||||
using Flyshot.ControllerClientCompat;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Flyshot.Server.Host.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 提供只读状态监控页面和控制器状态快照 API。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
<button id="refresh" type="button">刷新</button>
|
||||
</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>采样时间</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"),
|
||||
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 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.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>
|
||||
/// 初始化状态监控控制器。
|
||||
/// </summary>
|
||||
/// <param name="compatService">ControllerClient 兼容层服务。</param>
|
||||
public StatusController(IControllerClientCompatService compatService)
|
||||
{
|
||||
_compatService = compatService ?? throw new ArgumentNullException(nameof(compatService));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回浏览器可直接打开的状态监控页面。
|
||||
/// </summary>
|
||||
/// <returns>HTML 状态页面。</returns>
|
||||
[HttpGet("/status")]
|
||||
public ContentResult GetStatusPage()
|
||||
{
|
||||
return Content(StatusPageHtml, "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 ControllerClient 兼容层与控制器运行时状态快照。
|
||||
/// </summary>
|
||||
/// <returns>面向状态页和外部诊断的 JSON 快照。</returns>
|
||||
[HttpGet("/api/status/snapshot")]
|
||||
public IActionResult GetSnapshot()
|
||||
{
|
||||
var snapshot = _compatService.GetControllerSnapshot();
|
||||
var isSetup = _compatService.IsSetUp;
|
||||
|
||||
// 状态页需要在机器人未初始化时仍能打开,因此只有初始化后才读取机器人元数据。
|
||||
var robotName = isSetup ? _compatService.GetRobotName() : null;
|
||||
var degreesOfFreedom = isSetup ? _compatService.GetDegreesOfFreedom() : 0;
|
||||
var uploadedTrajectories = isSetup ? _compatService.ListTrajectoryNames() : Array.Empty<string>();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Status = "ok",
|
||||
Service = "flyshot-server-host",
|
||||
ServerVersion = _compatService.GetServerVersion(),
|
||||
ClientVersion = _compatService.GetClientVersion(),
|
||||
IsSetup = isSetup,
|
||||
RobotName = robotName,
|
||||
DegreesOfFreedom = degreesOfFreedom,
|
||||
UploadedTrajectories = uploadedTrajectories,
|
||||
Snapshot = snapshot
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user