* 扩展 ControllerClient 兼容层的执行参数和运行时编排 * 新增 /status 页面与 /api/status/snapshot 状态快照接口 * 补充 FANUC 协议、客户端和状态接口的最小验证测试 * 更新 README、兼容要求和真机 Socket 通信实现计划
139 lines
4.4 KiB
C#
139 lines
4.4 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Flyshot.Runtime.Fanuc.Protocol;
|
|
|
|
namespace Flyshot.Core.Tests;
|
|
|
|
/// <summary>
|
|
/// 验证 FANUC TCP 10010 状态通道客户端的后台接收与缓存能力。
|
|
/// </summary>
|
|
public sealed class FanucStateClientTests : IDisposable
|
|
{
|
|
private readonly TcpListener _listener;
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
/// <summary>
|
|
/// 在随机可用端口启动本地模拟控制器。
|
|
/// </summary>
|
|
public FanucStateClientTests()
|
|
{
|
|
_listener = new TcpListener(IPAddress.Loopback, 0);
|
|
_listener.Start();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取分配给本地模拟控制器的端口。
|
|
/// </summary>
|
|
private int Port => ((IPEndPoint)_listener.LocalEndpoint).Port;
|
|
|
|
/// <summary>
|
|
/// 清理模拟控制器和取消源。
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_cts.Cancel();
|
|
_listener.Stop();
|
|
_cts.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证状态客户端可以连接本地模拟控制器。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ConnectAsync_ConnectsToLocalListener()
|
|
{
|
|
using var client = new FanucStateClient();
|
|
var acceptTask = _listener.AcceptTcpClientAsync();
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
|
|
Assert.True(client.IsConnected);
|
|
await acceptTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证后台循环能正确解析抓包样本状态帧。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task GetLatestFrame_ReceivesAndParsesCapturedStateFrame()
|
|
{
|
|
using var client = new FanucStateClient();
|
|
var capturedFrame = Convert.FromHexString(
|
|
"646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64");
|
|
|
|
var handlerTask = RunStreamingControllerAsync(capturedFrame, _cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
|
|
// 给后台循环留出接收和解析的时间。
|
|
await Task.Delay(200, _cts.Token);
|
|
|
|
var latest = client.GetLatestFrame();
|
|
Assert.NotNull(latest);
|
|
Assert.Equal(0u, latest.MessageId);
|
|
Assert.Equal(6, latest.Pose.Count);
|
|
Assert.Equal(9, latest.JointOrExtensionValues.Count);
|
|
Assert.Equal([2u, 0u, 0u, 1u], latest.TailWords);
|
|
|
|
client.Disconnect();
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证状态客户端在连接前调用 GetLatestFrame 返回 null。
|
|
/// </summary>
|
|
[Fact]
|
|
public void GetLatestFrame_BeforeConnect_ReturnsNull()
|
|
{
|
|
using var client = new FanucStateClient();
|
|
Assert.Null(client.GetLatestFrame());
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证 Disconnect 后最新帧被清空。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Disconnect_ClearsLatestFrame()
|
|
{
|
|
using var client = new FanucStateClient();
|
|
var capturedFrame = Convert.FromHexString(
|
|
"646f7a0000005a000000004388a23243f1ed7f43e9de6bc265031ec2b33cc3c278e0153f8742f53c3f128dbc929529bc7861d63cb0184c3c1ca1a7000000000000000000000000000000020000000000000000000000017a6f64");
|
|
|
|
var handlerTask = RunStreamingControllerAsync(capturedFrame, _cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
await Task.Delay(200, _cts.Token);
|
|
Assert.NotNull(client.GetLatestFrame());
|
|
|
|
client.Disconnect();
|
|
Assert.Null(client.GetLatestFrame());
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 启动模拟控制器,持续发送状态帧流。
|
|
/// </summary>
|
|
private async Task RunStreamingControllerAsync(byte[] frames, CancellationToken cancellationToken)
|
|
{
|
|
using var controller = await _listener.AcceptTcpClientAsync(cancellationToken);
|
|
await using var stream = controller.GetStream();
|
|
|
|
try
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
await stream.WriteAsync(frames, cancellationToken);
|
|
await Task.Delay(50, cancellationToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// 正常取消。
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// 客户端断开。
|
|
}
|
|
}
|
|
}
|