✨ feat(planning): 添加离线飞拍规划能力

* 固化离线飞拍轨迹加载、保存和规划结果。

* 真机初始化失败时保留模型加载后的离线规划能力。

* 更新状态接口、调试页和回归测试覆盖。
This commit is contained in:
2026-06-24 11:40:07 +08:00
parent 070d2ba5c0
commit 5193f537b0
24 changed files with 1398 additions and 738 deletions

View File

@@ -900,47 +900,6 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.Equal(0, commands[^1].WriteIoValue);
}
/// <summary>
/// 验证仿真模式下即使传入稠密路点,也会回退到单点同步更新。
/// </summary>
[Fact]
public void ExecuteTrajectory_WithDenseWaypoints_SimulationMode_FallsBackToSinglePoint()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: true);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var denseTrajectory = new[]
{
new[] { 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.008, 0.11, 0.21, 0.31, 0.41, 0.51, 0.61 },
new[] { 0.016, 0.12, 0.22, 0.32, 0.42, 0.52, 0.62 }
};
var result = new TrajectoryResult(
programName: "demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(0.016),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4,
denseJointTrajectory: denseTrajectory);
runtime.ExecuteTrajectory(result, [0.12, 0.22, 0.32, 0.42, 0.52, 0.62]);
var snapshot = runtime.GetSnapshot();
Assert.False(snapshot.IsInMotion);
Assert.Equal([0.12, 0.22, 0.32, 0.42, 0.52, 0.62], snapshot.JointPositions);
}
/// <summary>
/// 验证稠密执行前若 J519 首次未就绪,会先尝试一次 EnableRobot,再在 500ms 后复查状态。
/// </summary>
@@ -997,24 +956,6 @@ public sealed class FanucControllerRuntimeDenseTests
Assert.Contains("seq=22", exception.Message, StringComparison.Ordinal);
}
/// <summary>
/// 验证 StopMove 在没有任何后台发送任务运行时不会抛出异常。
/// </summary>
[Fact]
public void StopMove_DoesNotThrowWhenNoSendTaskRunning()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: true);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var exception = Record.Exception(() => runtime.StopMove());
Assert.Null(exception);
Assert.False(runtime.GetSnapshot().IsInMotion);
}
/// <summary>
/// 验证 IO 地址组中的地址号按 writeIoIndex=1 的手册语义映射为 writeIoValue 位掩码。
/// </summary>
@@ -1171,7 +1112,6 @@ public sealed class FanucControllerRuntimeDenseTests
private static void ForceRealModeEnabled(FanucControllerRuntime runtime, double speedRatio)
{
SetPrivateField(runtime, "_activeControllerIsSimulation", false);
SetPrivateField(runtime, "_connectedRobotIp", "127.0.0.1");
SetPrivateField(runtime, "_isEnabled", true);
SetPrivateField(runtime, "_bufferSize", 2);

View File

@@ -16,40 +16,6 @@ namespace Flyshot.Core.Tests;
/// </summary>
public sealed class RuntimeOrchestrationTests
{
/// <summary>
/// 验证 FANUC 最小运行时执行轨迹后会更新状态快照与最终关节位置。
/// </summary>
[Fact]
public void FanucControllerRuntime_ExecuteTrajectory_UpdatesSnapshotAndFinalJointPositions()
{
var runtime = new FanucControllerRuntime();
var robot = TestRobotFactory.CreateRobotProfile();
runtime.ResetRobot(robot, "FANUC_LR_Mate_200iD");
runtime.SetActiveController(sim: true);
runtime.Connect("192.168.10.101");
runtime.EnableRobot(bufferSize: 2);
var result = new TrajectoryResult(
programName: "demo",
method: PlanningMethod.Icsp,
isValid: true,
duration: TimeSpan.FromSeconds(1.2),
shotEvents: Array.Empty<ShotEvent>(),
triggerTimeline: Array.Empty<TrajectoryDoEvent>(),
artifacts: Array.Empty<TrajectoryArtifact>(),
failureReason: null,
usedCache: false,
originalWaypointCount: 4,
plannedWaypointCount: 4);
runtime.ExecuteTrajectory(result, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
var snapshot = runtime.GetSnapshot();
Assert.Equal("Connected", snapshot.ConnectionState);
Assert.False(snapshot.IsInMotion);
Assert.Equal([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], snapshot.JointPositions);
}
/// <summary>
/// 验证真机运行时会把 TCP 10010 状态通道健康度映射为可诊断连接状态。
/// </summary>
@@ -761,7 +727,6 @@ public sealed class RuntimeOrchestrationTests
{
var service = TestRobotFactory.CreateCompatService();
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: true);
service.Connect("192.168.10.101");
service.EnableRobot(2);
@@ -803,7 +768,6 @@ public sealed class RuntimeOrchestrationTests
};
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
runtime.SetSpeedRatio(0.5);
@@ -849,7 +813,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -894,7 +857,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -938,7 +900,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -979,7 +940,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -1030,7 +990,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -1081,7 +1040,6 @@ public sealed class RuntimeOrchestrationTests
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
service.SetActiveController(sim: false);
service.Connect("192.168.10.101");
service.EnableRobot(2);
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
@@ -1240,8 +1198,8 @@ public sealed class RuntimeOrchestrationTests
$"ActualSend 应按当前 speedRatio 的执行视图导出,不能再用旧 JointDetialTraj 规划时长约束点数。actual={pointRows.Length}, planningRows={planningExportRowsAtServoPeriod}");
Assert.Equal(0.0, pointRows[0][0], precision: 6);
Assert.Equal(0.008, pointRows[1][0], precision: 6);
Assert.Equal(0.008, timingRows[1][2], precision: 6);
Assert.Equal(1.0, timingRows[1][3], precision: 6);
Assert.Equal(0.004, timingRows[1][2], precision: 6);
Assert.Equal(0.5, timingRows[1][3], precision: 6);
Assert.Equal(0.5, timingRows[1][4], precision: 6);
Assert.Equal(0.0, timingRows[1][5], precision: 6);
Assert.Contains("\"trigger_window_seconds\": 0.1", shotEventsJson);
@@ -1371,6 +1329,142 @@ public sealed class RuntimeOrchestrationTests
return configRoot;
}
/// <summary>
/// 验证未加载机器人模型时仍可上传、列出和删除飞拍资产。
/// </summary>
[Fact]
public void ControllerClientCompatService_ManagesFlyshotAssetsWithoutRobotSetup()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
Assert.Contains("demo-flyshot", service.ListTrajectoryNames());
Assert.Contains(
"\"demo-flyshot\"",
File.ReadAllText(Path.Combine(configRoot, "RobotConfig.json")),
StringComparison.Ordinal);
service.DeleteTrajectory("demo-flyshot");
Assert.DoesNotContain("demo-flyshot", service.ListTrajectoryNames());
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证重新加载机器人模型不会清空已经上传的飞拍资产。
/// </summary>
[Fact]
public void ControllerClientCompatService_SetUpRobot_PreservesUploadedFlyshotAssets()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.UploadTrajectory(TestRobotFactory.CreateUploadedTrajectoryWithSingleShot());
service.SetUpRobot("FANUC_LR_Mate_200iD");
Assert.Contains("demo-flyshot", service.ListTrajectoryNames());
Assert.Equal("OfflineReady", service.GetStatusSnapshotMetadata().OperatingMode);
Assert.True(service.GetStatusSnapshotMetadata().CanPlanOffline);
Assert.False(service.GetStatusSnapshotMetadata().CanExecuteRealMotion);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证离线状态可以生成完整最终发送点列,并返回导出文件绝对路径。
/// </summary>
[Fact]
public void ControllerClientCompatService_SaveTrajectoryInfo_ReturnsOfflinePlanningResult()
{
var configRoot = CreateTempConfigRoot();
WriteRobotConfigWithDemoTrajectory(configRoot, planningSpeedScale: 0.45, speedRatio: 1.0);
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
service.SetUpRobot("FANUC_LR_Mate_200iD");
var result = service.SaveTrajectoryInfo("demo-flyshot");
Assert.True(result.IsValid);
Assert.Equal("demo-flyshot", result.TrajectoryName);
Assert.Equal(1.0, result.SpeedRatio, precision: 6);
Assert.NotEmpty(result.FinalSendPoints);
Assert.NotEmpty(result.ShotEvents);
Assert.NotEmpty(result.CartesianTrajectory);
Assert.True(result.Diagnostics.MaxAbsVelocityDegreesPerSecond > 0.0);
Assert.All(result.ArtifactPaths.Values, path => Assert.True(Path.IsPathFullyQualified(path)));
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 验证模型加载成功但真机连接失败时,状态降级为 OfflineReady 并保留离线规划能力。
/// </summary>
[Fact]
public void ControllerClientCompatService_InitializeRobot_ConnectionFailureKeepsOfflineReady()
{
var configRoot = CreateTempConfigRoot();
try
{
var options = new ControllerClientCompatOptions { ConfigRoot = configRoot };
var runtime = new RecordingControllerRuntime { ThrowOnConnect = true };
var service = new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
runtime,
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
Assert.Throws<InvalidOperationException>(
() => service.InitializeRobot("FANUC_LR_Mate_200iD", "192.168.10.101"));
var metadata = service.GetStatusSnapshotMetadata();
Assert.Equal("OfflineReady", metadata.OperatingMode);
Assert.True(metadata.CanPlanOffline);
Assert.False(metadata.CanExecuteRealMotion);
Assert.NotNull(metadata.LastConnectionError);
Assert.NotNull(metadata.LastConnectionErrorAt);
}
finally
{
Directory.Delete(configRoot, recursive: true);
}
}
/// <summary>
/// 写入包含一条飞拍轨迹的最小 RobotConfig.json,供兼容服务从统一配置恢复轨迹。
/// </summary>
@@ -1952,7 +2046,7 @@ internal static class TestRobotFactory
return new ControllerClientCompatService(
options,
new ControllerClientCompatRobotCatalog(options, new RobotModelLoader()),
new FanucControllerRuntime(),
new RecordingControllerRuntime(),
new ControllerClientTrajectoryOrchestrator(),
new RobotConfigLoader());
}
@@ -2024,6 +2118,11 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// </summary>
public TrajectoryResult? LastExecutedResult { get; private set; }
/// <summary>
/// 获取或设置连接时是否抛出测试异常。
/// </summary>
public bool ThrowOnConnect { get; set; }
/// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName)
{
@@ -2038,6 +2137,10 @@ internal sealed class RecordingControllerRuntime : IControllerRuntime
/// <inheritdoc />
public void Connect(string robotIp)
{
if (ThrowOnConnect)
{
throw new InvalidOperationException("测试连接失败。");
}
}
/// <inheritdoc />

View File

@@ -36,6 +36,14 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
Assert.Contains("id=\"debug-static-docs\"", html, StringComparison.Ordinal);
Assert.Contains("POST /move_pose/", html, StringComparison.Ordinal);
Assert.Contains("POST /upload_flyshot/", html, StringComparison.Ordinal);
Assert.Contains("POST /init_mpc_robot", html, StringComparison.Ordinal);
Assert.Contains("id=\"offline-planning-panel\"", html, StringComparison.Ordinal);
Assert.Contains("id=\"offline-trajectory-select\"", html, StringComparison.Ordinal);
Assert.Contains("生成轨迹不会向机器人发送命令", html, StringComparison.Ordinal);
Assert.DoesNotContain("/init_mpc_robt", html, StringComparison.Ordinal);
Assert.DoesNotContain("/set_active_controller/", html, StringComparison.Ordinal);
Assert.DoesNotContain("\"sim\"", html, StringComparison.Ordinal);
Assert.DoesNotContain("\"server_ip\"", html, StringComparison.Ordinal);
Assert.DoesNotContain("__SWAGGER_JSON_URL__", html, StringComparison.Ordinal);
Assert.Contains("/assets/debug.css", html, StringComparison.Ordinal);
Assert.Contains("/assets/debug.js", html, StringComparison.Ordinal);
@@ -45,6 +53,9 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
var script = await scriptResponse.Content.ReadAsStringAsync();
Assert.Contains("/api/debug/config", script, StringComparison.Ordinal);
Assert.Contains("/list_flyShotTraj/", script, StringComparison.Ordinal);
Assert.Contains("/save_traj_info/", script, StringComparison.Ordinal);
Assert.Contains("renderOfflinePlanningResult", script, StringComparison.Ordinal);
}
/// <summary>
@@ -157,11 +168,14 @@ public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IC
using var document = await System.Text.Json.JsonDocument.ParseAsync(responseStream);
var paths = document.RootElement.GetProperty("paths");
// 这些路径分别覆盖:基础探活、状态快照、版本查询、上传飞拍轨迹四种典型形态。
// 这些路径分别覆盖:基础探活、状态快照、真机初始化、上传和离线规划。
AssertPathExists(paths, "/healthz");
AssertPathExists(paths, "/api/status/snapshot");
AssertPathExists(paths, "/get_server_version");
AssertPathExists(paths, "/init_mpc_robot");
AssertPathExists(paths, "/upload_flyshot");
AssertPathExists(paths, "/save_traj_info");
Assert.False(paths.TryGetProperty("/init_mpc_robt", out _));
Assert.False(paths.TryGetProperty("/set_active_controller/", out _));
}
/// <summary>

View File

@@ -1,10 +1,147 @@
using System.Net;
using System.Text.Json;
using Flyshot.Core.Domain;
using Flyshot.Runtime.Common;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Flyshot.Server.IntegrationTests;
public sealed class FlyshotServerFactory : WebApplicationFactory<Program>;
/// <summary>
/// 使用可控 fake runtime 启动测试宿主,避免集成测试访问真实 FANUC 控制器。
/// </summary>
public sealed class FlyshotServerFactory : WebApplicationFactory<Program>
{
/// <inheritdoc />
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.RemoveAll<IControllerRuntime>();
services.AddSingleton<IControllerRuntime, IntegrationFakeControllerRuntime>();
});
}
}
/// <summary>
/// 为 HTTP 集成测试提供不访问硬件的控制器运行时。
/// </summary>
internal sealed class IntegrationFakeControllerRuntime : IControllerRuntime
{
private RobotProfile? _robot;
private bool _connected;
private bool _enabled;
private double _speedRatio = 1.0;
private double[] _tcp = [0.0, 0.0, 0.0];
private double[] _joints = Array.Empty<double>();
private double[] _pose = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0];
/// <inheritdoc />
public void ResetRobot(RobotProfile robot, string robotName)
{
_robot = robot;
_connected = false;
_enabled = false;
_joints = new double[robot.DegreesOfFreedom];
}
/// <inheritdoc />
public void Connect(string robotIp)
{
_ = _robot ?? throw new InvalidOperationException("Robot has not been setup.");
_connected = true;
}
/// <inheritdoc />
public void Disconnect()
{
_connected = false;
_enabled = false;
}
/// <inheritdoc />
public void EnableRobot(int bufferSize)
{
if (!_connected)
{
throw new InvalidOperationException("Controller has not been connected.");
}
_enabled = true;
}
/// <inheritdoc />
public void DisableRobot() => _enabled = false;
/// <inheritdoc />
public void StopMove()
{
}
/// <inheritdoc />
public double GetSpeedRatio() => _speedRatio;
/// <inheritdoc />
public void SetSpeedRatio(double ratio) => _speedRatio = ratio;
/// <inheritdoc />
public IReadOnlyList<double> GetTcp() => _tcp.ToArray();
/// <inheritdoc />
public void SetTcp(double x, double y, double z) => _tcp = [x, y, z];
/// <inheritdoc />
public bool GetIo(int port, string ioType) => false;
/// <inheritdoc />
public void SetIo(int port, bool value, string ioType)
{
}
/// <inheritdoc />
public IReadOnlyList<double> GetJointPositions() => _joints.ToArray();
/// <inheritdoc />
public IReadOnlyList<double> GetPose() => _pose.ToArray();
/// <inheritdoc />
public ControllerStateSnapshot GetSnapshot()
{
return new ControllerStateSnapshot(
DateTimeOffset.UtcNow,
_connected ? "Connected" : "Disconnected",
_enabled,
false,
_speedRatio,
_joints,
_pose,
Array.Empty<RuntimeAlarm>());
}
/// <inheritdoc />
public void ExecuteTrajectory(TrajectoryResult result, IReadOnlyList<double> finalJointPositions)
{
if (!_enabled)
{
throw new InvalidOperationException("Robot has not been enabled.");
}
_joints = finalJointPositions.ToArray();
}
/// <inheritdoc />
public void ExecuteCartesianTrajectory(TrajectoryResult result, IReadOnlyList<double> finalPose)
{
if (!_enabled)
{
throw new InvalidOperationException("Robot has not been enabled.");
}
_pose = [.. finalPose, 1.0];
}
}
public sealed class HealthEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{

View File

@@ -11,18 +11,39 @@ namespace Flyshot.Server.IntegrationTests;
public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{
/// <summary>
/// 验证根路径会返回旧 HTTP 服务使用的 Hello World JSON,而不是跳转到健康检查页。
/// 验证计划中明确移除的旧路由不会再出现在宿主中。
/// </summary>
/// <param name="method">请求方法。</param>
/// <param name="path">已移除路由。</param>
[Theory]
[InlineData("GET", "/")]
[InlineData("POST", "/connect_server/?server_ip=127.0.0.1&port=50001")]
[InlineData("GET", "/get_server_version/")]
[InlineData("GET", "/get_client_version/")]
[InlineData("POST", "/setup_robot_from_env/?env_file=test")]
[InlineData("POST", "/set_show_tcp/")]
[InlineData("GET", "/is_setup/")]
[InlineData("POST", "/set_active_controller/?sim=true")]
[InlineData("POST", "/init_mpc_robt")]
public async Task RemovedRoutes_ReturnNotFound(string method, string path)
{
using var client = factory.CreateClient();
using var request = new HttpRequestMessage(new HttpMethod(method), path);
using var response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
/// <summary>
/// 验证已移除的旧根路径返回 404。
/// </summary>
[Fact]
public async Task Root_ReturnsLegacyHelloWorldPayload()
public async Task Root_ReturnsNotFound()
{
using var client = factory.CreateClient();
using var response = await client.GetAsync("/");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
using var json = await ReadJsonAsync(response);
Assert.Equal("Hello World", json.RootElement.GetProperty("message").GetString());
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
/// <summary>
@@ -33,47 +54,7 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
{
using var client = factory.CreateClient();
using (var connectServerResponse = await client.PostAsync("/connect_server/?server_ip=127.0.0.1&port=50001", content: null))
{
Assert.Equal(HttpStatusCode.OK, connectServerResponse.StatusCode);
using var connectServerJson = await ReadJsonAsync(connectServerResponse);
Assert.Equal("connected", connectServerJson.RootElement.GetProperty("status").GetString());
}
using (var setupResponse = await client.PostAsync("/setup_robot/?robot_name=FANUC_LR_Mate_200iD", content: null))
{
Assert.Equal(HttpStatusCode.OK, setupResponse.StatusCode);
using var setupJson = await ReadJsonAsync(setupResponse);
Assert.Equal("robot setup", setupJson.RootElement.GetProperty("status").GetString());
}
using (var isSetupResponse = await client.GetAsync("/is_setup/"))
{
Assert.Equal(HttpStatusCode.OK, isSetupResponse.StatusCode);
using var isSetupJson = await ReadJsonAsync(isSetupResponse);
Assert.True(isSetupJson.RootElement.GetProperty("is_setup").GetBoolean());
}
using (var activeControllerResponse = await client.PostAsync("/set_active_controller/?sim=true", content: null))
{
Assert.Equal(HttpStatusCode.OK, activeControllerResponse.StatusCode);
using var activeControllerJson = await ReadJsonAsync(activeControllerResponse);
Assert.Equal("active controller set", activeControllerJson.RootElement.GetProperty("status").GetString());
}
using (var connectRobotResponse = await client.PostAsync("/connect_robot/?ip=192.168.10.101", content: null))
{
Assert.Equal(HttpStatusCode.OK, connectRobotResponse.StatusCode);
using var connectRobotJson = await ReadJsonAsync(connectRobotResponse);
Assert.Equal("robot connected", connectRobotJson.RootElement.GetProperty("status").GetString());
}
using (var enableRobotResponse = await client.GetAsync("/enable_robot/"))
{
Assert.Equal(HttpStatusCode.OK, enableRobotResponse.StatusCode);
using var enableRobotJson = await ReadJsonAsync(enableRobotResponse);
Assert.True(enableRobotJson.RootElement.GetProperty("enable_robot").GetBoolean());
}
await InitializeRobotAsync(client);
using (var robotInfoResponse = await client.GetAsync("/robot_info/"))
{
@@ -207,6 +188,10 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
{
using var client = factory.CreateClient();
await InitializeRobotAsync(client);
using (var speedResponse = await client.PostAsJsonAsync("/set_speedRatio/", new { speed = 0.5 }))
{
Assert.Equal(HttpStatusCode.OK, speedResponse.StatusCode);
}
var uploadPayload = new
{
@@ -222,10 +207,10 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
shot_flags = new[] { false, true, false, false },
waypoints = new[]
{
new[] { 0.1, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.2, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.3, 0.2, 0.3, 0.4, 0.5, 0.6 },
new[] { 0.4, 0.2, 0.3, 0.4, 0.5, 0.6 }
new[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.001, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.002, 0.0, 0.0, 0.0, 0.0, 0.0 },
new[] { 0.003, 0.0, 0.0, 0.0, 0.0, 0.0 }
}
};
@@ -282,7 +267,8 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
{
Assert.Equal(HttpStatusCode.OK, saveInfoResponse.StatusCode);
using var saveInfoJson = await ReadJsonAsync(saveInfoResponse);
Assert.True(saveInfoJson.RootElement.GetProperty("success").GetBoolean());
Assert.True(saveInfoJson.RootElement.GetProperty("isValid").GetBoolean());
Assert.True(saveInfoJson.RootElement.GetProperty("finalSendPointCount").GetInt32() > 0);
}
using (var deleteResponse = await client.PostAsJsonAsync("/delete_flyshot/", new { name = "demo-http-flyshot" }))
@@ -298,13 +284,10 @@ public sealed class LegacyHttpApiCompatibilityTests(FlyshotServerFactory factory
/// </summary>
private static async Task InitializeRobotAsync(HttpClient client)
{
using var initResponse = await client.PostAsJsonAsync("/init_mpc_robt", new
using var initResponse = await client.PostAsJsonAsync("/init_mpc_robot", new
{
server_ip = "127.0.0.1",
port = 50001,
robot_name = "FANUC_LR_Mate_200iD",
robot_ip = "192.168.10.101",
sim = true
robot_ip = "192.168.10.101"
});
Assert.Equal(HttpStatusCode.OK, initResponse.StatusCode);

View File

@@ -113,13 +113,15 @@ public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFi
Assert.Equal("ok", root.GetProperty("status").GetString());
Assert.True(root.GetProperty("isSetup").GetBoolean());
Assert.Equal("RealReady", root.GetProperty("operatingMode").GetString());
Assert.True(root.GetProperty("canPlanOffline").GetBoolean());
Assert.True(root.GetProperty("canExecuteRealMotion").GetBoolean());
Assert.Equal("FANUC_LR_Mate_200iD", root.GetProperty("robotName").GetString());
Assert.Equal(6, root.GetProperty("degreesOfFreedom").GetInt32());
var uploadedTrajectories = root.GetProperty("uploadedTrajectories")
.EnumerateArray()
.Select(static value => value.GetString())
.ToArray();
Assert.Contains("20251015", uploadedTrajectories);
Assert.Contains("UTTC_MS11", uploadedTrajectories);
Assert.Equal("Connected", snapshot.GetProperty("connectionState").GetString());
Assert.True(snapshot.GetProperty("isEnabled").GetBoolean());
@@ -139,11 +141,6 @@ public sealed class StatusEndpointTests(FlyshotServerFactory factory) : IClassFi
Assert.Equal(HttpStatusCode.OK, setupResponse.StatusCode);
}
using (var activeControllerResponse = await client.PostAsync("/set_active_controller/?sim=true", content: null))
{
Assert.Equal(HttpStatusCode.OK, activeControllerResponse.StatusCode);
}
using (var connectRobotResponse = await client.PostAsync("/connect_robot/?ip=192.168.10.101", content: null))
{
Assert.Equal(HttpStatusCode.OK, connectRobotResponse.StatusCode);