680 lines
26 KiB
C#
680 lines
26 KiB
C#
using RobotNet.Script;
|
|
using System.Buffers.Binary;
|
|
using System.Net;
|
|
using System.Net.Sockets;
|
|
|
|
namespace RobotNet.ScriptManager.Connections;
|
|
|
|
public class CcLinkIeBasicClient(IeBasicClientOptions options) : ICcLinkIeBasicClient
|
|
{
|
|
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
|
private UdpClient? _udp;
|
|
private TcpClient? _tcp;
|
|
private NetworkStream? _tcpStream;
|
|
private IPEndPoint? _remoteEp;
|
|
private CancellationTokenSource? _pollCts;
|
|
|
|
private IeBasicStatus _status = new()
|
|
{
|
|
LinkUp = false,
|
|
ConsecutiveErrors = 0,
|
|
LastEndCode = null,
|
|
LastErrorText = null,
|
|
AvgRtt = null,
|
|
MaxRtt = null
|
|
};
|
|
|
|
private volatile bool _connected;
|
|
private long _rttCount;
|
|
private long _rttSumTicks;
|
|
|
|
public bool IsConnected => _connected;
|
|
public int RetryCount { get => options.RetryCount; set => options = options with { RetryCount = value }; }
|
|
|
|
public event EventHandler<PollUpdatedEventArgs>? Polled;
|
|
|
|
// ====== Public API ======
|
|
|
|
public async Task ConnectAsync(CancellationToken ct = default)
|
|
{
|
|
if (options.FrameFormat != SlmpFrameFormat.Format3E)
|
|
throw new NotSupportedException("Triển khai tham chiếu này hiện hỗ trợ 3E binary frame.");
|
|
|
|
if (options.Transport == IeBasicTransport.Udp)
|
|
{
|
|
_udp?.Dispose();
|
|
_udp = new UdpClient();
|
|
_udp.Client.ReceiveTimeout = (int)options.Timeout.TotalMilliseconds;
|
|
_udp.Client.SendTimeout = (int)options.Timeout.TotalMilliseconds;
|
|
|
|
var ip = await ResolveHostAsync(options.Host, ct);
|
|
_remoteEp = new IPEndPoint(ip, options.Port);
|
|
_udp.Connect(_remoteEp);
|
|
}
|
|
else
|
|
{
|
|
_tcp?.Close();
|
|
_tcp = new TcpClient
|
|
{
|
|
NoDelay = true,
|
|
ReceiveTimeout = (int)options.Timeout.TotalMilliseconds,
|
|
SendTimeout = (int)options.Timeout.TotalMilliseconds
|
|
};
|
|
|
|
var ip = await ResolveHostAsync(options.Host, ct);
|
|
await _tcp.ConnectAsync(ip, options.Port, ct);
|
|
_tcpStream = _tcp.GetStream();
|
|
}
|
|
|
|
_connected = true;
|
|
_status = _status with { LinkUp = true, ConsecutiveErrors = 0, LastErrorText = null, LastEndCode = null };
|
|
}
|
|
|
|
public Task DisconnectAsync(CancellationToken ct = default)
|
|
{
|
|
StopPollingInternal();
|
|
|
|
_connected = false;
|
|
|
|
try { _tcpStream?.Dispose(); } catch { }
|
|
try { _tcp?.Close(); } catch { }
|
|
try { _udp?.Dispose(); } catch { }
|
|
|
|
_tcpStream = null;
|
|
_tcp = null;
|
|
_udp = null;
|
|
|
|
_status = _status with { LinkUp = false };
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task<bool[]> ReadBitsAsync(DeviceCode device, int startAddress, int count, CancellationToken ct = default)
|
|
{
|
|
// 3E binary: Command 0x0401, Subcommand 0x0001 (bit units)
|
|
var payload = BuildBatchRead(device, startAddress, count, isBit: true);
|
|
var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
|
|
EnsureOk(resp);
|
|
|
|
// Bit data: packed per bit (LSB-first) theo manual. Ở đây đơn giản dùng từng byte -> từng bit.
|
|
return UnpackBits(resp.Payload.Span, count);
|
|
}
|
|
|
|
public async Task WriteBitsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<bool> values, CancellationToken ct = default)
|
|
{
|
|
// 3E binary: Command 0x1401, Subcommand 0x0001 (bit units)
|
|
var payload = BuildBatchWriteBits(device, startAddress, values.Span);
|
|
var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct);
|
|
EnsureOk(resp);
|
|
}
|
|
|
|
public async Task<ushort[]> ReadWordsAsync(DeviceCode device, int startAddress, int wordCount, CancellationToken ct = default)
|
|
{
|
|
// 3E binary: Command 0x0401, Subcommand 0x0000 (word units)
|
|
var payload = BuildBatchRead(device, startAddress, wordCount, isBit: false);
|
|
var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
|
|
EnsureOk(resp);
|
|
|
|
// Word data: little-endian UInt16
|
|
if (resp.Payload.Length < wordCount * 2)
|
|
throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
|
|
var span = resp.Payload.Span;
|
|
var result = new ushort[wordCount];
|
|
for (int i = 0; i < wordCount; i++)
|
|
result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2));
|
|
return result;
|
|
}
|
|
|
|
public async Task WriteWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<ushort> words, CancellationToken ct = default)
|
|
{
|
|
// 3E binary: Command 0x1401, Subcommand 0x0000 (word units)
|
|
var payload = BuildBatchWriteWords(device, startAddress, words.Span);
|
|
var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct);
|
|
EnsureOk(resp);
|
|
}
|
|
|
|
public async Task<uint[]> ReadDWordsAsync(DeviceCode device, int startAddress, int dwordCount, CancellationToken ct = default)
|
|
{
|
|
if (SupportsDirectDWordRead(device))
|
|
{
|
|
// 3E binary: Command 0x0401, Subcommand 0x0002 (dword units)
|
|
var payload = BuildBatchRead(device, startAddress, dwordCount, isBit: false, isDWord: true);
|
|
var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct);
|
|
EnsureOk(resp);
|
|
|
|
if (resp.Payload.Length < dwordCount * 4)
|
|
throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
|
|
var span = resp.Payload.Span;
|
|
var result = new uint[dwordCount];
|
|
for (int i = 0; i < dwordCount; i++)
|
|
result[i] = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(i * 4, 4));
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
// Đọc dword như đọc 2 word/liên tiếp
|
|
var words = await ReadWordsAsync(device, startAddress, dwordCount * 2, ct);
|
|
var result = new uint[dwordCount];
|
|
|
|
if (options.LittleEndianWordOrder)
|
|
{
|
|
for (int i = 0, j = 0; i < dwordCount; i++, j += 2)
|
|
result[i] = (uint)(words[j] | ((uint)words[j + 1] << 16));
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0, j = 0; i < dwordCount; i++, j += 2)
|
|
result[i] = (uint)(words[j + 1] | ((uint)words[j] << 16));
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public async Task WriteDWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<uint> dwords, CancellationToken ct = default)
|
|
{
|
|
var span = dwords.Span;
|
|
var buf = new ushort[span.Length * 2];
|
|
|
|
if (options.LittleEndianWordOrder)
|
|
{
|
|
for (int i = 0, j = 0; i < span.Length; i++, j += 2)
|
|
{
|
|
buf[j] = (ushort)(span[i] & 0xFFFF);
|
|
buf[j + 1] = (ushort)((span[i] >> 16) & 0xFFFF);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0, j = 0; i < span.Length; i++, j += 2)
|
|
{
|
|
buf[j] = (ushort)((span[i] >> 16) & 0xFFFF);
|
|
buf[j + 1] = (ushort)(span[i] & 0xFFFF);
|
|
}
|
|
}
|
|
await WriteWordsAsync(device, startAddress, buf, ct);
|
|
}
|
|
|
|
public async Task<ushort[]> ReadRandomWordsAsync((DeviceCode Device, int Address)[] points, CancellationToken ct = default)
|
|
{
|
|
// 3E binary: Read Random (0x0403). Payload: điểm (addr+devcode)
|
|
var payload = BuildRandomRead(points);
|
|
var resp = await SendSlmpAsync(SlmpCommand.ReadRandom, payload, ct);
|
|
EnsureOk(resp);
|
|
|
|
// Trả về word liên tiếp
|
|
var span = resp.Payload.Span;
|
|
if (span.Length < points.Length * 2) throw new InvalidOperationException("Payload ngắn hơn mong đợi.");
|
|
var result = new ushort[points.Length];
|
|
for (int i = 0; i < points.Length; i++)
|
|
result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2));
|
|
return result;
|
|
}
|
|
|
|
public async Task WriteRandomWordsAsync((DeviceCode Device, int Address)[] points, ReadOnlyMemory<ushort> values, CancellationToken ct = default)
|
|
{
|
|
if (points.Length != values.Length)
|
|
throw new ArgumentException("Số điểm và số giá trị không khớp.");
|
|
|
|
var payload = BuildRandomWrite(points, values.Span);
|
|
var resp = await SendSlmpAsync(SlmpCommand.WriteRandom, payload, ct);
|
|
EnsureOk(resp);
|
|
}
|
|
|
|
public async Task<IeBasicStatus> GetStatusAsync(CancellationToken ct = default)
|
|
{
|
|
// Có thể gửi NodeMonitor để “làm nóng” trạng thái; ở đây trả về snapshot hiện có
|
|
await Task.Yield();
|
|
return _status;
|
|
}
|
|
|
|
public async Task<bool> PingAsync(CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
// Node monitor: dùng CMD DeviceInfo/NodeMonitor rỗng để kiểm phản hồi
|
|
var resp = await SendSlmpAsync(SlmpCommand.NodeMonitor, ReadOnlyMemory<byte>.Empty, ct);
|
|
return resp.EndCode == SlmpEndCode.Completed;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task<ModuleIdentity> IdentifyAsync(CancellationToken ct = default)
|
|
{
|
|
// Tối giản: gửi DeviceInfo và parse sơ bộ (tuỳ PLC mà payload khác nhau)
|
|
var resp = await SendSlmpAsync(SlmpCommand.DeviceInfo, ReadOnlyMemory<byte>.Empty, ct);
|
|
// Ở đây không chuẩn hoá do payload phụ thuộc model; trả về khung rỗng để người dùng tự giải.
|
|
return new ModuleIdentity
|
|
{
|
|
Vendor = "Mitsubishi Electric",
|
|
AdditionalInfo = $"Raw bytes: {resp.Payload.Length} (xem manual model để parse)"
|
|
};
|
|
}
|
|
|
|
public async Task<SlmpResponse> SendSlmpAsync(SlmpCommand command, ReadOnlyMemory<byte> payload, CancellationToken ct = default)
|
|
{
|
|
if (!_connected) throw new InvalidOperationException("Chưa kết nối.");
|
|
|
|
// Dựng 3E frame
|
|
var req = Build3EFrame(options, (ushort)command, subcommand: CurrentSubcommandHint, payload);
|
|
|
|
// Gửi/nhận với retry
|
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
for (int attempt = 0; attempt <= RetryCount; attempt++)
|
|
{
|
|
try
|
|
{
|
|
var rawResp = await SendAndReceiveAsync(req, ct);
|
|
var resp = Parse3EResponse(rawResp);
|
|
|
|
sw.Stop();
|
|
TrackRtt(sw.Elapsed);
|
|
|
|
_status = _status with
|
|
{
|
|
LinkUp = true,
|
|
ConsecutiveErrors = 0,
|
|
LastEndCode = resp.EndCode,
|
|
LastErrorText = null,
|
|
AvgRtt = TimeSpan.FromTicks((_rttCount > 0) ? _rttSumTicks / _rttCount : 0),
|
|
MaxRtt = (_status.MaxRtt == null || sw.Elapsed > _status.MaxRtt) ? sw.Elapsed : _status.MaxRtt
|
|
};
|
|
return resp;
|
|
}
|
|
catch (Exception ex) when (attempt < RetryCount)
|
|
{
|
|
_status = _status with
|
|
{
|
|
LinkUp = false,
|
|
ConsecutiveErrors = _status.ConsecutiveErrors + 1,
|
|
LastErrorText = ex.Message,
|
|
LastEndCode = SlmpEndCode.TransportError
|
|
};
|
|
// thử lại
|
|
await Task.Delay(10, ct);
|
|
}
|
|
}
|
|
|
|
sw.Stop();
|
|
throw new TimeoutException("SLMP: Hết retry mà vẫn lỗi.");
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await DisconnectAsync();
|
|
_sendLock.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
DisposeAsync().AsTask().GetAwaiter().GetResult();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
public void StartPolling(PollOptions options, CancellationToken ct = default)
|
|
{
|
|
StopPollingInternal();
|
|
_pollCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
var token = _pollCts.Token;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
// Strategy: gom vùng bit và word/dword riêng để tối ưu
|
|
while (!token.IsCancellationRequested && _connected)
|
|
{
|
|
try
|
|
{
|
|
// Đơn giản: chỉ đọc word; nếu có bit thì đọc riêng
|
|
var wordsAreas = options.Areas;
|
|
ushort[]? words = null;
|
|
bool[]? bits = null;
|
|
|
|
// (Minh hoạ) Đọc tất cả vùng word liên tiếp bằng cách gộp, bạn có thể tối ưu thêm theo thiết bị.
|
|
if (wordsAreas.Length > 0)
|
|
{
|
|
// Đây đọc lần lượt từng vùng cho đơn giản (dễ hiểu; bạn có thể hợp nhất để giảm gói)
|
|
var aggWords = new System.Collections.Generic.List<ushort>();
|
|
foreach (var (Device, Start, Count) in wordsAreas)
|
|
{
|
|
if (Device == DeviceCode.X || Device == DeviceCode.Y || Device == DeviceCode.M ||
|
|
Device == DeviceCode.L || Device == DeviceCode.S || Device == DeviceCode.B)
|
|
{
|
|
// Bit area
|
|
var b = await ReadBitsAsync(Device, Start, Count, token);
|
|
// Nối bit (minh hoạ): không ghép; đẩy riêng
|
|
bits = b;
|
|
}
|
|
else
|
|
{
|
|
var w = await ReadWordsAsync(Device, Start, Count, token);
|
|
aggWords.AddRange(w);
|
|
}
|
|
}
|
|
if (aggWords.Count > 0) words = [.. aggWords];
|
|
}
|
|
|
|
Polled?.Invoke(this, new PollUpdatedEventArgs
|
|
{
|
|
Timestamp = DateTimeOffset.UtcNow,
|
|
Bits = bits,
|
|
Words = words,
|
|
DWords = null
|
|
});
|
|
|
|
await Task.Delay(options.Interval, token);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch
|
|
{
|
|
// bỏ qua vòng lỗi và tiếp tục (tuỳ nhu cầu: bạn có thể tăng backoff)
|
|
await Task.Delay(TimeSpan.FromMilliseconds(50), token);
|
|
}
|
|
}
|
|
}, token);
|
|
}
|
|
|
|
public Task StopPollingAsync(CancellationToken ct = default)
|
|
{
|
|
StopPollingInternal();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ====== Internal helpers ======
|
|
|
|
private void StopPollingInternal()
|
|
{
|
|
try { _pollCts?.Cancel(); } catch { }
|
|
try { _pollCts?.Dispose(); } catch { }
|
|
_pollCts = null;
|
|
}
|
|
|
|
private static async Task<IPAddress> ResolveHostAsync(string host, CancellationToken ct)
|
|
{
|
|
if (IPAddress.TryParse(host, out var ip)) return ip;
|
|
var entry = await Dns.GetHostEntryAsync(host, ct);
|
|
foreach (var addr in entry.AddressList)
|
|
if (addr.AddressFamily == AddressFamily.InterNetwork) return addr;
|
|
throw new InvalidOperationException("Không tìm thấy IPv4 cho host.");
|
|
}
|
|
|
|
private void TrackRtt(TimeSpan elapsed)
|
|
{
|
|
var ticks = elapsed.Ticks;
|
|
System.Threading.Interlocked.Add(ref _rttSumTicks, ticks);
|
|
System.Threading.Interlocked.Increment(ref _rttCount);
|
|
}
|
|
|
|
private static void EnsureOk(SlmpResponse resp)
|
|
{
|
|
if (resp.EndCode != SlmpEndCode.Completed)
|
|
throw new InvalidOperationException($"SLMP EndCode: 0x{(ushort)resp.EndCode:X4}");
|
|
}
|
|
|
|
// ====== SLMP 3E Binary Build/Parse ======
|
|
|
|
// Gợi ý subcommand hiện hành (đặt trước khi Build3EFrame): 0x0000=word, 0x0001=bit
|
|
private ushort CurrentSubcommandHint = 0x0000;
|
|
|
|
private static byte[] Build3EFrame(IeBasicClientOptions opt, ushort command, ushort subcommand, ReadOnlyMemory<byte> userData)
|
|
{
|
|
// Tài liệu 3E binary (TCP): Subheader 0x5000; (UDP): 0x5400
|
|
// Header:
|
|
// [0-1] Subheader (LE)
|
|
// [2] Network No
|
|
// [3] PC No (để 0x00)
|
|
// [4-5] Module I/O No (LE)
|
|
// [6] Multidrop No
|
|
// [7-8] Data Length (LE) = 2(timer) + 2(cmd) + 2(subcmd) + payload.Length
|
|
// [9-10] Timer (LE) đơn vị 250 ms hoặc 1? (tuỳ manual; thường 0x0010 ~ 4s). Ở đây lấy theo Timeout ~ ms/10.
|
|
// [11-12] Command (LE)
|
|
// [13-14] Subcommand (LE)
|
|
// [15..] Payload
|
|
|
|
var subheader = (opt.Transport == IeBasicTransport.Udp) ? (ushort)0x5400 : (ushort)0x5000;
|
|
var dataLen = (ushort)(2 + 2 + 2 + userData.Length); // timer + cmd + subcmd + payload
|
|
|
|
// Timer: convert từ Timeout ~ ms -> giá trị phù hợp. Ở đây minh hoạ: ms/10 (tuỳ manual).
|
|
var timer = (ushort)Math.Clamp((int)(opt.Timeout.TotalMilliseconds / 10.0), 1, 0xFFFE);
|
|
|
|
var buf = new byte[15 + userData.Length];
|
|
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), subheader);
|
|
buf[2] = opt.NetworkNo;
|
|
buf[3] = 0x00; // PC No
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), opt.ModuleIoNo);
|
|
buf[6] = opt.MultidropNo;
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(7, 2), dataLen);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(9, 2), timer);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(11, 2), command);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(13, 2), subcommand);
|
|
userData.Span.CopyTo(buf.AsSpan(15));
|
|
|
|
return buf;
|
|
}
|
|
|
|
private async Task<byte[]> SendAndReceiveAsync(byte[] request, CancellationToken ct)
|
|
{
|
|
await _sendLock.WaitAsync(ct).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
if (options.Transport == IeBasicTransport.Udp)
|
|
{
|
|
if (_udp is null) throw new InvalidOperationException("UDP client null.");
|
|
await _udp.SendAsync(request, request.Length);
|
|
var recv = await _udp.ReceiveAsync(ct);
|
|
return recv.Buffer;
|
|
}
|
|
else
|
|
{
|
|
if (_tcpStream is null) throw new InvalidOperationException("TCP stream null.");
|
|
|
|
// Gửi request
|
|
await _tcpStream.WriteAsync(request, ct);
|
|
await _tcpStream.FlushAsync(ct);
|
|
|
|
// Đọc header tối thiểu 15 bytes để biết data length
|
|
var header = new byte[15];
|
|
await ReadExactAsync(_tcpStream, header, ct);
|
|
|
|
// Lấy data length
|
|
var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(7, 2));
|
|
var rest = new byte[dataLen]; // bao gồm timer/cmd/subcmd/endcode/payload (ở response thay timer bằng EndCode)
|
|
await ReadExactAsync(_tcpStream, rest, ct);
|
|
|
|
// Gộp lại thành frame đầy đủ cho parser
|
|
var combined = new byte[header.Length + rest.Length];
|
|
Buffer.BlockCopy(header, 0, combined, 0, header.Length);
|
|
Buffer.BlockCopy(rest, 0, combined, header.Length, rest.Length);
|
|
return combined;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_sendLock.Release();
|
|
}
|
|
}
|
|
|
|
private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken ct)
|
|
{
|
|
int read = 0;
|
|
while (read < buffer.Length)
|
|
{
|
|
var n = await stream.ReadAsync(buffer.AsMemory(read, buffer.Length - read), ct);
|
|
if (n == 0) throw new SocketException((int)SocketError.ConnectionReset);
|
|
read += n;
|
|
}
|
|
}
|
|
|
|
private static SlmpResponse Parse3EResponse(byte[] frame)
|
|
{
|
|
// Response 3E:
|
|
// [0-1] Subheader
|
|
// [2] NetworkNo
|
|
// [3] PCNo
|
|
// [4-5] ModuleIoNo
|
|
// [6] MultidropNo
|
|
// [7-8] Data Length (LE)
|
|
// [9-10] End Code (LE) <-- khác request (timer)
|
|
// [11..] Payload
|
|
if (frame.Length < 11)
|
|
throw new InvalidOperationException("Frame quá ngắn.");
|
|
|
|
var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(7, 2));
|
|
if (frame.Length < 11 + dataLen)
|
|
throw new InvalidOperationException("Frame không đủ chiều dài.");
|
|
|
|
var endCode = (SlmpEndCode)BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(9, 2));
|
|
var payload = new ReadOnlyMemory<byte>(frame, 11, dataLen - 2); // trừ endcode (2B)
|
|
return new SlmpResponse { EndCode = endCode, Payload = payload, Timestamp = DateTimeOffset.UtcNow };
|
|
}
|
|
|
|
// ====== Payload builders (3E binary) ======
|
|
|
|
private static byte DevCodeBinary(DeviceCode device) => device switch
|
|
{
|
|
// Mã phổ biến trong MC 3E binary (tham chiếu; kiểm tra manual cho chắc theo CPU):
|
|
DeviceCode.X => 0x9C,
|
|
DeviceCode.Y => 0x9D,
|
|
DeviceCode.M => 0x90,
|
|
DeviceCode.L => 0x92,
|
|
DeviceCode.B => 0xA0,
|
|
DeviceCode.S => 0x98,
|
|
DeviceCode.D => 0xA8,
|
|
DeviceCode.W => 0xB4,
|
|
DeviceCode.R => 0xAF,
|
|
DeviceCode.ZR => 0xB0,
|
|
_ => 0x00
|
|
};
|
|
|
|
private ReadOnlyMemory<byte> BuildBatchWriteWords(DeviceCode device, int start, ReadOnlySpan<ushort> words)
|
|
{
|
|
CurrentSubcommandHint = 0x0000; // word units
|
|
|
|
var buf = new byte[3 + 1 + 2 + words.Length * 2];
|
|
buf[0] = (byte)(start & 0xFF);
|
|
buf[1] = (byte)((start >> 8) & 0xFF);
|
|
buf[2] = (byte)((start >> 16) & 0xFF);
|
|
buf[3] = DevCodeBinary(device);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)words.Length);
|
|
|
|
var off = 6;
|
|
for (int i = 0; i < words.Length; i++, off += 2)
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off, 2), words[i]);
|
|
|
|
return buf;
|
|
}
|
|
|
|
private ReadOnlyMemory<byte> BuildBatchWriteBits(DeviceCode device, int start, ReadOnlySpan<bool> bits)
|
|
{
|
|
CurrentSubcommandHint = 0x0001; // bit units
|
|
|
|
// Bit packing theo MC (1 bit/byte hoặc 2 bit/byte tuỳ subcmd). Ở đây dùng 1 bit/bit (1 byte/bit) để đơn giản;
|
|
// nhiều PLC chấp nhận. Nếu cần tối ưu, gói bit theo nibble.
|
|
var buf = new byte[3 + 1 + 2 + bits.Length];
|
|
buf[0] = (byte)(start & 0xFF);
|
|
buf[1] = (byte)((start >> 8) & 0xFF);
|
|
buf[2] = (byte)((start >> 16) & 0xFF);
|
|
buf[3] = DevCodeBinary(device);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)bits.Length);
|
|
|
|
for (int i = 0; i < bits.Length; i++)
|
|
buf[6 + i] = (byte)(bits[i] ? 0x01 : 0x00);
|
|
|
|
return buf;
|
|
}
|
|
|
|
private static ReadOnlyMemory<byte> BuildRandomRead((DeviceCode Device, int Address)[] points)
|
|
{
|
|
// Format tối giản: [count(2)] + N * (addr(3) + dev(1))
|
|
var buf = new byte[2 + points.Length * 4];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length);
|
|
var off = 2;
|
|
foreach (var (Device, Address) in points)
|
|
{
|
|
buf[off + 0] = (byte)(Address & 0xFF);
|
|
buf[off + 1] = (byte)((Address >> 8) & 0xFF);
|
|
buf[off + 2] = (byte)((Address >> 16) & 0xFF);
|
|
buf[off + 3] = DevCodeBinary(Device);
|
|
off += 4;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
private static ReadOnlyMemory<byte> BuildRandomWrite((DeviceCode Device, int Address)[] points, ReadOnlySpan<ushort> values)
|
|
{
|
|
// [count(2)] + N*(addr(3)+dev(1)+value(2))
|
|
var buf = new byte[2 + points.Length * (4 + 2)];
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length);
|
|
var off = 2;
|
|
for (int i = 0; i < points.Length; i++)
|
|
{
|
|
var (Device, Address) = points[i];
|
|
buf[off + 0] = (byte)(Address & 0xFF);
|
|
buf[off + 1] = (byte)((Address >> 8) & 0xFF);
|
|
buf[off + 2] = (byte)((Address >> 16) & 0xFF);
|
|
buf[off + 3] = DevCodeBinary(Device);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off + 4, 2), values[i]);
|
|
off += 6;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
private static bool[] UnpackBits(ReadOnlySpan<byte> data, int expectedCount)
|
|
{
|
|
// Đơn giản hoá: nếu payload trả về mỗi bit = 1 byte (0x00/0x01)
|
|
// Nếu PLC trả về packed bits, bạn cần giải theo bit-level. Ở đây xử lý cả hai:
|
|
if (data.Length == expectedCount)
|
|
{
|
|
var res = new bool[expectedCount];
|
|
for (int i = 0; i < expectedCount; i++) res[i] = data[i] != 0;
|
|
return res;
|
|
}
|
|
else
|
|
{
|
|
// Giải packed: LSB-first trong từng byte
|
|
var res = new bool[expectedCount];
|
|
int idx = 0;
|
|
for (int b = 0; b < data.Length && idx < expectedCount; b++)
|
|
{
|
|
byte val = data[b];
|
|
for (int bit = 0; bit < 8 && idx < expectedCount; bit++, idx++)
|
|
res[idx] = ((val >> bit) & 0x01) != 0;
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
|
|
private ReadOnlyMemory<byte> BuildBatchRead(DeviceCode device, int start, int count, bool isBit, bool isDWord = false)
|
|
{
|
|
// Request data:
|
|
// [0-2] Address (3 bytes, little-endian 24-bit)
|
|
// [3] Device Code (1 byte)
|
|
// [4-5] Points/Count (LE, word/bit/dword tuỳ subcmd)
|
|
// [6-7] Reserved? (tuỳ lệnh) - KHÔNG cần cho 0x0401
|
|
// Ở đây dùng định dạng tối giản: addr(3) + dev(1) + count(2) + subcmd đã set ở header
|
|
|
|
if (isDWord)
|
|
CurrentSubcommandHint = 0x0002; // dword units
|
|
else
|
|
CurrentSubcommandHint = isBit ? (ushort)0x0001 : (ushort)0x0000;
|
|
|
|
var buf = new byte[3 + 1 + 2];
|
|
// 24-bit little-endian
|
|
buf[0] = (byte)(start & 0xFF);
|
|
buf[1] = (byte)((start >> 8) & 0xFF);
|
|
buf[2] = (byte)((start >> 16) & 0xFF);
|
|
buf[3] = DevCodeBinary(device);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)count);
|
|
return buf;
|
|
}
|
|
|
|
// 5. Hàm kiểm tra device có hỗ trợ đọc dword trực tiếp không (tuỳ CPU, ví dụ D, W, R, ZR thường hỗ trợ)
|
|
private static bool SupportsDirectDWordRead(DeviceCode device)
|
|
{
|
|
return device == DeviceCode.D || device == DeviceCode.W || device == DeviceCode.R || device == DeviceCode.ZR;
|
|
}
|
|
}
|