using System; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using DevExpress.XtraEditors; using DevExpress.XtraGrid.Views.Grid; using LFP_Manager.Function; namespace LFP_Manager.Forms { public partial class fmxPacketLog : DevExpress.XtraEditors.XtraForm { private DataTable packetTable = null; private int packetIndex = 0; private int txCount = 0; private int rxCount = 0; private bool isCapturing = true; private const int MAX_LOG_COUNT = 5000; public fmxPacketLog() { InitializeComponent(); InitDataTable(); } private void InitDataTable() { packetTable = new DataTable(); packetTable.Columns.Add("no", typeof(int)); packetTable.Columns.Add("time", typeof(string)); packetTable.Columns.Add("dir", typeof(string)); packetTable.Columns.Add("dev_id", typeof(string)); packetTable.Columns.Add("func", typeof(string)); packetTable.Columns.Add("len", typeof(int)); packetTable.Columns.Add("hex", typeof(string)); packetTable.Columns.Add("desc", typeof(string)); packetTable.Columns.Add("crc", typeof(string)); packetTable.Columns.Add("raw_bytes", typeof(object)); gcPacketLog.DataSource = packetTable; ApplyFilter(); } public void AddPacketLog(DateTime timestamp, bool isTx, byte[] data, string description) { if (!isCapturing || data == null || data.Length == 0) return; if (this.InvokeRequired) { this.BeginInvoke(new Action(() => AddPacketLogRow(timestamp, isTx, data, description))); } else { AddPacketLogRow(timestamp, isTx, data, description); } } private void AddPacketLogRow(DateTime timestamp, bool isTx, byte[] data, string description) { try { if (packetTable == null) return; // Max rows management if (packetTable.Rows.Count >= MAX_LOG_COUNT) { for (int i = 0; i < 500; i++) { if (packetTable.Rows.Count > 0) packetTable.Rows.RemoveAt(0); } } packetIndex++; if (isTx) txCount++; else rxCount++; string dir = isTx ? "TX" : "RX"; string devId = data.Length > 0 ? string.Format("0x{0:X2}", data[0]) : "-"; string func = data.Length > 1 ? string.Format("0x{0:X2}", data[1]) : "-"; int len = data.Length; StringBuilder sbHex = new StringBuilder(data.Length * 3); for (int i = 0; i < data.Length; i++) { sbHex.AppendFormat("{0:X2} ", data[i]); } string hex = sbHex.ToString().TrimEnd(); // CRC Check string crcResult = "OK"; if (data.Length >= 4) { byte[] calcCrc = csSerialCommFunction.GetCRC(data, data.Length - 2); byte rxCrcH = data[data.Length - 2]; byte rxCrcL = data[data.Length - 1]; if (calcCrc[1] != rxCrcH || calcCrc[0] != rxCrcL) { crcResult = "ERR"; } } DataRow row = packetTable.NewRow(); row["no"] = packetIndex; row["time"] = timestamp.ToString("HH:mm:ss.fff"); row["dir"] = dir; row["dev_id"] = devId; row["func"] = func; row["len"] = len; row["hex"] = hex; row["desc"] = description; row["crc"] = crcResult; row["raw_bytes"] = data; packetTable.Rows.Add(row); lblStatus.Text = string.Format("Total: {0} | TX: {1} | RX: {2}", packetIndex, txCount, rxCount); if (chkAutoScroll.Checked && gvPacketLog.RowCount > 0) { gvPacketLog.MoveLast(); } } catch { } } private void gvPacketLog_FocusedRowChanged(object sender, DevExpress.XtraGrid.Views.Base.FocusedRowChangedEventArgs e) { DataRow row = gvPacketLog.GetFocusedDataRow(); if (row == null) { txtDetail.Text = string.Empty; return; } byte[] rawBytes = row["raw_bytes"] as byte[]; string dir = row["dir"].ToString(); string desc = row["desc"].ToString(); string time = row["time"].ToString(); txtDetail.Text = AnalyzePacket(rawBytes, dir, desc, time); } private string AnalyzePacket(byte[] data, string dir, string desc, string time) { if (data == null || data.Length == 0) return "No Data"; StringBuilder sb = new StringBuilder(); sb.AppendLine("================================================================================"); sb.AppendLine(string.Format("[{0}] {1} (Length: {2} bytes) - {3}", dir, desc, data.Length, time)); sb.AppendLine("================================================================================"); // 1. Raw HEX Dump formatted sb.AppendLine("[RAW HEX DUMP]"); for (int i = 0; i < data.Length; i += 16) { sb.AppendFormat("{0:X4}: ", i); int count = Math.Min(16, data.Length - i); for (int j = 0; j < count; j++) { sb.AppendFormat("{0:X2} ", data[i + j]); } sb.AppendLine(); } sb.AppendLine(); // 2. Modbus Protocol Interpretation sb.AppendLine("[MODBUS FRAME INTERPRETATION]"); if (data.Length >= 2) { byte devId = data[0]; byte funcCode = data[1]; sb.AppendLine(string.Format(" - Device (Slave) Address : 0x{0:X2} ({0})", devId)); sb.AppendLine(string.Format(" - Function Code : 0x{0:X2} ({1})", funcCode, GetFunctionCodeName(funcCode))); if (dir == "TX") { if (data.Length >= 6) { ushort regAddr = (ushort)((data[2] << 8) | data[3]); ushort regCount = (ushort)((data[4] << 8) | data[5]); sb.AppendLine(string.Format(" - Target Register Address: 0x{0:X4} ({0})", regAddr)); sb.AppendLine(string.Format(" - Register Count / Value : 0x{0:X4} ({0})", regCount)); } } else // RX { if (funcCode == 0x03 && data.Length >= 3) { byte byteCount = data[2]; sb.AppendLine(string.Format(" - Data Byte Count : {0} bytes ({1} registers)", byteCount, byteCount / 2)); int regIndex = 0; for (int p = 3; p + 1 < data.Length - 2; p += 2) { ushort val = (ushort)((data[p] << 8) | data[p + 1]); sb.AppendLine(string.Format(" [Reg +{0:D2}] Offset 0x{1:X2} : 0x{2:X4} ({3})", regIndex++, p - 3, val, (short)val)); } } else if (funcCode > 0x80) { byte errCode = data.Length > 2 ? data[2] : (byte)0; sb.AppendLine(string.Format(" - Modbus Exception Code : 0x{0:X2} ({1})", errCode, GetExceptionName(errCode))); } } // CRC Check if (data.Length >= 4) { byte[] calcCrc = csSerialCommFunction.GetCRC(data, data.Length - 2); byte rxCrcH = data[data.Length - 2]; byte rxCrcL = data[data.Length - 1]; bool crcOk = (calcCrc[1] == rxCrcH && calcCrc[0] == rxCrcL); sb.AppendLine(string.Format(" - CRC16 Checksum : [H:0x{0:X2}, L:0x{1:X2}] (Calc: 0x{2:X2}{3:X2}) -> {4}", rxCrcH, rxCrcL, calcCrc[1], calcCrc[0], crcOk ? "VALID (OK)" : "INVALID (MISMATCH)")); } } return sb.ToString(); } private string GetFunctionCodeName(byte func) { switch (func) { case 0x01: return "Read Coil Status (0x01)"; case 0x02: return "Read Input Status (0x02)"; case 0x03: return "Read Holding Registers (0x03)"; case 0x04: return "Read Input Registers (0x04)"; case 0x05: return "Force Single Coil (0x05)"; case 0x06: return "Preset Single Register (0x06)"; case 0x10: return "Preset Multiple Registers (0x10)"; default: if (func > 0x80) return string.Format("Exception Response (0x{0:X2})", func); return string.Format("Custom / Unknown (0x{0:X2})", func); } } private string GetExceptionName(byte err) { switch (err) { case 0x01: return "Illegal Function"; case 0x02: return "Illegal Data Address"; case 0x03: return "Illegal Data Value"; case 0x04: return "Slave Device Failure"; case 0x05: return "Acknowledge"; case 0x06: return "Slave Device Busy"; default: return "Unknown Exception"; } } private void gvPacketLog_RowStyle(object sender, RowStyleEventArgs e) { if (e.RowHandle < 0) return; DataRow row = gvPacketLog.GetDataRow(e.RowHandle); if (row == null) return; string dir = row["dir"].ToString(); string crc = row["crc"].ToString(); if (crc == "ERR") { e.Appearance.ForeColor = Color.Red; e.Appearance.Font = new Font(e.Appearance.Font, FontStyle.Bold); } else if (dir == "TX") { e.Appearance.ForeColor = Color.DarkBlue; } else if (dir == "RX") { e.Appearance.ForeColor = Color.DarkGreen; } } private void btnCapture_Click(object sender, EventArgs e) { isCapturing = !isCapturing; btnCapture.Text = isCapturing ? "Pause" : "Resume"; btnCapture.Appearance.ForeColor = isCapturing ? Color.Black : Color.DarkRed; } private void btnClear_Click(object sender, EventArgs e) { packetTable?.Rows.Clear(); packetIndex = 0; txCount = 0; rxCount = 0; lblStatus.Text = "Total: 0 | TX: 0 | RX: 0"; txtDetail.Text = string.Empty; } private void cmbFilter_SelectedIndexChanged(object sender, EventArgs e) { ApplyFilter(); } private void ApplyFilter() { if (packetTable == null) return; string selected = cmbFilter.Text; DataView dv = packetTable.DefaultView; if (selected == "TX Only") { dv.RowFilter = "dir = 'TX'"; } else if (selected == "RX Only") { dv.RowFilter = "dir = 'RX'"; } else if (selected == "Error Only") { dv.RowFilter = "crc = 'ERR'"; } else { dv.RowFilter = string.Empty; } } private void btnExport_Click(object sender, EventArgs e) { if (packetTable == null || packetTable.Rows.Count == 0) { MessageBox.Show("No packet logs to export.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } SaveFileDialog sDialog = new SaveFileDialog(); sDialog.Title = "Export Packet Logs"; sDialog.DefaultExt = "xls"; sDialog.Filter = "Excel files (*.xls)|*.xls|All files (*.*)|*.*"; if (sDialog.ShowDialog() == DialogResult.OK) { try { DataTable exportTable = packetTable.DefaultView.ToTable(false, "no", "time", "dir", "dev_id", "func", "len", "hex", "desc", "crc"); csExcelExport.ExportToExcelExt(exportTable, sDialog.FileName); MessageBox.Show("Complete Exporting Packet Log File.", "Result", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show(ex.Message, "Exception", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void fmxPacketLog_FormClosing(object sender, FormClosingEventArgs e) { // Just hide on close so it can be re-opened quickly if (e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; this.Hide(); } } } }