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;
}
}