416 lines
15 KiB
C#
416 lines
15 KiB
C#
namespace RobotNet.Script;
|
||
|
||
/// <summary>
|
||
/// Client CC‑Link IE Basic (SLMP over TCP/UDP) dùng để kết nối PC ↔ PLC Mitsubishi qua Ethernet thường,
|
||
/// hỗ trợ đọc/ghi device, polling chu kỳ và gửi lệnh SLMP thô.
|
||
/// </summary>
|
||
public interface ICcLinkIeBasicClient : IAsyncDisposable, IDisposable
|
||
{
|
||
/// <summary>
|
||
/// Trạng thái đã kết nối phiên transport (TCP/UDP) tới PLC hay chưa.
|
||
/// </summary>
|
||
bool IsConnected { get; }
|
||
|
||
/// <summary>
|
||
/// Số lần thử gửi lại khi lỗi tạm thời/timeout.
|
||
/// </summary>
|
||
int RetryCount { get; set; }
|
||
|
||
/// <summary>
|
||
/// Ngắt kết nối transport và giải phóng tài nguyên liên quan.
|
||
/// </summary>
|
||
/// <param name="ct">Token hủy bất đồng bộ.</param>
|
||
Task DisconnectAsync(CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Đọc mảng bit (X/Y/M/L/S/B...) từ PLC.
|
||
/// </summary>
|
||
/// <param name="device">Loại device code.</param>
|
||
/// <param name="startAddress">Địa chỉ bắt đầu.</param>
|
||
/// <param name="count">Số bit cần đọc.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Mảng giá trị bit.</returns>
|
||
Task<bool[]> ReadBitsAsync(DeviceCode device, int startAddress, int count, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Ghi mảng bit (X/Y/M/L/S/B...) vào PLC.
|
||
/// </summary>
|
||
/// <param name="device">Loại device code.</param>
|
||
/// <param name="startAddress">Địa chỉ bắt đầu.</param>
|
||
/// <param name="values">Dãy giá trị bit cần ghi.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
Task WriteBitsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<bool> values, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Đọc mảng word (D/W/R/ZR/B...) từ PLC.
|
||
/// </summary>
|
||
/// <param name="device">Loại device code.</param>
|
||
/// <param name="startAddress">Địa chỉ bắt đầu (đơn vị word).</param>
|
||
/// <param name="wordCount">Số word cần đọc.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Mảng word (UInt16).</returns>
|
||
Task<ushort[]> ReadWordsAsync(DeviceCode device, int startAddress, int wordCount, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Ghi mảng word (D/W/R/ZR/B...) vào PLC.
|
||
/// </summary>
|
||
/// <param name="device">Loại device code.</param>
|
||
/// <param name="startAddress">Địa chỉ bắt đầu (đơn vị word).</param>
|
||
/// <param name="words">Dãy word cần ghi.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
Task WriteWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<ushort> words, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Đọc mảng double‑word (UInt32) từ PLC (áp dụng cho vùng hỗ trợ dword).
|
||
/// </summary>
|
||
/// <param name="device">Loại device code.</param>
|
||
/// <param name="startAddress">Địa chỉ bắt đầu (đơn vị dword).</param>
|
||
/// <param name="dwordCount">Số dword cần đọc.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Mảng dword (UInt32).</returns>
|
||
Task<uint[]> ReadDWordsAsync(DeviceCode device, int startAddress, int dwordCount, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Ghi mảng double‑word (UInt32) vào PLC (áp dụng cho vùng hỗ trợ dword).
|
||
/// </summary>
|
||
/// <param name="device">Loại device code.</param>
|
||
/// <param name="startAddress">Địa chỉ bắt đầu (đơn vị dword).</param>
|
||
/// <param name="dwords">Dãy dword cần ghi.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
Task WriteDWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory<uint> dwords, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Đọc ngẫu nhiên nhiều điểm word rời rạc (mixed areas) trong một lần.
|
||
/// </summary>
|
||
/// <param name="points">Danh sách (device, address) cần đọc.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Mảng word theo thứ tự điểm yêu cầu.</returns>
|
||
Task<ushort[]> ReadRandomWordsAsync((DeviceCode Device, int Address)[] points, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Ghi ngẫu nhiên nhiều điểm word rời rạc (mixed areas) trong một lần.
|
||
/// </summary>
|
||
/// <param name="points">Danh sách (device, address) cần ghi.</param>
|
||
/// <param name="values">Giá trị word theo thứ tự điểm.</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
Task WriteRandomWordsAsync((DeviceCode Device, int Address)[] points, ReadOnlyMemory<ushort> values, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Sự kiện bắn ra sau mỗi chu kỳ polling hoàn tất.
|
||
/// </summary>
|
||
event EventHandler<PollUpdatedEventArgs>? Polled;
|
||
|
||
/// <summary>
|
||
/// Bắt đầu polling chu kỳ một hoặc nhiều vùng device.
|
||
/// </summary>
|
||
/// <param name="options">Tùy chọn polling (chu kỳ, vùng, giới hạn kích thước).</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
void StartPolling(PollOptions options, CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Dừng polling chu kỳ.
|
||
/// </summary>
|
||
/// <param name="ct">Token hủy.</param>
|
||
Task StopPollingAsync(CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Lấy trạng thái liên kết/SLMP gần nhất (end code, thống kê thời gian phản hồi...).
|
||
/// </summary>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Thông tin trạng thái IE Basic.</returns>
|
||
Task<IeBasicStatus> GetStatusAsync(CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Kiểm tra liên lạc PLC (ví dụ node monitor) ở mức nhanh/gọn.
|
||
/// </summary>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>true nếu phản hồi hợp lệ, ngược lại false.</returns>
|
||
Task<bool> PingAsync(CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Truy vấn thông tin nhận dạng module/CPU (nếu PLC cho phép).
|
||
/// </summary>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Thông tin định danh CPU/module.</returns>
|
||
Task<ModuleIdentity> IdentifyAsync(CancellationToken ct = default);
|
||
|
||
/// <summary>
|
||
/// Gửi lệnh SLMP thô (mở rộng/đặc thù) và nhận phản hồi.
|
||
/// </summary>
|
||
/// <param name="command">Mã lệnh SLMP.</param>
|
||
/// <param name="payload">Payload SLMP theo định dạng frame chọn (3E/4E).</param>
|
||
/// <param name="ct">Token hủy.</param>
|
||
/// <returns>Phản hồi SLMP gồm end code và payload.</returns>
|
||
Task<SlmpResponse> SendSlmpAsync(SlmpCommand command, ReadOnlyMemory<byte> payload, CancellationToken ct = default);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Loại transport cho CC‑Link IE Basic (UDP hoặc TCP).
|
||
/// </summary>
|
||
public enum IeBasicTransport
|
||
{
|
||
/// <summary>Giao tiếp qua UDP (thường dùng cho IE Basic).</summary>
|
||
Udp,
|
||
|
||
/// <summary>Giao tiếp qua TCP.</summary>
|
||
Tcp
|
||
}
|
||
|
||
/// <summary>
|
||
/// Định dạng frame SLMP.
|
||
/// </summary>
|
||
public enum SlmpFrameFormat
|
||
{
|
||
/// <summary>Khung 3E (Format 3E).</summary>
|
||
Format3E,
|
||
|
||
/// <summary>Khung 4E (Format 4E).</summary>
|
||
Format4E
|
||
}
|
||
|
||
/// <summary>
|
||
/// Device code (vùng thiết bị) cấp ứng dụng.
|
||
/// </summary>
|
||
public enum DeviceCode
|
||
{
|
||
/// <summary>Input (bit).</summary>
|
||
X,
|
||
/// <summary>Output (bit).</summary>
|
||
Y,
|
||
/// <summary>Internal relay (bit).</summary>
|
||
M,
|
||
/// <summary>Latch relay (bit).</summary>
|
||
L,
|
||
/// <summary>Link relay (bit/word tùy CPU).</summary>
|
||
B,
|
||
/// <summary>Step relay (bit).</summary>
|
||
S,
|
||
/// <summary>Data register (word).</summary>
|
||
D,
|
||
/// <summary>Link register (word).</summary>
|
||
W,
|
||
/// <summary>File register (word).</summary>
|
||
R,
|
||
/// <summary>Extended file register (word).</summary>
|
||
ZR
|
||
}
|
||
|
||
/// <summary>
|
||
/// Tham số logic phía local (PC/sender) khi gửi SLMP.
|
||
/// </summary>
|
||
/// <param name="NetworkNo">Số mạng logic.</param>
|
||
/// <param name="ModuleIoNo">Địa chỉ I/O module logic (thường 0x03FF).</param>
|
||
/// <param name="MultidropNo">Số đa điểm (nếu dùng).</param>
|
||
/// <param name="StationNo">Số trạm logic.</param>
|
||
public readonly record struct IeBasicLocal(
|
||
byte NetworkNo,
|
||
ushort ModuleIoNo,
|
||
byte MultidropNo,
|
||
byte StationNo
|
||
);
|
||
|
||
/// <summary>
|
||
/// Cấu hình đầu xa: địa chỉ IP/Port và tham số logic đích cho SLMP.
|
||
/// </summary>
|
||
/// <param name="Host">Địa chỉ IP hoặc hostname PLC/module.</param>
|
||
/// <param name="Port">Cổng SLMP (ví dụ 5007).</param>
|
||
/// <param name="NetworkNo">Số mạng logic đích.</param>
|
||
/// <param name="ModuleIoNo">Địa chỉ I/O module logic đích.</param>
|
||
/// <param name="MultidropNo">Số đa điểm (nếu dùng).</param>
|
||
/// <param name="StationNo">Số trạm logic đích.</param>
|
||
public readonly record struct IeBasicRemote(
|
||
string Host,
|
||
int Port,
|
||
byte NetworkNo,
|
||
ushort ModuleIoNo,
|
||
byte MultidropNo,
|
||
byte StationNo
|
||
);
|
||
|
||
/// <summary>
|
||
/// Tùy chọn khởi tạo/kết nối IE Basic client.
|
||
/// </summary>
|
||
public sealed record IeBasicClientOptions(
|
||
string Host,
|
||
int Port,
|
||
byte NetworkNo,
|
||
ushort ModuleIoNo,
|
||
byte MultidropNo,
|
||
byte StationNo
|
||
)
|
||
{
|
||
/// <summary>Transport: UDP hoặc TCP.</summary>
|
||
public IeBasicTransport Transport { get; init; } = IeBasicTransport.Udp;
|
||
|
||
/// <summary>Định dạng frame SLMP (3E/4E).</summary>
|
||
public SlmpFrameFormat FrameFormat { get; init; } = SlmpFrameFormat.Format3E;
|
||
|
||
/// <summary>Tham số logic phía local.</summary>
|
||
public IeBasicLocal Local { get; init; } =
|
||
new IeBasicLocal(0, 0x03FF, 0, 0);
|
||
|
||
/// <summary>Timeout cho mỗi yêu cầu.</summary>
|
||
public TimeSpan Timeout { get; init; } = TimeSpan.FromMilliseconds(1000);
|
||
|
||
/// <summary>Số lần retry khi lỗi tạm thời.</summary>
|
||
public int RetryCount { get; init; } = 2;
|
||
|
||
/// <summary>Tự động reconnect nếu mất liên kết.</summary>
|
||
public bool AutoReconnect { get; init; } = true;
|
||
|
||
/// <summary>
|
||
/// Quy ước ghép cặp word trong DWord/Float (true: little‑endian word order).
|
||
/// </summary>
|
||
public bool LittleEndianWordOrder { get; init; } = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Tùy chọn cho polling chu kỳ các vùng device.
|
||
/// </summary>
|
||
public sealed record PollOptions
|
||
{
|
||
/// <summary>Khoảng thời gian giữa hai lần poll.</summary>
|
||
public TimeSpan Interval { get; init; } = TimeSpan.FromMilliseconds(20);
|
||
|
||
/// <summary>Danh sách vùng cần poll (device, địa chỉ bắt đầu, số lượng).</summary>
|
||
public (DeviceCode Device, int Start, int Count)[] Areas { get; init; } = [];
|
||
|
||
/// <summary>Giới hạn số word tối đa mỗi chu kỳ (để tránh gói quá lớn).</summary>
|
||
public int MaxWordsPerCycle { get; init; } = 256;
|
||
|
||
/// <summary>Gom vùng theo từng loại device để tối ưu số gói.</summary>
|
||
public bool AlignAreasByDevice { get; init; } = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Dữ liệu trả về sau mỗi chu kỳ polling.
|
||
/// </summary>
|
||
public sealed class PollUpdatedEventArgs : EventArgs
|
||
{
|
||
/// <summary>Thời điểm khung dữ liệu được cập nhật.</summary>
|
||
public DateTimeOffset Timestamp { get; init; }
|
||
|
||
/// <summary>Mảng bit (nếu có vùng bit được cấu hình).</summary>
|
||
public ReadOnlyMemory<bool>? Bits { get; init; }
|
||
|
||
/// <summary>Mảng word (nếu có vùng word được cấu hình).</summary>
|
||
public ReadOnlyMemory<ushort>? Words { get; init; }
|
||
|
||
/// <summary>Mảng dword (nếu có vùng dword được cấu hình).</summary>
|
||
public ReadOnlyMemory<uint>? DWords { get; init; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Trạng thái liên kết/SLMP để chẩn đoán và theo dõi.
|
||
/// </summary>
|
||
public sealed record IeBasicStatus
|
||
{
|
||
/// <summary>Liên kết hiện “đang lên” (có phản hồi) hay không.</summary>
|
||
public bool LinkUp { get; init; }
|
||
|
||
/// <summary>Số lỗi liên tiếp gần nhất.</summary>
|
||
public int ConsecutiveErrors { get; init; }
|
||
|
||
/// <summary>End code SLMP của lỗi gần nhất (nếu có).</summary>
|
||
public SlmpEndCode? LastEndCode { get; init; }
|
||
|
||
/// <summary>Mô tả lỗi gần nhất (nếu có).</summary>
|
||
public string? LastErrorText { get; init; }
|
||
|
||
/// <summary>Thời gian phản hồi trung bình ước tính.</summary>
|
||
public TimeSpan? AvgRtt { get; init; }
|
||
|
||
/// <summary>Thời gian phản hồi lớn nhất quan sát.</summary>
|
||
public TimeSpan? MaxRtt { get; init; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Thông tin nhận dạng CPU/module (nếu PLC cho phép truy vấn).
|
||
/// </summary>
|
||
public sealed record ModuleIdentity
|
||
{
|
||
/// <summary>Dòng CPU (ví dụ iQ‑R, iQ‑F).</summary>
|
||
public string? CpuSeries { get; init; }
|
||
|
||
/// <summary>Model cụ thể (ví dụ R04ENCPU).</summary>
|
||
public string? CpuModel { get; init; }
|
||
|
||
/// <summary>Phiên bản firmware.</summary>
|
||
public string? Firmware { get; init; }
|
||
|
||
/// <summary>Nhà sản xuất (thường là Mitsubishi Electric).</summary>
|
||
public string? Vendor { get; init; }
|
||
|
||
/// <summary>Thông tin bổ sung khác (nếu có).</summary>
|
||
public string? AdditionalInfo { get; init; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Mã lệnh SLMP rút gọn (có thể mở rộng khi hiện thực adapter).
|
||
/// </summary>
|
||
public enum SlmpCommand : ushort
|
||
{
|
||
/// <summary>Đọc device theo dải liên tục (bit/word).</summary>
|
||
ReadDevice = 0x0401,
|
||
|
||
/// <summary>Ghi device theo dải liên tục (bit/word).</summary>
|
||
WriteDevice = 0x1401,
|
||
|
||
/// <summary>Đọc ngẫu nhiên nhiều điểm.</summary>
|
||
ReadRandom = 0x0403,
|
||
|
||
/// <summary>Ghi ngẫu nhiên nhiều điểm.</summary>
|
||
WriteRandom = 0x1402,
|
||
|
||
/// <summary>Node monitor / kiểm tra liên lạc.</summary>
|
||
NodeMonitor = 0x0619,
|
||
|
||
/// <summary>Truy vấn thông tin thiết bị/PLC.</summary>
|
||
DeviceInfo = 0x0601
|
||
}
|
||
|
||
/// <summary>
|
||
/// Mã end code SLMP (hoàn thành/thất bại), gồm cả mã nội bộ ánh xạ lỗi transport/timeout.
|
||
/// </summary>
|
||
public enum SlmpEndCode : ushort
|
||
{
|
||
/// <summary>Hoàn thành thành công.</summary>
|
||
Completed = 0x0000,
|
||
|
||
/// <summary>Lệnh không hợp lệ.</summary>
|
||
InvalidCommand = 0xC059,
|
||
|
||
/// <summary>Truy cập bị từ chối.</summary>
|
||
AccessDenied = 0xC056,
|
||
|
||
/// <summary>Lỗi phạm vi device.</summary>
|
||
DeviceRangeError = 0xC051,
|
||
|
||
/// <summary>Thiết bị bận.</summary>
|
||
Busy = 0xCEE0,
|
||
|
||
/// <summary>Quá thời gian chờ (gán nội bộ).</summary>
|
||
Timeout = 0xFFFF,
|
||
|
||
/// <summary>Lỗi transport/socket (gán nội bộ).</summary>
|
||
TransportError = 0xFFFE,
|
||
|
||
/// <summary>Lỗi không xác định (gán nội bộ).</summary>
|
||
Unknown = 0xFFFD
|
||
}
|
||
|
||
/// <summary>
|
||
/// Phản hồi SLMP thô trả về từ <see cref="ICcLinkIeBasicClient.SendSlmpAsync"/>.
|
||
/// </summary>
|
||
public sealed record SlmpResponse
|
||
{
|
||
/// <summary>End code của phản hồi.</summary>
|
||
public SlmpEndCode EndCode { get; init; }
|
||
|
||
/// <summary>Payload nhị phân trả về (theo frame 3E/4E).</summary>
|
||
public ReadOnlyMemory<byte> Payload { get; init; }
|
||
|
||
/// <summary>Thời điểm nhận gói.</summary>
|
||
public DateTimeOffset Timestamp { get; init; }
|
||
}
|