Files
FlyShotHost/tests/Flyshot.Core.Tests/FanucStateClientTests.cs
yunxiao.zhu a78e6761cb feat(fanuc): 添加协议编解码与状态页" -m "* 固化 10010 状态帧、10012 命令帧和 60015 J519 包编解码
* 扩展 ControllerClient 兼容层的执行参数和运行时编排
  * 新增 /status 页面与 /api/status/snapshot 状态快照接口
  * 补充 FANUC 协议、客户端和状态接口的最小验证测试
  * 更新 README、兼容要求和真机 Socket 通信实现计划
2026-04-24 21:26:25 +08:00

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)
{
// 客户端断开。
}
}
}