* 新增飞拍轨迹文件存储,支持上传、加载与删除 * 接通 ControllerClientCompat 到运行时的轨迹编排 * 完善 FANUC 命令与 J519 客户端发送链路 * 补充密集轨迹执行、运行时编排和协议客户端测试 * 更新 README 与 AGENTS 中的当前实现状态
201 lines
7.6 KiB
C#
201 lines
7.6 KiB
C#
using System.Net;
|
|
using System.Net.Sockets;
|
|
using Flyshot.Runtime.Fanuc.Protocol;
|
|
|
|
namespace Flyshot.Core.Tests;
|
|
|
|
/// <summary>
|
|
/// 验证 FANUC TCP 10012 命令客户端的帧收发与响应解析。
|
|
/// </summary>
|
|
public sealed class FanucCommandClientTests : IDisposable
|
|
{
|
|
private readonly TcpListener _listener;
|
|
private readonly CancellationTokenSource _cts = new();
|
|
|
|
/// <summary>
|
|
/// 在随机可用端口启动本地模拟控制器。
|
|
/// </summary>
|
|
public FanucCommandClientTests()
|
|
{
|
|
_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 FanucCommandClient();
|
|
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>
|
|
/// 验证 StopProgram 命令帧与抓包样本一致,并能解析成功响应。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task StopProgramAsync_SendsCorrectFrameAndParsesSuccess()
|
|
{
|
|
using var client = new FanucCommandClient();
|
|
var handlerTask = RunSingleResponseControllerAsync(
|
|
Convert.FromHexString("646f7a0000001a0000210300000008525642555354534d7a6f64"),
|
|
Convert.FromHexString("646f7a0000001200002103000000007a6f64"),
|
|
_cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
var response = await client.StopProgramAsync("RVBUSTSM", _cts.Token);
|
|
|
|
Assert.True(response.IsSuccess);
|
|
Assert.Equal(FanucCommandMessageIds.StopProgram, response.MessageId);
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证 ResetRobot 空命令帧能正确发送并解析结果响应。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task ResetRobotAsync_SendsEmptyCommandAndParsesResponse()
|
|
{
|
|
using var client = new FanucCommandClient();
|
|
var expectedFrame = FanucCommandProtocol.PackEmptyCommand(FanucCommandMessageIds.ResetRobot);
|
|
var responseFrame = Convert.FromHexString("646f7a0000001200002100000000007a6f64");
|
|
var handlerTask = RunSingleResponseControllerAsync(expectedFrame, responseFrame, _cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
var response = await client.ResetRobotAsync(_cts.Token);
|
|
|
|
Assert.True(response.IsSuccess);
|
|
Assert.Equal(FanucCommandMessageIds.ResetRobot, response.MessageId);
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证 GetProgramStatus 命令帧能正确发送并解析程序状态响应。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task GetProgramStatusAsync_SendsFrameAndParsesStatusResponse()
|
|
{
|
|
using var client = new FanucCommandClient();
|
|
var expectedFrame = FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.GetProgramStatus, "RVBUSTSM");
|
|
var responseFrame = Convert.FromHexString("646f7a000000160000200300000000000000017a6f64");
|
|
var handlerTask = RunSingleResponseControllerAsync(expectedFrame, responseFrame, _cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
var response = await client.GetProgramStatusAsync("RVBUSTSM", _cts.Token);
|
|
|
|
Assert.True(response.IsSuccess);
|
|
Assert.Equal(FanucCommandMessageIds.GetProgramStatus, response.MessageId);
|
|
Assert.Equal(1u, response.ProgramStatus);
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证 StartProgram 命令帧能正确发送并解析成功响应。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task StartProgramAsync_SendsCorrectFrameAndParsesSuccess()
|
|
{
|
|
using var client = new FanucCommandClient();
|
|
var expectedFrame = FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.StartProgram, "RVBUSTSM");
|
|
var responseFrame = Convert.FromHexString("646f7a0000001200002102000000007a6f64");
|
|
var handlerTask = RunSingleResponseControllerAsync(expectedFrame, responseFrame, _cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
var response = await client.StartProgramAsync("RVBUSTSM", _cts.Token);
|
|
|
|
Assert.True(response.IsSuccess);
|
|
Assert.Equal(FanucCommandMessageIds.StartProgram, response.MessageId);
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证命令响应 result_code 非零时,客户端会抛出可诊断异常而不是让上层误判成功。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task StopProgramAsync_NonZeroResultCode_ThrowsDiagnosticException()
|
|
{
|
|
using var client = new FanucCommandClient();
|
|
var handlerTask = RunSingleResponseControllerAsync(
|
|
FanucCommandProtocol.PackProgramCommand(FanucCommandMessageIds.StopProgram, "RVBUSTSM"),
|
|
Convert.FromHexString("646f7a00000012000021030000002a7a6f64"),
|
|
_cts.Token);
|
|
|
|
await client.ConnectAsync("127.0.0.1", Port, _cts.Token);
|
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => client.StopProgramAsync("RVBUSTSM", _cts.Token));
|
|
|
|
Assert.Contains("0x2103", exception.Message);
|
|
Assert.Contains("42", exception.Message);
|
|
await handlerTask.WaitAsync(TimeSpan.FromSeconds(2), _cts.Token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证在连接前调用命令会抛出 InvalidOperationException。
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task SendProgramCommandAsync_BeforeConnect_Throws()
|
|
{
|
|
using var client = new FanucCommandClient();
|
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
() => client.StopProgramAsync("RVBUSTSM", _cts.Token));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 启动模拟控制器,接收一条请求帧并比对期望内容,然后返回预设响应。
|
|
/// </summary>
|
|
private async Task RunSingleResponseControllerAsync(
|
|
byte[] expectedFrame,
|
|
byte[] responseFrame,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
using var controller = await _listener.AcceptTcpClientAsync(cancellationToken);
|
|
await using var stream = controller.GetStream();
|
|
|
|
var buffer = new byte[expectedFrame.Length];
|
|
await ReadExactAsync(stream, buffer, cancellationToken);
|
|
Assert.Equal(expectedFrame, buffer);
|
|
|
|
await stream.WriteAsync(responseFrame, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从流中精确读取指定长度的字节。
|
|
/// </summary>
|
|
private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken cancellationToken)
|
|
{
|
|
var totalRead = 0;
|
|
while (totalRead < buffer.Length)
|
|
{
|
|
var read = await stream.ReadAsync(buffer.AsMemory(totalRead), cancellationToken);
|
|
if (read == 0)
|
|
{
|
|
throw new IOException("模拟控制器读取到 EOF。");
|
|
}
|
|
|
|
totalRead += read;
|
|
}
|
|
}
|
|
}
|