using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Wire; namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Pccc; /// Pure builders/parsers for Execute PCCC (CIP 0x4B on class 0x67) carrying SLC/MicroLogix /// protected typed logical commands and PLC-5 range reads. Framing is taken from the golden captures. /// The Execute PCCC request is sent bare (no Unconnected Send, no route) per the captures. public static class PcccServices { /// Largest data byte count one typed read/write may carry (single-byte count on the wire). public const int MaxDataBytes = 236; /// True: PLC-5 range READ (cmd 0x0F fnc 0x01) is capture-verified. public static bool Plc5Verified { get; } = true; /// False: no PLC-5 write capture exists, so no PLC-5 write builder ships. public static bool Plc5WriteVerified { get; } = false; /// Requestor vendor id the native transport puts in Execute PCCC: libplctag's, from the captures /// (07 3D F3 45 43 50 21 in slc500-read-n7.hex). /// Not the CIP originator vendor (). ab_server answers every /// PCCC request with this fixed requestor id (PCCC_RESP_PREFIX in its pccc.c) instead of /// echoing the request's, while real controllers echo whatever was sent. Sending libplctag's identity is /// the only choice that lets the echo check in pass against both (found by /// Task 17 slc500: with the 0x1337 originator identity every native PCCC reply was BadReply). public const ushort RequestorVendor = 0xF33D; /// Requestor serial number paired with (libplctag's, from the captures). public const uint RequestorSerial = 0x21504345; /// Wraps a PCCC command in Execute PCCC: 4B 02 20 67 24 01 + requestor id (7, vendor, serial) + PCCC. public static byte[] ExecutePccc(ushort requestorVendor, uint requestorSerial, ReadOnlySpan pccc) => new CipWriter().U8(0x4B).U8(2).U8(0x20).U8(0x67).U8(0x24).U8(0x01) .U8(7).U16(requestorVendor).U32(requestorSerial).Bytes(pccc).ToArray(); private static void Field(CipWriter w, int v) { if (v < 0) throw new AbTransportException(AbStatus.OutOfBounds, $"Negative PCCC field {v}."); if (v > ushort.MaxValue) throw new AbTransportException(AbStatus.OutOfBounds, $"PCCC field {v} exceeds 65535."); if (v < 0xFF) w.U8((byte)v); else w.U8(0xFF).U16((ushort)v); } private static void CheckCount(int n) { if (n < 0 || n > MaxDataBytes) throw new AbTransportException(AbStatus.TooLarge, $"PCCC byte count {n} exceeds {MaxDataBytes}."); } private static void Logical(CipWriter w, PcccAddress a) { Field(w, a.EffectiveFileNumber); w.U8(a.FileTypeCode); Field(w, a.Element); Field(w, a.ResolveSubElement().Word); } /// Protected typed logical read with 3 address fields (0x0F/0xA2). public static byte[] SlcTypedRead(ushort tns, PcccAddress addr, int byteCount) { CheckCount(byteCount); var w = new CipWriter().U8(0x0F).U8(0).U16(tns).U8(0xA2).U8((byte)byteCount); Logical(w, addr); return w.ToArray(); } /// Protected typed logical write with 3 address fields (0x0F/0xAA). SLC/MicroLogix/LogixPccc /// only: it must not be used for PLC-5 (the family guard lives in the AbLegacy native runtime, Task 14). public static byte[] SlcTypedWrite(ushort tns, PcccAddress addr, ReadOnlySpan data) { CheckCount(data.Length); var w = new CipWriter().U8(0x0F).U8(0).U16(tns).U8(0xAA).U8((byte)data.Length); Logical(w, addr); return w.Bytes(data).ToArray(); } /// PLC-5 range read (0x0F/0x01), as captured from libplctag: /// offset u16 (0), totalTrans u16, levelMask u8 (0x06 = file+element), file*, element*, byteCount u8. /// totalTrans counts 16-bit WORDS (DF1 word range read; the capture shows 1 for one 2-byte N element). /// Only N-file reads are capture-verified, other PLC-5 file types are unverified; sub-element addressing is not and throws Unsupported. public static byte[] Plc5RangeRead(ushort tns, PcccAddress addr, ushort totalTrans, int byteCount) { CheckCount(byteCount); if (addr.ResolveSubElement().Word != 0) throw new AbTransportException(AbStatus.Unsupported, "PLC-5 sub-element addressing is not capture-verified."); var w = new CipWriter().U8(0x0F).U8(0).U16(tns).U8(0x01).U16(0).U16(totalTrans).U8(0x06); Field(w, addr.EffectiveFileNumber); Field(w, addr.Element); return w.U8((byte)byteCount).ToArray(); } /// Validates an Execute PCCC reply and returns the PCCC data bytes. /// The parsed CIP reply. /// The request's TNS; checked before STS so a stale error reply is BadReply. /// Optional full echoed requestor id (length byte included) to compare. public static ReadOnlyMemory ParseReply(CipReply reply, ushort expectedTns, byte[]? expectedRequestorId = null) { if (reply.GeneralStatus != 0) throw new AbTransportException(reply.Status, $"Execute PCCC CIP status 0x{reply.GeneralStatus:X2}."); var r = new CipReader(reply.Data.Span); var idLen = r.U8(); if (idLen < 1) throw new AbTransportException(AbStatus.BadReply, "Empty requestor id in PCCC reply."); var idBody = r.Bytes(idLen - 1); if (expectedRequestorId is not null && !(expectedRequestorId.Length == idLen && expectedRequestorId[0] == idLen && idBody.SequenceEqual(expectedRequestorId.AsSpan(1)))) throw new AbTransportException(AbStatus.BadReply, "PCCC reply requestor id does not match the request."); var cmd = r.U8(); if (cmd != 0x4F) throw new AbTransportException(AbStatus.BadReply, $"PCCC reply command 0x{cmd:X2} != 0x4F."); var sts = r.U8(); var tns = r.U16(); if (tns != expectedTns) throw new AbTransportException(AbStatus.BadReply, $"PCCC TNS {tns} != {expectedTns}."); byte? ext = sts == 0xF0 ? r.U8() : null; if (sts != 0) throw new AbTransportException(AbStatusCodes.FromPccc(sts, ext), $"PCCC STS 0x{sts:X2}{(ext is { } e ? $" EXT 0x{e:X2}" : "")}."); return reply.Data[r.Position..]; } }