Files
DT_BR_GUI/LFP_Manager/Forms/fmxPacketLog.cs
jkwoo e041527b56 V1.0.2.0
* Added RS232/RS485 Packet Analyzer (Tools -> Packet Analyzer)
* Updated BMS History register request/response length by model
* Fixed decimal truncation issue in Brazil locale for DB/CSV Log and Excel Export
2026-11-06 15:35:44 +09:00

426 lines
16 KiB
C#

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 = null)
{
if (!isCapturing || data == null || data.Length == 0) return;
if (string.IsNullOrEmpty(description))
{
description = GetAutoDescription(data, isTx);
}
if (this.InvokeRequired)
{
this.BeginInvoke(new Action(() => AddPacketLogRow(timestamp, isTx, data, description)));
}
else
{
AddPacketLogRow(timestamp, isTx, data, description);
}
}
private string GetAutoDescription(byte[] data, bool isTx)
{
if (data == null || data.Length < 2) return "-";
byte devId = data[0];
byte func = data[1];
if (isTx)
{
if (data.Length >= 6)
{
ushort regAddr = (ushort)((data[2] << 8) | data[3]);
ushort regCount = (ushort)((data[4] << 8) | data[5]);
switch (func)
{
case 0x01: return string.Format("Req: Read Coils (Addr:0x{0:X4}, Qty:{1})", regAddr, regCount);
case 0x02: return string.Format("Req: Read Discrete Inputs (Addr:0x{0:X4}, Qty:{1})", regAddr, regCount);
case 0x03: return string.Format("Req: Read Holding Regs (Addr:0x{0:X4}, Qty:{1})", regAddr, regCount);
case 0x04: return string.Format("Req: Read Input Regs (Addr:0x{0:X4}, Qty:{1})", regAddr, regCount);
case 0x05: return string.Format("Req: Write Single Coil (Addr:0x{0:X4}, Val:0x{1:X4})", regAddr, regCount);
case 0x06: return string.Format("Req: Write Single Reg (Addr:0x{0:X4}, Val:0x{1:X4})", regAddr, regCount);
case 0x10: return string.Format("Req: Write Multiple Regs (Addr:0x{0:X4}, Qty:{1})", regAddr, regCount);
}
}
return string.Format("Req: Func 0x{0:X2} (Slave 0x{1:X2})", func, devId);
}
else
{
if (func > 0x80)
{
byte err = data.Length > 2 ? data[2] : (byte)0;
return string.Format("Resp: Exception (0x{0:X2} - {1})", err, GetExceptionName(err));
}
switch (func)
{
case 0x01:
case 0x02:
case 0x03:
case 0x04:
int byteCount = data.Length > 2 ? data[2] : 0;
return string.Format("Resp: OK ({0} bytes data)", byteCount);
case 0x05:
case 0x06:
case 0x10:
return "Resp: Write Success";
default:
return string.Format("Resp: Func 0x{0:X2}", func);
}
}
}
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();
}
}
}
}