Phase 06 complete: - Namespace renamed in 62 .cs files (namespace + using directives) - AssemblyName + RootNamespace updated in csproj files - FanucProgramManager.csproj → NcProgramManager.csproj - FanucProgramManager.sln → NcProgramManager.sln - FanucProgramManager.Tests/ → NcProgramManager.Tests/ - FANUCMachine.cs deleted (dead code, zero references) Fanuc-prefixed class names kept (FanucMachine, FanucProgram, IFanucMachine) — manufacturer identifiers, not project names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
212 lines
7.3 KiB
C#
212 lines
7.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
using System.Text;
|
|
using System.Threading;
|
|
|
|
namespace NcProgramManager.Tests.Integration.Stubs
|
|
{
|
|
/// <summary>
|
|
/// Minimal in-process LSV2 server.
|
|
/// Frame format: [0x02][SIZE_HI][SIZE_LO][CMD_HI][CMD_LO][data...][CRC_HI][CRC_LO]
|
|
/// SIZE = len(CMD) + len(data) = 2 + dataLen
|
|
/// </summary>
|
|
public sealed class Lsv2ServerStub : IDisposable
|
|
{
|
|
private const ushort T_OK = 0x0000;
|
|
private const ushort T_SE = 0x0001;
|
|
private const ushort T_LS_SELECT = 0x00A0;
|
|
private const ushort T_LS_LOG_IN = 0x00A1;
|
|
private const ushort T_LS_LOG_OUT = 0x00A2;
|
|
private const ushort T_R_FL = 0x0023;
|
|
private const ushort T_S_FL = 0x0024;
|
|
private const ushort T_FD = 0x0025;
|
|
|
|
private readonly TcpListener _listener;
|
|
private readonly Thread _acceptThread;
|
|
private volatile bool _running;
|
|
|
|
public int Port { get; }
|
|
|
|
/// <summary>In-memory file store: full path → content.</summary>
|
|
public Dictionary<string, string> Files { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
|
|
public Lsv2ServerStub()
|
|
{
|
|
_listener = new TcpListener(IPAddress.Loopback, 0);
|
|
_listener.Start();
|
|
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
|
_running = true;
|
|
_acceptThread = new Thread(AcceptLoop) { IsBackground = true, Name = "Lsv2Stub-Accept" };
|
|
_acceptThread.Start();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_running = false;
|
|
try { _listener.Stop(); } catch { }
|
|
}
|
|
|
|
private void AcceptLoop()
|
|
{
|
|
while (_running)
|
|
{
|
|
TcpClient client;
|
|
try { client = _listener.AcceptTcpClient(); }
|
|
catch { break; }
|
|
var t = new Thread(() => HandleClient(client)) { IsBackground = true, Name = "Lsv2Stub-Client" };
|
|
t.Start();
|
|
}
|
|
}
|
|
|
|
private void HandleClient(TcpClient client)
|
|
{
|
|
using (client)
|
|
using (var stream = client.GetStream())
|
|
{
|
|
while (true)
|
|
{
|
|
ushort cmd;
|
|
byte[] data = ReadFrame(stream, out cmd);
|
|
if (data == null) break;
|
|
|
|
switch (cmd)
|
|
{
|
|
case T_LS_SELECT:
|
|
case T_LS_LOG_IN:
|
|
SendFrame(stream, T_OK, new byte[0]);
|
|
break;
|
|
|
|
case T_LS_LOG_OUT:
|
|
SendFrame(stream, T_OK, new byte[0]);
|
|
return;
|
|
|
|
case T_R_FL:
|
|
{
|
|
// data = path\0
|
|
string path = ExtractString(data, 0);
|
|
string content;
|
|
if (!Files.TryGetValue(path, out content))
|
|
{
|
|
SendFrame(stream, T_SE, new byte[0]);
|
|
break;
|
|
}
|
|
byte[] contentBytes = Encoding.ASCII.GetBytes(content);
|
|
SendFrame(stream, T_FD, contentBytes);
|
|
SendFrame(stream, T_OK, new byte[0]);
|
|
break;
|
|
}
|
|
|
|
case T_S_FL:
|
|
{
|
|
// data = path\0 + content
|
|
string path = ExtractString(data, 0);
|
|
int pathLen = Encoding.ASCII.GetByteCount(path) + 1; // +1 for \0
|
|
int contentLen = data.Length - pathLen;
|
|
string content = contentLen > 0
|
|
? Encoding.ASCII.GetString(data, pathLen, contentLen)
|
|
: "";
|
|
Files[path] = content;
|
|
SendFrame(stream, T_OK, new byte[0]);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
SendFrame(stream, T_SE, new byte[0]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static byte[] ReadFrame(Stream stream, out ushort cmd)
|
|
{
|
|
cmd = 0xFFFF;
|
|
int stx = stream.ReadByte();
|
|
if (stx != 0x02) return null;
|
|
|
|
int sizeHi = stream.ReadByte();
|
|
int sizeLo = stream.ReadByte();
|
|
if (sizeHi < 0 || sizeLo < 0) return null;
|
|
int size = (sizeHi << 8) | sizeLo;
|
|
if (size < 2) return null;
|
|
|
|
int cmdHi = stream.ReadByte();
|
|
int cmdLo = stream.ReadByte();
|
|
if (cmdHi < 0 || cmdLo < 0) return null;
|
|
cmd = (ushort)((cmdHi << 8) | cmdLo);
|
|
|
|
int dataLen = size - 2;
|
|
byte[] data = new byte[dataLen];
|
|
int read = 0;
|
|
while (read < dataLen)
|
|
{
|
|
int n = stream.Read(data, read, dataLen - read);
|
|
if (n == 0) return null;
|
|
read += n;
|
|
}
|
|
|
|
// consume CRC
|
|
stream.ReadByte();
|
|
stream.ReadByte();
|
|
|
|
return data;
|
|
}
|
|
|
|
private static void SendFrame(Stream stream, ushort cmdCode, byte[] data)
|
|
{
|
|
int dataLen = data.Length;
|
|
ushort size = (ushort)(2 + dataLen);
|
|
|
|
byte sizeHi = (byte)(size >> 8);
|
|
byte sizeLo = (byte)(size & 0xFF);
|
|
byte cmdHi = (byte)(cmdCode >> 8);
|
|
byte cmdLo = (byte)(cmdCode & 0xFF);
|
|
|
|
byte[] crcInput = new byte[4 + dataLen];
|
|
crcInput[0] = sizeHi; crcInput[1] = sizeLo;
|
|
crcInput[2] = cmdHi; crcInput[3] = cmdLo;
|
|
Buffer.BlockCopy(data, 0, crcInput, 4, dataLen);
|
|
ushort crc = Crc16(crcInput);
|
|
|
|
byte[] frame = new byte[1 + 4 + dataLen + 2];
|
|
int i = 0;
|
|
frame[i++] = 0x02;
|
|
frame[i++] = sizeHi;
|
|
frame[i++] = sizeLo;
|
|
frame[i++] = cmdHi;
|
|
frame[i++] = cmdLo;
|
|
Buffer.BlockCopy(data, 0, frame, i, dataLen);
|
|
i += dataLen;
|
|
frame[i++] = (byte)(crc >> 8);
|
|
frame[i] = (byte)(crc & 0xFF);
|
|
|
|
stream.Write(frame, 0, frame.Length);
|
|
stream.Flush();
|
|
}
|
|
|
|
private static string ExtractString(byte[] data, int offset)
|
|
{
|
|
int end = offset;
|
|
while (end < data.Length && data[end] != 0) end++;
|
|
return Encoding.ASCII.GetString(data, offset, end - offset);
|
|
}
|
|
|
|
// CRC16 CCITT — same poly/init as Lsv2Client
|
|
private static ushort Crc16(byte[] data)
|
|
{
|
|
ushort crc = 0xFFFF;
|
|
foreach (byte b in data)
|
|
{
|
|
crc ^= (ushort)(b << 8);
|
|
for (int i = 0; i < 8; i++)
|
|
crc = (crc & 0x8000) != 0
|
|
? (ushort)((crc << 1) ^ 0x1021)
|
|
: (ushort)(crc << 1);
|
|
}
|
|
return crc;
|
|
}
|
|
}
|
|
}
|