2026-08-14 14:43:11 +08:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
|
using IoTClient.Clients.PLC;
|
|
|
|
|
|
using IoTClient.Common.Enums;
|
|
|
|
|
|
|
|
|
|
|
|
namespace StandardScene.Signal.Plc
|
|
|
|
|
|
{
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 西门子 S7 长连接封装(IoTClient)。一个机构一个实例,由对应 Worker 独占。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// 字节地址兼容老 Hsl 写法 <c>DB1.0</c> 与 IoTClient 写法 <c>DB1.DBB0</c>;
|
|
|
|
|
|
/// 心跳位地址保持原样(如 <c>DB1.DBX2.0</c>),不做 DBB 转换。
|
|
|
|
|
|
/// 连接失败时读/写返回 false,由 Worker 标断线,不在这里抛给扫车线程。
|
|
|
|
|
|
/// </remarks>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public sealed class SiemensPlcSession : IDisposable
|
|
|
|
|
|
{
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>匹配 Hsl 风格「DB块号.字节偏移」,捕获组 1=DB,2=偏移。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
private static readonly Regex BareDbByte = new Regex(@"^DB(\d+)\.(\d+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
|
|
|
|
|
|
|
|
|
|
private readonly SiemensVersion _version;
|
|
|
|
|
|
private readonly string _ip;
|
|
|
|
|
|
private readonly int _port;
|
|
|
|
|
|
private readonly byte _slot;
|
|
|
|
|
|
private SiemensClient _client;
|
|
|
|
|
|
private readonly object _sync = new object();
|
|
|
|
|
|
|
2026-08-24 14:55:35 +08:00
|
|
|
|
/// <param name="version">西门子系列名,如 S7_1500;空或无法解析时默认 S7_1500。</param>
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <param name="ip">机构 IP。</param>
|
|
|
|
|
|
/// <param name="port">S7 端口,通常 102。</param>
|
|
|
|
|
|
/// <param name="slot">机架槽号,S7-1500 常见 1。</param>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public SiemensPlcSession(string version, string ip, int port, int slot)
|
|
|
|
|
|
{
|
|
|
|
|
|
_version = ParseVersion(version);
|
|
|
|
|
|
_ip = ip ?? "";
|
|
|
|
|
|
_port = port > 0 ? port : 102;
|
|
|
|
|
|
_slot = (byte)Math.Max(0, slot);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>当前客户端是否已 Open 成功。未创建客户端时为 false。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public bool Connected
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_sync) return _client?.Connected == true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>惰性创建客户端并 Open。已连接则直接 true;失败返回 false 不抛。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public bool EnsureOpen()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_sync)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_client == null)
|
|
|
|
|
|
_client = new SiemensClient(_version, _ip, _port, _slot);
|
|
|
|
|
|
if (_client.Connected)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
var r = _client.Open();
|
|
|
|
|
|
return r != null && r.IsSucceed;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>读一字节。地址会先规范化;未连接或失败时 value=0 且返回 false。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public bool TryReadByte(string address, out byte value)
|
|
|
|
|
|
{
|
|
|
|
|
|
value = 0;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = _client.ReadByte(NormalizeByteAddress(address));
|
|
|
|
|
|
if (r == null || !r.IsSucceed)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
value = r.Value;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>写一字节(进入中/到位等)。失败返回 false。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public bool TryWriteByte(string address, byte value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = _client.Write(NormalizeByteAddress(address), value);
|
|
|
|
|
|
return r != null && r.IsSucceed;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 14:55:35 +08:00
|
|
|
|
/// <summary>从起始字节连续读 length 字节,用于工位 BOOL 拆位。</summary>
|
|
|
|
|
|
public bool TryReadBytes(string startAddress, ushort length, out byte[] data)
|
|
|
|
|
|
{
|
|
|
|
|
|
data = Array.Empty<byte>();
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(startAddress) || length == 0 || !EnsureOpen())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = _client.Read(NormalizeByteAddress(startAddress), length, false);
|
|
|
|
|
|
if (r == null || !r.IsSucceed || r.Value == null)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
data = r.Value;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>读心跳位。地址不做 DBB 转换。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public bool TryReadBool(string address, out bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
value = false;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = _client.ReadBoolean(address.Trim());
|
|
|
|
|
|
if (r == null || !r.IsSucceed)
|
|
|
|
|
|
return false;
|
|
|
|
|
|
value = r.Value;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>写心跳位(通常写当前值的反码,让 PLC 看到翻转)。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public bool TryWriteBool(string address, bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
|
|
|
|
|
|
return false;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = _client.Write(address.Trim(), value);
|
|
|
|
|
|
return r != null && r.IsSucceed;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>关闭并丢弃客户端,下次 EnsureOpen 会新建。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public void Close()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_sync)
|
|
|
|
|
|
{
|
|
|
|
|
|
try { _client?.Close(); }
|
|
|
|
|
|
catch { }
|
|
|
|
|
|
_client = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose() => Close();
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// <c>DB1.0</c> → <c>DB1.DBB0</c>;已是 DBB/DBW/DBX 的地址原样返回。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
internal static string NormalizeByteAddress(string address)
|
|
|
|
|
|
{
|
|
|
|
|
|
var addr = address.Trim();
|
|
|
|
|
|
var m = BareDbByte.Match(addr);
|
|
|
|
|
|
return m.Success ? $"DB{m.Groups[1].Value}.DBB{m.Groups[2].Value}" : addr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>空或无法解析时默认 S7_1500。允许把 S7-1500 写成带连字符。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
internal static SiemensVersion ParseVersion(string version)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(version))
|
|
|
|
|
|
return SiemensVersion.S7_1500;
|
|
|
|
|
|
return Enum.TryParse(version.Replace("-", "_"), true, out SiemensVersion parsed)
|
|
|
|
|
|
? parsed
|
|
|
|
|
|
: SiemensVersion.S7_1500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|