using System.Buffers.Binary; using System.Text; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Wire; /// /// FWLIB ODBALMHIS struct decoder for the cnc_rdalmhistry alarm-history /// extension (issue #267, plan PR F3-a). Documents + decodes the historical-alarm /// payload returned by FANUC controllers when asked for the most-recent N ring-buffer /// entries. /// /// /// ODBALMHIS layout (per FOCAS reference, abridged): /// /// short num_alm — number of valid alarm-history records that follow. /// Negative on CNC-reported error. /// ALMHIS_data alm[N] — repeated entry record. Each record carries: /// /// short year, month, day, hour, minute, second — wall-clock /// time the CNC stamped on the entry. Surfaced here as /// in UTC; the wire field is the CNC's /// local time, but the deployment doc instructs operators to keep their /// CNC clocks on UTC for the history projection so the dedup key stays /// stable across DST transitions. /// short axis_no — axis the alarm relates to (1-based; /// 0 means "no specific axis"). /// short alm_type — alarm type (P/S/OT/SV/SR/MC/SP/PW/IO). /// The numeric encoding varies slightly per series; surfaced as-is so /// downstream consumers don't lose detail. /// short alm_no — alarm number within the type. /// short msg_len — length of the message string that follows. /// Capped server-side at 32 chars on most series. /// char msg[msg_len] — message text. Trimmed of trailing /// nulls + spaces before publishing. /// /// /// /// The simulator-mock surface assigns command id 0x0F1A to /// cnc_rdalmhistry — see docs/v2/implementation/focas-simulator-plan.md. /// public static class FocasAlarmHistoryDecoder { /// Wire-protocol command identifier the simulator routes cnc_rdalmhistry on. public const ushort CommandId = 0x0F1A; /// /// Decode a packed ODBALMHIS payload into a list of /// records ordered most-recent-first (the /// FANUC ring buffer's natural order). Returns an empty list when the buffer is /// too small to hold the count prefix or when the CNC reported zero entries. /// /// /// Layout of in little-endian wire form: /// /// Bytes 0..1 — short num_alm /// Bytes 2..N — repeated entry blocks. Each block: 14 bytes of fixed /// header (year, month, day, hour, minute, second, axis_no, alm_type, /// alm_no, msg_len — 7×short with the seventh shared between /// axis_no+packing — laid out as 10 little-endian shorts here for /// simplicity), followed by msg_len ASCII bytes. The simulator pads /// each block to a 4-byte boundary; this decoder follows. /// /// Real FWLIB hands back a Marshal-shaped struct, not a packed buffer; the /// packed-buffer convention here is purely for the simulator + IPC transport so /// the wire protocol stays language-neutral. Tier-C Fwlib32-backed clients /// short-circuit this decoder by surfacing the struct fields directly. /// public static IReadOnlyList Decode(ReadOnlySpan payload) { if (payload.Length < 2) return Array.Empty(); var count = BinaryPrimitives.ReadInt16LittleEndian(payload[..2]); if (count <= 0) return Array.Empty(); var entries = new List(count); var offset = 2; for (var i = 0; i < count; i++) { // Each entry: 10 little-endian shorts of header (20 bytes) + msg_len bytes. // Header layout: year, month, day, hour, minute, second, axis_no, alm_type, // alm_no, msg_len. const int headerBytes = 20; if (offset + headerBytes > payload.Length) break; var header = payload.Slice(offset, headerBytes); var year = BinaryPrimitives.ReadInt16LittleEndian(header[0..2]); var month = BinaryPrimitives.ReadInt16LittleEndian(header[2..4]); var day = BinaryPrimitives.ReadInt16LittleEndian(header[4..6]); var hour = BinaryPrimitives.ReadInt16LittleEndian(header[6..8]); var minute = BinaryPrimitives.ReadInt16LittleEndian(header[8..10]); var second = BinaryPrimitives.ReadInt16LittleEndian(header[10..12]); var axisNo = BinaryPrimitives.ReadInt16LittleEndian(header[12..14]); var almType = BinaryPrimitives.ReadInt16LittleEndian(header[14..16]); var almNo = BinaryPrimitives.ReadInt16LittleEndian(header[16..18]); var msgLen = BinaryPrimitives.ReadInt16LittleEndian(header[18..20]); offset += headerBytes; if (msgLen < 0 || offset + msgLen > payload.Length) break; var msgBytes = payload.Slice(offset, msgLen); var msg = Encoding.ASCII.GetString(msgBytes).TrimEnd('\0', ' '); offset += msgLen; // Pad to 4-byte boundary so per-entry blocks stay self-delimiting on the wire. var pad = (4 - (msgLen % 4)) % 4; offset += pad; DateTimeOffset occurrence; try { occurrence = new DateTimeOffset( year, month, day, hour, minute, second, TimeSpan.Zero); } catch (ArgumentOutOfRangeException) { // CNC reported a malformed timestamp — skip the entry rather than // exception-spew the entire history poll. The dedup key would be // unstable for malformed timestamps anyway. continue; } entries.Add(new FocasAlarmHistoryEntry( OccurrenceTime: occurrence, AxisNo: axisNo, AlarmType: almType, AlarmNumber: almNo, Message: msg)); } return entries; } /// /// Encode into the wire format /// consumes. Used by the simulator-mock + tests to build canned payloads without /// having to know the byte-level layout. Output is a fresh array; callers don't /// need to manage a pooled buffer. /// public static byte[] Encode(IReadOnlyList entries) { ArgumentNullException.ThrowIfNull(entries); // Pre-size: 2-byte count + 20-byte header + msg + pad per entry. var size = 2; foreach (var e in entries) { var msg = e.Message ?? string.Empty; var msgBytes = Encoding.ASCII.GetByteCount(msg); size += 20 + msgBytes + ((4 - (msgBytes % 4)) % 4); } var buf = new byte[size]; var span = buf.AsSpan(); BinaryPrimitives.WriteInt16LittleEndian(span[..2], (short)Math.Min(entries.Count, short.MaxValue)); var offset = 2; foreach (var e in entries) { var msg = e.Message ?? string.Empty; var t = e.OccurrenceTime.ToUniversalTime(); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 0, 2), (short)t.Year); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 2, 2), (short)t.Month); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 4, 2), (short)t.Day); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 6, 2), (short)t.Hour); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 8, 2), (short)t.Minute); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 10, 2), (short)t.Second); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 12, 2), (short)e.AxisNo); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 14, 2), (short)e.AlarmType); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 16, 2), (short)e.AlarmNumber); var msgLen = Encoding.ASCII.GetByteCount(msg); BinaryPrimitives.WriteInt16LittleEndian(span.Slice(offset + 18, 2), (short)msgLen); offset += 20; Encoding.ASCII.GetBytes(msg, span.Slice(offset, msgLen)); offset += msgLen; offset += (4 - (msgLen % 4)) % 4; } return buf; } }