feat: 实现 ControllerClient HTTP 兼容层及 FANUC 运行时

- 新增 Flyshot.ControllerClientCompat 兼容层模块
  - 新增 Flyshot.Runtime.Fanuc 运行时模块
  - 新增 LegacyHttpApiController 暴露 HTTP 兼容 API
  - 补充 RuntimeOrchestrationTests 等测试覆盖
  - 补充 docs/ 兼容性需求与逆向工程文档
  - 更新 Host 注册、配置及解决方案引用

  变更概览:
  - Flyshot.ControllerClientCompat — 旧 ControllerClient 语义的 HTTP 适配
  - Flyshot.Runtime.Fanuc — IControllerRuntime 的 FANUC 真机实现
  - LegacyHttpApiController — HTTP API 兼容旧 SDK
  - docs/ — 兼容性需求与逆向工程分析文档
  - 测试:RuntimeOrchestrationTests、LegacyHttpApiCompatibilityTests
This commit is contained in:
2026-04-24 16:55:25 +08:00
parent 4eeaa3fef3
commit 8a20d9f507
35 changed files with 3869 additions and 10 deletions

View File

@@ -0,0 +1,614 @@
using System.Text.Json;
using Flyshot.ControllerClientCompat;
using Microsoft.AspNetCore.Mvc;
namespace Flyshot.Server.Host.Controllers;
/// <summary>
/// 提供对 `flyshot-uaes-interface` 既有 FastAPI HTTP 路由层的一比一 MVC 兼容控制器。
/// </summary>
[ApiController]
public sealed class LegacyHttpApiController : ControllerBase
{
private readonly IControllerClientCompatService _compatService;
/// <summary>
/// 初始化旧 HTTP 兼容控制器。
/// </summary>
/// <param name="compatService">ControllerClient 兼容服务。</param>
public LegacyHttpApiController(IControllerClientCompatService compatService)
{
_compatService = compatService ?? throw new ArgumentNullException(nameof(compatService));
}
/// <summary>
/// 兼容旧根路径探活接口。
/// </summary>
/// <returns>旧 HTTP 服务约定的 Hello World 响应。</returns>
[HttpGet("/")]
public IActionResult Root()
{
return Ok(new { message = "Hello World" });
}
/// <summary>
/// 兼容旧 `/connect_server/` 路由;在 replacement 宿主中仅记录调用方期望连接的地址。
/// </summary>
/// <param name="server_ip">旧客户端传入的服务端 IP。</param>
/// <param name="port">旧客户端传入的服务端端口。</param>
/// <returns>与旧 FastAPI 层一致的状态响应。</returns>
[HttpPost("/connect_server/")]
public IActionResult ConnectServer([FromQuery] string server_ip, [FromQuery] int port)
{
try
{
_compatService.ConnectServer(server_ip, port);
return Ok(new { status = "connected" });
}
catch
{
return LegacyBadRequest("Connect Server failed");
}
}
/// <summary>
/// 兼容旧 `/setup_robot/` 路由。
/// </summary>
/// <param name="robot_name">旧 HTTP 层使用的机器人名称。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/setup_robot/")]
public IActionResult SetupRobot([FromQuery] string robot_name)
{
try
{
_compatService.SetUpRobot(robot_name);
return Ok(new { status = "robot setup" });
}
catch
{
return LegacyBadRequest("SetUpRobot failed");
}
}
/// <summary>
/// 兼容旧 `/is_setup/` 路由。
/// </summary>
/// <returns>当前机器人是否完成初始化。</returns>
[HttpGet("/is_setup/")]
public IActionResult IsSetup()
{
return Ok(new { is_setup = _compatService.IsSetUp });
}
/// <summary>
/// 兼容旧 `/enable_robot/` 路由;保持原 Python 服务固定传 `8` 的行为。
/// </summary>
/// <returns>旧 FastAPI 层风格的布尔状态响应。</returns>
[HttpGet("/enable_robot/")]
public IActionResult EnableRobot()
{
try
{
_compatService.EnableRobot(8);
return Ok(new { enable_robot = true });
}
catch
{
return LegacyBadRequest("EnableRobot failed");
}
}
/// <summary>
/// 兼容旧 `/disable_robot/` 路由。
/// </summary>
/// <returns>旧 FastAPI 层风格的布尔状态响应。</returns>
[HttpGet("/disable_robot/")]
public IActionResult DisableRobot()
{
try
{
_compatService.DisableRobot();
return Ok(new { disable_robot = true });
}
catch
{
return LegacyBadRequest("DisableRobot failed");
}
}
/// <summary>
/// 兼容旧 `/set_active_controller/` 路由。
/// </summary>
/// <param name="sim">是否切到仿真控制器。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/set_active_controller/")]
public IActionResult SetActiveController([FromQuery] bool sim)
{
try
{
_compatService.SetActiveController(sim);
return Ok(new { status = "active controller set" });
}
catch
{
return LegacyBadRequest("SetActiveController failed");
}
}
/// <summary>
/// 兼容旧 `/connect_robot/` 路由。
/// </summary>
/// <param name="ip">控制器 IP。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/connect_robot/")]
public IActionResult ConnectRobot([FromQuery] string ip)
{
try
{
_compatService.Connect(ip);
return Ok(new { status = "robot connected" });
}
catch
{
return LegacyBadRequest("Connect failed");
}
}
/// <summary>
/// 兼容旧 `/robot_info/` 路由。
/// </summary>
/// <returns>旧 HTTP 层聚合的机器人元信息。</returns>
[HttpGet("/robot_info/")]
public IActionResult GetRobotInfo()
{
try
{
return Ok(new
{
name = _compatService.GetRobotName(),
server_version = _compatService.ServerVersion,
dof = _compatService.GetDegreesOfFreedom(),
speed_ratio = _compatService.GetSpeedRatio()
});
}
catch
{
return LegacyBadRequest("GetRobotInfo failed");
}
}
/// <summary>
/// 兼容旧 `/set_tcp/` 路由。
/// </summary>
/// <param name="tcp_data">三维 TCP 请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/set_tcp/")]
public IActionResult SetTcp([FromBody] LegacyTcpRequest tcp_data)
{
try
{
_compatService.SetTcp(tcp_data.x, tcp_data.y, tcp_data.z);
return Ok(new { status = "TCP set" });
}
catch
{
return LegacyBadRequest("SetTCP failed");
}
}
/// <summary>
/// 兼容旧 `/get_tcp/` 路由。
/// </summary>
/// <returns>当前 TCP 三维坐标。</returns>
[HttpGet("/get_tcp/")]
public IActionResult GetTcp()
{
try
{
return Ok(new { tcp = _compatService.GetTcp() });
}
catch
{
return LegacyBadRequest("GetTCP failed");
}
}
/// <summary>
/// 兼容旧 `/set_io/` 路由。
/// </summary>
/// <param name="port">IO 端口号。</param>
/// <param name="value">IO 值。</param>
/// <param name="io_type">IO 类型字符串。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/set_io/")]
public IActionResult SetIo([FromQuery] int port, [FromQuery] bool value, [FromQuery] string io_type)
{
try
{
_compatService.SetIo(port, value, io_type);
return Ok(new { status = "IO set" });
}
catch
{
return LegacyBadRequest("SetDigitalOutput failed");
}
}
/// <summary>
/// 兼容旧 `/get_joint_position/` 路由。
/// </summary>
/// <returns>旧 HTTP 层定义的关节位置 JSON 外形。</returns>
[HttpGet("/get_joint_position/")]
public IActionResult GetJointPosition()
{
try
{
return Ok(new { success = true, points = _compatService.GetJointPositions() });
}
catch
{
return LegacyBadRequest("GetJointPosition failed");
}
}
/// <summary>
/// 兼容旧 `/move_joint/` 路由。
/// </summary>
/// <param name="joint_data">关节位置请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/move_joint/")]
public IActionResult MoveJoint([FromBody] LegacyJointPositionRequest joint_data)
{
try
{
_compatService.MoveJoint(joint_data.joints);
return Ok(new { status = "robot moved" });
}
catch
{
return LegacyBadRequest("MoveJoint failed");
}
}
/// <summary>
/// 兼容旧 `/list_flyShotTraj/` 路由。
/// </summary>
/// <returns>已上传飞拍轨迹名称列表。</returns>
[HttpGet("/list_flyShotTraj/")]
public IActionResult ListFlyshotTrajectories()
{
var names = _compatService.ListTrajectoryNames();
if (names.Count == 0)
{
return LegacyBadRequest("ListFlyShotTraj failed");
}
return Ok(new { flyshot_trajs = names });
}
/// <summary>
/// 兼容旧 `/execute_trajectory/` 路由,并接受两种历史请求体形状。
/// </summary>
/// <param name="waypoints">轨迹请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/execute_trajectory/")]
public IActionResult ExecuteTrajectory([FromBody] JsonElement waypoints)
{
try
{
_compatService.ExecuteTrajectory(ParseLegacyTrajectoryWaypoints(waypoints));
return Ok(new { status = "trajectory executed" });
}
catch
{
return LegacyBadRequest("ExecuteTrajectory failed");
}
}
/// <summary>
/// 兼容旧 `/upload_flyshot/` 路由。
/// </summary>
/// <param name="trajectory_data">飞拍上传请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/upload_flyshot/")]
public IActionResult UploadFlyshot([FromBody] LegacyFlightTrajectoryRequest trajectory_data)
{
if (trajectory_data.shot_flags.Count != trajectory_data.waypoints.Count)
{
return LegacyValidationError("shot_flags长度必须与路点数量相同");
}
if (trajectory_data.offset_values.Count != trajectory_data.waypoints.Count)
{
return LegacyValidationError("offset_values长度必须与路点数量相同");
}
if (trajectory_data.addrs.Count != trajectory_data.waypoints.Count)
{
return LegacyValidationError("addrs长度必须与路点数量相同");
}
try
{
var trajectory = new ControllerClientCompatUploadedTrajectory(
name: trajectory_data.name,
waypoints: trajectory_data.waypoints,
shotFlags: trajectory_data.shot_flags,
offsetValues: trajectory_data.offset_values.Select(static value => (int)value),
addressGroups: trajectory_data.addrs);
_compatService.UploadTrajectory(trajectory);
return Ok(new { status = "FlyShot uploaded" });
}
catch
{
return LegacyBadRequest("UploadFlyShotTraj failed");
}
}
/// <summary>
/// 兼容旧 `/execute_flyshot/` 路由。
/// </summary>
/// <param name="data">包含轨迹名称的请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/execute_flyshot/")]
public IActionResult ExecuteFlyshot([FromBody] LegacyNameRequest data)
{
try
{
_compatService.ExecuteTrajectoryByName(data.name);
return Ok(new { status = "FlyShot executed", success = true });
}
catch (Exception exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { detail = exception.Message });
}
}
/// <summary>
/// 兼容旧 `/set_speedRatio/` 路由。
/// </summary>
/// <param name="data">速度倍率请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/set_speedRatio/")]
public IActionResult SetSpeedRatio([FromBody] LegacySpeedRatioRequest data)
{
try
{
_compatService.SetSpeedRatio(data.speed);
return Ok(new { message = "set_speedRatio executed", returnCode = 0 });
}
catch
{
return LegacyBadRequest("set_speedRatio failed");
}
}
/// <summary>
/// 兼容旧 `/delete_flyshot/` 路由。
/// </summary>
/// <param name="request">包含轨迹名称的请求体。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/delete_flyshot/")]
public IActionResult DeleteFlyshot([FromBody] LegacyNameRequest request)
{
try
{
_compatService.DeleteTrajectory(request.name);
return Ok(new { status = "FlyShot deleted" });
}
catch
{
return LegacyBadRequest("DeleteFlyShotTraj failed");
}
}
/// <summary>
/// 兼容旧 `/init_mpc_robt` 路由,保留历史拼写。
/// </summary>
/// <param name="data">初始化请求体。</param>
/// <returns>旧 FastAPI 层风格的初始化结果。</returns>
[HttpPost("/init_mpc_robt")]
public IActionResult InitMpcRobot([FromBody] LegacyInitMpcRobotRequest data)
{
try
{
_compatService.ConnectServer(data.server_ip, data.port);
_compatService.SetUpRobot(data.robot_name);
if (!_compatService.IsSetUp)
{
return LegacyBadRequest("Robot not setup");
}
_compatService.SetActiveController(sim: false);
_compatService.Connect(data.robot_ip);
_compatService.EnableRobot(2);
return Ok(new { message = "init_Success", returnCode = 0 });
}
catch
{
return LegacyBadRequest("Connect Server failed");
}
}
/// <summary>
/// 兼容旧 `/get_pose` 路由。
/// </summary>
/// <returns>当前末端位姿数组。</returns>
[HttpGet("/get_pose")]
public IActionResult GetPose()
{
try
{
return Ok(new { pose = _compatService.GetPose() });
}
catch
{
return LegacyBadRequest("GetPose failed");
}
}
/// <summary>
/// 解析旧 `/execute_trajectory/` 可能出现的两种历史请求体形状。
/// </summary>
/// <param name="waypoints">原始 JSON 请求体。</param>
/// <returns>统一后的关节路点集合。</returns>
private static IReadOnlyList<IReadOnlyList<double>> ParseLegacyTrajectoryWaypoints(JsonElement waypoints)
{
if (waypoints.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException("ExecuteTrajectory request body must be an array.");
}
var parsedWaypoints = new List<IReadOnlyList<double>>();
foreach (var waypointElement in waypoints.EnumerateArray())
{
if (waypointElement.ValueKind == JsonValueKind.Array)
{
parsedWaypoints.Add(waypointElement.EnumerateArray().Select(static value => value.GetDouble()).ToArray());
continue;
}
if (waypointElement.ValueKind == JsonValueKind.Object && waypointElement.TryGetProperty("joints", out var jointElement))
{
parsedWaypoints.Add(jointElement.EnumerateArray().Select(static value => value.GetDouble()).ToArray());
continue;
}
throw new InvalidOperationException("Unsupported waypoint payload shape.");
}
return parsedWaypoints;
}
/// <summary>
/// 构造与旧 FastAPI `HTTPException(status_code=400, detail=...)` 等价的响应。
/// </summary>
/// <param name="detail">错误详情文本。</param>
/// <returns>400 JSON 响应。</returns>
private BadRequestObjectResult LegacyBadRequest(string detail)
{
return BadRequest(new { detail });
}
/// <summary>
/// 构造与旧 FastAPI `422` 输入校验失败等价的响应。
/// </summary>
/// <param name="detail">错误详情文本。</param>
/// <returns>422 JSON 响应。</returns>
private ObjectResult LegacyValidationError(string detail)
{
return StatusCode(StatusCodes.Status422UnprocessableEntity, new { detail });
}
}
/// <summary>
/// 表示旧 `/set_tcp/` 路由使用的三维 TCP 请求体。
/// </summary>
public sealed class LegacyTcpRequest
{
/// <summary>
/// 获取或设置 TCP X。
/// </summary>
public double x { get; init; }
/// <summary>
/// 获取或设置 TCP Y。
/// </summary>
public double y { get; init; }
/// <summary>
/// 获取或设置 TCP Z。
/// </summary>
public double z { get; init; }
}
/// <summary>
/// 表示旧 `/move_joint/` 路由使用的关节请求体。
/// </summary>
public sealed class LegacyJointPositionRequest
{
/// <summary>
/// 获取或设置目标关节数组。
/// </summary>
public List<double> joints { get; init; } = [];
}
/// <summary>
/// 表示旧 `/upload_flyshot/` 路由使用的飞拍上传请求体。
/// </summary>
public sealed class LegacyFlightTrajectoryRequest
{
/// <summary>
/// 获取或设置地址组集合。
/// </summary>
public List<List<int>> addrs { get; init; } = [];
/// <summary>
/// 获取或设置飞拍轨迹名称。
/// </summary>
public string name { get; init; } = string.Empty;
/// <summary>
/// 获取或设置偏移周期集合。
/// </summary>
public List<double> offset_values { get; init; } = [];
/// <summary>
/// 获取或设置拍照标志集合。
/// </summary>
public List<bool> shot_flags { get; init; } = [];
/// <summary>
/// 获取或设置关节路点集合。
/// </summary>
public List<List<double>> waypoints { get; init; } = [];
}
/// <summary>
/// 表示旧 `/execute_flyshot/` 与 `/delete_flyshot/` 路由使用的名称请求体。
/// </summary>
public sealed class LegacyNameRequest
{
/// <summary>
/// 获取或设置轨迹名称。
/// </summary>
public string name { get; init; } = string.Empty;
}
/// <summary>
/// 表示旧 `/set_speedRatio/` 路由使用的速度倍率请求体。
/// </summary>
public sealed class LegacySpeedRatioRequest
{
/// <summary>
/// 获取或设置目标速度倍率。
/// </summary>
public double speed { get; init; }
}
/// <summary>
/// 表示旧 `/init_mpc_robt` 路由使用的初始化请求体。
/// </summary>
public sealed class LegacyInitMpcRobotRequest
{
/// <summary>
/// 获取或设置目标服务端 IP。
/// </summary>
public string server_ip { get; init; } = string.Empty;
/// <summary>
/// 获取或设置目标服务端端口。
/// </summary>
public int port { get; init; }
/// <summary>
/// 获取或设置机器人名称。
/// </summary>
public string robot_name { get; init; } = string.Empty;
/// <summary>
/// 获取或设置机器人控制器 IP。
/// </summary>
public string robot_ip { get; init; } = string.Empty;
}