| | |
| | | using Microsoft.Extensions.Options; |
| | | using Microsoft.Extensions.Options; |
| | | using WIDESEAWCS_S7Simulator.Core.Interfaces; |
| | | using WIDESEAWCS_S7Simulator.Core.Protocol; |
| | | |
| | | namespace WIDESEAWCS_S7Simulator.Application.Protocol; |
| | | |
| | | /// <summary> |
| | | /// 化成堆垛机设备协议处理器。 |
| | | /// PlcLink 堆垛机设备协议处理器。 |
| | | /// </summary> |
| | | public class PlcLinkStackerProtocolHandler : IDeviceProtocolHandler |
| | | { |
| | | private readonly MirrorAckProtocolHandler _mirrorAckHandler; |
| | | private readonly ProtocolMonitoringOptions _options; |
| | | private const string ProtocolNameStatic = "PlcLinkStackerProtocol"; |
| | | |
| | | public PlcLinkStackerProtocolHandler( |
| | | MirrorAckProtocolHandler mirrorAckHandler, |
| | |
| | | |
| | | public bool Process(IMemoryStore memoryStore, ProtocolTemplate template, ProtocolRuntimeState runtimeState) |
| | | { |
| | | // 先执行 PlcLink 堆垛机固定 DB 交互逻辑。 |
| | | var changed = ApplyPlcLinkStackerOffsets(memoryStore, template, runtimeState); |
| | | |
| | | var ruleIds = _options.PlcLinkStackerRuleIds |
| | | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| | | .Distinct(StringComparer.OrdinalIgnoreCase) |
| | | .ToArray(); |
| | | if (ruleIds.Length == 0) |
| | | { |
| | | return false; |
| | | return changed; |
| | | } |
| | | |
| | | bool changed = false; |
| | | foreach (var ruleId in ruleIds) |
| | | { |
| | | var rule = _options.MirrorAckRules |
| | |
| | | continue; |
| | | } |
| | | |
| | | changed |= _mirrorAckHandler.Process(memoryStore, template, runtimeState, rule, $"{ProtocolName}:{ruleId}"); |
| | | var stateKey = $"{ProtocolName}:{ruleId}"; |
| | | changed |= _mirrorAckHandler.Process(memoryStore, template, runtimeState, rule, stateKey); |
| | | } |
| | | |
| | | return changed; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// PlcLink 堆垛机偏移交互规则: |
| | | /// 1) 初始化(仅首次):DB910 offset6=1,offset20=1 |
| | | /// 2) DB900 offset40=1:DB910 offset28=DB900 offset2,DB910 offset20=2 |
| | | /// 3) DB900 offset40=2:DB910 offset28=0,DB910 offset20=1 |
| | | /// </summary> |
| | | private static bool ApplyPlcLinkStackerOffsets( |
| | | IMemoryStore memoryStore, |
| | | ProtocolTemplate template, |
| | | ProtocolRuntimeState runtimeState) |
| | | { |
| | | const int db900 = 900; |
| | | const int db910 = 910; |
| | | bool changed = false; |
| | | |
| | | var db900Offset2 = ResolveField(template, db900, 2); |
| | | var db900Offset40 = ResolveField(template, db900, 40); |
| | | var db910Offset6 = ResolveField(template, db910, 6); |
| | | var db910Offset20 = ResolveField(template, db910, 20); |
| | | var db910Offset28 = ResolveField(template, db910, 28); |
| | | var db910Offset42 = ResolveField(template, db910, 42); |
| | | |
| | | // 初始化只在实例启动后的第一次轮询写入一次,避免覆盖现场值。 |
| | | var initKey = BuildInitStateKey(template); |
| | | if (!runtimeState.LastWcsAckByKey.ContainsKey(initKey)) |
| | | { |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset6, 1); |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset20, 1); |
| | | runtimeState.LastWcsAckByKey[initKey] = 1; |
| | | } |
| | | |
| | | var offset40 = TryReadNumeric(memoryStore, db900Offset40); |
| | | if (offset40 == 1) |
| | | { |
| | | var offset2 = TryReadNumeric(memoryStore, db900Offset2); |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset28, offset2); |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset20, 2); |
| | | } |
| | | else if (offset40 == 2) |
| | | { |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset28, 0); |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset20, 1); |
| | | changed |= TryWriteNumeric(memoryStore, db900Offset40, 0); |
| | | changed |= TryWriteNumeric(memoryStore, db910Offset42, 0); |
| | | } |
| | | |
| | | return changed; |
| | | } |
| | | |
| | | private static string BuildInitStateKey(ProtocolTemplate template) |
| | | { |
| | | var templateId = string.IsNullOrWhiteSpace(template.Id) ? "unknown" : template.Id; |
| | | return $"{ProtocolNameStatic}:Init:{templateId}:DB910"; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 优先按「DB号+Offset」匹配模板字段;找不到则按 Offset 回退;仍找不到则使用默认 Byte 映射。 |
| | | /// </summary> |
| | | private static ProtocolFieldMapping ResolveField(ProtocolTemplate template, int dbNumber, int offset) |
| | | { |
| | | var byDb = template.Fields.FirstOrDefault(x => x.DbNumber == dbNumber && x.Offset == offset); |
| | | if (byDb != null) |
| | | { |
| | | return byDb; |
| | | } |
| | | |
| | | var byOffset = template.Fields.FirstOrDefault(x => x.Offset == offset); |
| | | if (byOffset != null) |
| | | { |
| | | return byOffset; |
| | | } |
| | | |
| | | return new ProtocolFieldMapping |
| | | { |
| | | DbNumber = dbNumber, |
| | | Offset = offset, |
| | | Bit = 0, |
| | | DataType = ProtocolDataType.Byte |
| | | }; |
| | | } |
| | | |
| | | private static string BuildAddress(ProtocolFieldMapping field) |
| | | { |
| | | return field.DataType switch |
| | | { |
| | | ProtocolDataType.Bool => $"DB{field.DbNumber}.DBX{field.Offset}.{field.Bit}", |
| | | ProtocolDataType.Int => $"DB{field.DbNumber}.DBW{field.Offset}", |
| | | ProtocolDataType.DInt => $"DB{field.DbNumber}.DBD{field.Offset}", |
| | | _ => $"DB{field.DbNumber}.DBB{field.Offset}" |
| | | }; |
| | | } |
| | | |
| | | private static int TryReadNumeric(IMemoryStore memoryStore, ProtocolFieldMapping field) |
| | | { |
| | | var address = BuildAddress(field); |
| | | return field.DataType switch |
| | | { |
| | | ProtocolDataType.Bool => memoryStore.Read<bool>(address) ? 1 : 0, |
| | | ProtocolDataType.Int => memoryStore.Read<short>(address), |
| | | ProtocolDataType.DInt => memoryStore.Read<int>(address), |
| | | ProtocolDataType.String => TryReadStringNumeric(memoryStore, field), |
| | | _ => memoryStore.Read<byte>(address) |
| | | }; |
| | | } |
| | | |
| | | private static int TryReadStringNumeric(IMemoryStore memoryStore, ProtocolFieldMapping field) |
| | | { |
| | | var len = field.Length > 0 ? field.Length : 16; |
| | | var bytes = memoryStore.ReadBytes(BuildAddress(field), (ushort)len); |
| | | var text = System.Text.Encoding.ASCII.GetString(bytes).Trim('\0', ' '); |
| | | return int.TryParse(text, out var value) ? value : 0; |
| | | } |
| | | |
| | | private static bool TryWriteNumeric(IMemoryStore memoryStore, ProtocolFieldMapping field, int targetValue) |
| | | { |
| | | var address = BuildAddress(field); |
| | | switch (field.DataType) |
| | | { |
| | | case ProtocolDataType.Bool: |
| | | { |
| | | var next = targetValue != 0; |
| | | var current = memoryStore.Read<bool>(address); |
| | | if (current == next) return false; |
| | | memoryStore.Write(address, next); |
| | | return true; |
| | | } |
| | | case ProtocolDataType.Int: |
| | | { |
| | | var next = (short)targetValue; |
| | | var current = memoryStore.Read<short>(address); |
| | | if (current == next) return false; |
| | | memoryStore.Write(address, next); |
| | | return true; |
| | | } |
| | | case ProtocolDataType.DInt: |
| | | { |
| | | var current = memoryStore.Read<int>(address); |
| | | if (current == targetValue) return false; |
| | | memoryStore.Write(address, targetValue); |
| | | return true; |
| | | } |
| | | case ProtocolDataType.String: |
| | | { |
| | | var len = field.Length > 0 ? field.Length : 16; |
| | | var nextText = targetValue.ToString(); |
| | | var nextBytes = new byte[len]; |
| | | var raw = System.Text.Encoding.ASCII.GetBytes(nextText); |
| | | Array.Copy(raw, nextBytes, Math.Min(raw.Length, nextBytes.Length)); |
| | | var currentBytes = memoryStore.ReadBytes(address, (ushort)len); |
| | | if (currentBytes.SequenceEqual(nextBytes)) return false; |
| | | memoryStore.WriteBytes(address, nextBytes); |
| | | return true; |
| | | } |
| | | default: |
| | | { |
| | | var next = (byte)targetValue; |
| | | var current = memoryStore.Read<byte>(address); |
| | | if (current == next) return false; |
| | | memoryStore.Write(address, next); |
| | | return true; |
| | | } |
| | | } |
| | | } |
| | | } |