From a62cebd17fa0b077ab366e4ca84346b854608d45 Mon Sep 17 00:00:00 2001 From: seuuung Date: Mon, 7 Sep 2026 10:55:44 +0900 Subject: [PATCH] Convert DCT_test from submodule to regular folder --- DCT_test | 1 - DCT_test/DCT_test.slnx | 3 + DCT_test/DCT_test/App.config | 6 + DCT_test/DCT_test/App.xaml | 9 + DCT_test/DCT_test/App.xaml.cs | 17 + DCT_test/DCT_test/ClientSession.cs | 27 + DCT_test/DCT_test/DCT_test.csproj | 102 +++ DCT_test/DCT_test/DctPacket.cs | 136 ++++ DCT_test/DCT_test/DctTcpServer.cs | 764 ++++++++++++++++++ DCT_test/DCT_test/LogFileReader.cs | 78 ++ DCT_test/DCT_test/MainWindow.xaml | 242 ++++++ DCT_test/DCT_test/MainWindow.xaml.cs | 710 ++++++++++++++++ DCT_test/DCT_test/Properties/AssemblyInfo.cs | 52 ++ .../DCT_test/Properties/Resources.Designer.cs | 71 ++ DCT_test/DCT_test/Properties/Resources.resx | 117 +++ .../DCT_test/Properties/Settings.Designer.cs | 30 + .../DCT_test/Properties/Settings.settings | 7 + DCT_test/README.md | 450 +++++++++++ DCT_test/Run_DCT_test.bat | 2 + DCT_test/build_csharp.bat | 9 + 20 files changed, 2832 insertions(+), 1 deletion(-) delete mode 160000 DCT_test create mode 100644 DCT_test/DCT_test.slnx create mode 100644 DCT_test/DCT_test/App.config create mode 100644 DCT_test/DCT_test/App.xaml create mode 100644 DCT_test/DCT_test/App.xaml.cs create mode 100644 DCT_test/DCT_test/ClientSession.cs create mode 100644 DCT_test/DCT_test/DCT_test.csproj create mode 100644 DCT_test/DCT_test/DctPacket.cs create mode 100644 DCT_test/DCT_test/DctTcpServer.cs create mode 100644 DCT_test/DCT_test/LogFileReader.cs create mode 100644 DCT_test/DCT_test/MainWindow.xaml create mode 100644 DCT_test/DCT_test/MainWindow.xaml.cs create mode 100644 DCT_test/DCT_test/Properties/AssemblyInfo.cs create mode 100644 DCT_test/DCT_test/Properties/Resources.Designer.cs create mode 100644 DCT_test/DCT_test/Properties/Resources.resx create mode 100644 DCT_test/DCT_test/Properties/Settings.Designer.cs create mode 100644 DCT_test/DCT_test/Properties/Settings.settings create mode 100644 DCT_test/README.md create mode 100644 DCT_test/Run_DCT_test.bat create mode 100644 DCT_test/build_csharp.bat diff --git a/DCT_test b/DCT_test deleted file mode 160000 index 74d2da3..0000000 --- a/DCT_test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 74d2da3be96ca194382d4eb0eddf6145933a4587 diff --git a/DCT_test/DCT_test.slnx b/DCT_test/DCT_test.slnx new file mode 100644 index 0000000..60a5859 --- /dev/null +++ b/DCT_test/DCT_test.slnx @@ -0,0 +1,3 @@ + + + diff --git a/DCT_test/DCT_test/App.config b/DCT_test/DCT_test/App.config new file mode 100644 index 0000000..56efbc7 --- /dev/null +++ b/DCT_test/DCT_test/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DCT_test/DCT_test/App.xaml b/DCT_test/DCT_test/App.xaml new file mode 100644 index 0000000..0dffeef --- /dev/null +++ b/DCT_test/DCT_test/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/DCT_test/DCT_test/App.xaml.cs b/DCT_test/DCT_test/App.xaml.cs new file mode 100644 index 0000000..e3fdfc2 --- /dev/null +++ b/DCT_test/DCT_test/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace DCT_test +{ + /// + /// App.xaml에 대한 상호 작용 논리 + /// + public partial class App : Application + { + } +} diff --git a/DCT_test/DCT_test/ClientSession.cs b/DCT_test/DCT_test/ClientSession.cs new file mode 100644 index 0000000..ddfabd3 --- /dev/null +++ b/DCT_test/DCT_test/ClientSession.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Sockets; + +namespace DCT_test +{ + public class ClientSession + { + public TcpClient Client { get; set; } + public string IpPort { get; set; } + public string ConnectTime { get; set; } + public uint SentPackets { get; set; } + public uint ReceivedPackets { get; set; } + public long FirmwareReceivedBytes { get; set; } + public string FirmwareUploadName { get; set; } + + public ClientSession(TcpClient client, string ipPort) + { + Client = client; + IpPort = ipPort; + ConnectTime = DateTime.Now.ToString("HH:mm:ss"); + SentPackets = 0; + ReceivedPackets = 0; + FirmwareReceivedBytes = 0; + FirmwareUploadName = string.Empty; + } + } +} diff --git a/DCT_test/DCT_test/DCT_test.csproj b/DCT_test/DCT_test/DCT_test.csproj new file mode 100644 index 0000000..8350cee --- /dev/null +++ b/DCT_test/DCT_test/DCT_test.csproj @@ -0,0 +1,102 @@ + + + + + Debug + AnyCPU + {2A257DC9-45C2-4539-9992-57340A291C74} + WinExe + DCT_test + DCT_test + v4.7.2 + 512 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 4 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + 4.0 + + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + App.xaml + Code + + + + + + + MainWindow.xaml + Code + + + + + Code + + + True + True + Resources.resx + + + True + Settings.settings + True + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + \ No newline at end of file diff --git a/DCT_test/DCT_test/DctPacket.cs b/DCT_test/DCT_test/DctPacket.cs new file mode 100644 index 0000000..078f1cb --- /dev/null +++ b/DCT_test/DCT_test/DctPacket.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; + +namespace DCT_test +{ + /// + /// DCT 표준 통신 프로토콜 바이너리 코덱 (PC C++ MainFrm 및 Android DctPacketCodec과 100% 동일) + /// 프레임 포맷: [Header(0xA1)] [Len_H(1B)] [Len_L(1B)] [CmdID(1B)] [Payload(N B)] [Checksum(1B)] [Footer(0x7E)] + /// - packLen = Payload.Length + 1 (CmdID 1바이트 포함) + /// - TotalLength = packLen + 5 (Header 1B + Len 2B + Checksum 1B + Footer 1B) + /// - Checksum = (sum(frame[0 .. TotalLength - 3]) ^ 0xFF) & 0xFF (1의 보수 NOT 누적합) + /// + public class DctPacket + { + public const byte START_BYTE = 0xA1; + public const byte END_BYTE = 0x7E; + public const int OVERHEAD_LENGTH = 5; // START(1) + LenH(1) + LenL(1) + Checksum(1) + END(1) + + public byte CommandId { get; set; } + public ushort Sequence { get; set; } + public byte[] Payload { get; set; } = new byte[0]; + + public ushort PackLen => (ushort)(Payload.Length + 1); // CmdID 1바이트 포함 + public int TotalLength => PackLen + OVERHEAD_LENGTH; + + public static DctPacket Create(byte cmdId, byte[] payload = null, ushort seq = 0) + { + return new DctPacket + { + CommandId = cmdId, + Sequence = seq, + Payload = payload ?? new byte[0] + }; + } + + public byte[] Encode() + { + ushort packLen = PackLen; + int totalLen = TotalLength; + byte[] frame = new byte[totalLen]; + + // 1. Header & Length (Big-Endian) + frame[0] = START_BYTE; + frame[1] = (byte)((packLen >> 8) & 0xFF); + frame[2] = (byte)(packLen & 0xFF); + + // 2. Command ID + frame[3] = CommandId; + + // 3. Payload + if (Payload != null && Payload.Length > 0) + { + Buffer.BlockCopy(Payload, 0, frame, 4, Payload.Length); + } + + // 4. Checksum (8비트 1의 보수 누적합: sum ^ 0xFF) + int sum = 0; + for (int i = 0; i < totalLen - 2; i++) + { + sum = (sum + frame[i]) & 0xFF; + } + frame[totalLen - 2] = (byte)((sum ^ 0xFF) & 0xFF); + + // 5. Footer + frame[totalLen - 1] = END_BYTE; + + return frame; + } + + public static List ParseStream(List buffer) + { + var packets = new List(); + + while (buffer.Count >= 6) // 최소 패킷 크기: START(1)+Len(2)+Cmd(1)+Chk(1)+END(1) = 6 + { + if (buffer[0] != START_BYTE) + { + buffer.RemoveAt(0); + continue; + } + + ushort packLen = (ushort)((buffer[1] << 8) | buffer[2]); + int totalLength = packLen + OVERHEAD_LENGTH; + + if (packLen < 1 || totalLength > 65535) + { + buffer.RemoveAt(0); + continue; + } + + if (buffer.Count < totalLength) + { + break; // 패킷 전체가 올 때까지 대기 + } + + if (buffer[totalLength - 1] != END_BYTE) + { + buffer.RemoveAt(0); + continue; + } + + // 체크섬 검증 + int sum = 0; + for (int i = 0; i < totalLength - 2; i++) + { + sum = (sum + buffer[i]) & 0xFF; + } + byte expectedChk = (byte)((sum ^ 0xFF) & 0xFF); + if (buffer[totalLength - 2] != expectedChk) + { + buffer.RemoveAt(0); + continue; + } + + byte cmdId = buffer[3]; + int payloadSize = packLen - 1; + byte[] payload = new byte[payloadSize]; + + if (payloadSize > 0) + { + buffer.CopyTo(4, payload, 0, payloadSize); + } + + packets.Add(new DctPacket + { + CommandId = cmdId, + Payload = payload + }); + + buffer.RemoveRange(0, totalLength); + } + + return packets; + } + } +} diff --git a/DCT_test/DCT_test/DctTcpServer.cs b/DCT_test/DCT_test/DctTcpServer.cs new file mode 100644 index 0000000..a781279 --- /dev/null +++ b/DCT_test/DCT_test/DctTcpServer.cs @@ -0,0 +1,764 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DCT_test +{ + public class DctTcpServer + { + private TcpListener _listener; + private CancellationTokenSource _cts; + private readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); + private readonly object _xcpConfigLock = new object(); + private byte[] _xcpMeasurementTypes = new byte[] { 3, 3, 2, 0 }; + private bool _xcpIsMsbFirst; + private readonly string[] _maintenanceLogNames = { "TEST_MDB_LOG.dat", "TEST_CAN_LOG.dat" }; + private readonly byte[][] _maintenanceLogData = + { + Encoding.ASCII.GetBytes("DCT TEST MDB LOG\r\n0001,12.5\r\n0002,13.0\r\n"), + Encoding.ASCII.GetBytes("DCT TEST CAN LOG\r\n0001,7E8,8,0102030405060708\r\n") + }; + + public bool IsRunning { get; private set; } + public int Port { get; private set; } = 6000; + + public ulong TotalCanAcks { get; private set; } + public ulong TotalSmAcks { get; private set; } + public ulong TotalXcpAcks { get; private set; } + + public bool ShowRxPackets { get; set; } = true; + public bool ShowHexDump { get; set; } = false; + + public event Action OnLogMessage; + public event Action OnClientConnected; + public event Action OnClientDisconnected; + public event Action OnClientListChanged; + public event Action OnStartSimRequested; + public event Action OnStopSimRequested; + + public ICollection ActiveClients => _clients.Values; + + /// + /// 마지막 0x41/0x42 설정과 같은 count/type/endian으로 PC 호환 0xD0 payload를 생성한다. + /// rawValues가 부족한 나머지 항목은 0으로 채운다. + /// + public byte[] BuildXcpDaqPayload(params double[] rawValues) + { + byte[] types; + bool isMsbFirst; + lock (_xcpConfigLock) + { + types = (byte[])_xcpMeasurementTypes.Clone(); + isMsbFirst = _xcpIsMsbFirst; + } + + var payload = new List(2 + types.Length * 5); + payload.Add((byte)((types.Length >> 8) & 0xFF)); + payload.Add((byte)(types.Length & 0xFF)); + for (int i = 0; i < types.Length; i++) + { + payload.Add(types[i]); + double value = i < rawValues.Length ? rawValues[i] : 0.0; + AppendXcpValue(payload, types[i], value, isMsbFirst); + } + return payload.ToArray(); + } + + public bool Start(int port) + { + if (IsRunning) return true; + + try + { + Port = port; + _cts = new CancellationTokenSource(); + _listener = new TcpListener(IPAddress.Any, port); + _listener.Start(10); + IsRunning = true; + + var ips = GetActiveIpAddresses(); + string primaryIp = ips.FirstOrDefault() ?? "127.0.0.1"; + Log($"[OK] C# DCT TCP 서버가 포트 {port}에서 대기 중입니다. (태블릿 접속용 IP: {primaryIp}:{port})"); + Task.Run(() => AcceptClientsAsync(_cts.Token)); + Task.Run(() => BroadcastBeaconAsync(port, _cts.Token)); + return true; + } + catch (Exception ex) + { + Log($"[ERROR] 서버 시작 실패: {ex.Message}"); + Stop(); + return false; + } + } + + public void Stop() + { + if (!IsRunning) return; + + IsRunning = false; + _cts?.Cancel(); + + try + { + _listener?.Stop(); + } + catch { } + + foreach (var kv in _clients) + { + try + { + kv.Value.Client?.Close(); + } + catch { } + } + _clients.Clear(); + + OnClientListChanged?.Invoke(); + Log("[INFO] TCP 서버가 중지되었습니다."); + } + + public void BroadcastPacket(DctPacket packet) + { + if (!IsRunning || _clients.IsEmpty) return; + + byte[] encoded = packet.Encode(); + foreach (var kv in _clients) + { + var session = kv.Value; + try + { + if (session.Client.Connected) + { + var stream = session.Client.GetStream(); + stream.Write(encoded, 0, encoded.Length); + session.SentPackets++; + } + } + catch + { + // Handled in read loop + } + } + } + + private async Task BroadcastBeaconAsync(int port, CancellationToken ct) + { + string hostName = Dns.GetHostName(); + + while (!ct.IsCancellationRequested && IsRunning) + { + try + { + var activeIps = GetActiveIpAddresses(); + + // 1. 기본 라우트를 통한 글로벌 255.255.255.255 브로드캐스트 + try + { + using (var globalUdp = new UdpClient()) + { + globalUdp.EnableBroadcast = true; + string primaryIp = activeIps.FirstOrDefault() ?? "127.0.0.1"; + string beacon = $"DCT_BEACON|DCT PC Server ({hostName})|{primaryIp}|{port}"; + byte[] bytes = Encoding.UTF8.GetBytes(beacon); + await globalUdp.SendAsync(bytes, bytes.Length, new IPEndPoint(IPAddress.Broadcast, 6005)); + } + } + catch { } + + // 2. 각 네트워크 어댑터별 서브넷 브로드캐스트 전송 + foreach (var ip in activeIps) + { + if (IPAddress.TryParse(ip, out var localAddr)) + { + try + { + using (var udp = new UdpClient(new IPEndPoint(localAddr, 0))) + { + udp.EnableBroadcast = true; + string beacon = $"DCT_BEACON|DCT PC Server ({hostName})|{ip}|{port}"; + byte[] bytes = Encoding.UTF8.GetBytes(beacon); + + try { await udp.SendAsync(bytes, bytes.Length, new IPEndPoint(IPAddress.Broadcast, 6005)); } catch { } + + var parts = ip.Split('.'); + if (parts.Length == 4) + { + string subnetBroadcast = $"{parts[0]}.{parts[1]}.{parts[2]}.255"; + if (IPAddress.TryParse(subnetBroadcast, out var subIp)) + { + try { await udp.SendAsync(bytes, bytes.Length, new IPEndPoint(subIp, 6005)); } catch { } + } + } + } + } + catch { } + } + } + } + catch { } + + await Task.Delay(1000, ct); + } + } + + public static List GetActiveIpAddresses() + { + var result = new List(); + try + { + var wifiIps = new List(); + var ethIps = new List(); + + foreach (var ni in NetworkInterface.GetAllNetworkInterfaces()) + { + if (ni.OperationalStatus != OperationalStatus.Up) continue; + if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue; + + string desc = ni.Description.ToLower(); + string name = ni.Name.ToLower(); + if (desc.Contains("vmware") || desc.Contains("virtual") || name.Contains("vethernet")) continue; + + var ipProps = ni.GetIPProperties(); + foreach (var addr in ipProps.UnicastAddresses) + { + if (addr.Address.AddressFamily == AddressFamily.InterNetwork) + { + string ipStr = addr.Address.ToString(); + if (ipStr.StartsWith("127.") || ipStr.StartsWith("169.254.")) continue; + + if (ni.NetworkInterfaceType == NetworkInterfaceType.Wireless80211 || name.Contains("wi-fi") || desc.Contains("wi-fi") || desc.Contains("wireless")) + { + wifiIps.Add(ipStr); + } + else + { + ethIps.Add(ipStr); + } + } + } + } + + result.AddRange(wifiIps); + result.AddRange(ethIps); + } + catch { } + + if (!result.Any()) result.Add("127.0.0.1"); + return result.Distinct().ToList(); + } + + private async Task AcceptClientsAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested && IsRunning) + { + try + { + var client = await _listener.AcceptTcpClientAsync(); + client.NoDelay = true; + + string ipPort = client.Client.RemoteEndPoint?.ToString() ?? "Unknown"; + var session = new ClientSession(client, ipPort); + _clients[ipPort] = session; + + Log($"[+] 태블릿 클라이언트 접속됨: {ipPort}"); + OnClientConnected?.Invoke(session); + OnClientListChanged?.Invoke(); + + _ = Task.Run(() => HandleClientAsync(session, ct), ct); + } + catch (ObjectDisposedException) + { + break; + } + catch (Exception ex) + { + if (!ct.IsCancellationRequested) + { + Log($"[WARN] Accept 오류: {ex.Message}"); + } + } + } + } + + private async Task HandleClientAsync(ClientSession session, CancellationToken ct) + { + var buffer = new List(); + byte[] readBuffer = new byte[4096]; + + try + { + var stream = session.Client.GetStream(); + while (!ct.IsCancellationRequested && session.Client.Connected) + { + int bytesRead = await stream.ReadAsync(readBuffer, 0, readBuffer.Length, ct); + if (bytesRead <= 0) break; + + for (int i = 0; i < bytesRead; i++) + { + buffer.Add(readBuffer[i]); + } + + session.ReceivedPackets++; + + var packets = DctPacket.ParseStream(buffer); + foreach (var pkt in packets) + { + HandlePacket(session, pkt); + } + + OnClientListChanged?.Invoke(); + } + } + catch + { + // Disconnected + } + finally + { + _clients.TryRemove(session.IpPort, out _); + try { session.Client.Close(); } catch { } + Log($"[-] 클라이언트 접속 해제: {session.IpPort}"); + OnClientDisconnected?.Invoke(session.IpPort); + OnClientListChanged?.Invoke(); + } + } + + private void HandlePacket(ClientSession session, DctPacket packet) + { + string cmdName = GetCommandName(packet.CommandId); + string rxLog = null; + + if (ShowRxPackets) + { + rxLog = $"[📥 RX <- {session.IpPort}] #{packet.Sequence:D5} | 0x{packet.CommandId:X2} ({cmdName}) | Payload: {packet.Payload.Length}B"; + if (ShowHexDump) + { + string hex = BitConverter.ToString(packet.Encode()).Replace("-", " "); + rxLog += $"\r\n └ [Raw RX]: {hex}"; + } + } + + switch (packet.CommandId) + { + case 0x00: // CMD_ENABLE + SendPacket(session, DctPacket.Create(0x80, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[HANDSHAKE] CMD_ENABLE (0x00) -> [SEND] RES_ENABLE (0x80, ACK: 0x01)"); + break; + + case 0x01: // CMD_VERSION + // PC RES_VERSION payload: [length:1][ASCII version:length], no trailing NUL. + byte[] verBytes = Encoding.ASCII.GetBytes("DCT-MOBI-v1.0.0"); + byte[] verPayload = new byte[verBytes.Length + 1]; + verPayload[0] = checked((byte)verBytes.Length); + Buffer.BlockCopy(verBytes, 0, verPayload, 1, verBytes.Length); + SendPacket(session, DctPacket.Create(0x81, verPayload, packet.Sequence)); + LogRx(rxLog, $"[HANDSHAKE] CMD_VERSION (0x01) -> [SEND] RES_VERSION (0x81, Len: {verBytes.Length}, 'DCT-MOBI-v1.0.0')"); + break; + + case 0x03: // CMD_HWID + byte[] hwBytes = new byte[] { 0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E }; + SendPacket(session, DctPacket.Create(0x83, hwBytes, packet.Sequence)); + LogRx(rxLog, $"[HANDSHAKE] CMD_HWID (0x03) -> [SEND] RES_HWID (0x83, 00:1A:2B:3C:4D:5E)"); + break; + + case 0x04: // CMD_SYNC_TIME + SendPacket(session, DctPacket.Create(0x84, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[HANDSHAKE] CMD_SYNC_TIME (0x04) -> [SEND] RES_SYNC_TIME (0x84, ACK: 0x01)"); + break; + + case 0x06: // CMD_CARTYPE + string carType = "Unknown"; + if (packet.Payload.Length > 1) + { + int len = packet.Payload[0]; + if (len > 0 && len < packet.Payload.Length) + carType = Encoding.ASCII.GetString(packet.Payload, 1, len); + } + SendPacket(session, DctPacket.Create(0x86, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_CARTYPE (0x06, CarType: '{carType}') -> [SEND] RES_CARTYPE (0x86, 0x01)"); + break; + + case 0x07: // CMD_CARTYPE_CONTROL + SendPacket(session, DctPacket.Create(0x87, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_CARTYPE_CONTROL (0x07) -> [SEND] RES_CARTYPE_CONTROL (0x87, 0x01)"); + break; + + case 0x08: // CMD_DB + SendPacket(session, DctPacket.Create(0x88, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_DB (0x08) -> [SEND] RES_DB (0x88, 0x01)"); + break; + + case 0x09: // CMD_DCT_ENV + SendPacket(session, DctPacket.Create(0x89, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_DCT_ENV (0x09) -> [SEND] RES_DCT_ENV (0x89, 0x01)"); + break; + + case 0x0F: // CMD_DISABLE + SendPacket(session, DctPacket.Create(0x8F, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[HANDSHAKE] CMD_DISABLE (0x0F) -> [SEND] RES_DISABLE (0x8F, 0x01)"); + break; + + case 0x10: // CMD_MDB_INFO + SendPacket(session, DctPacket.Create(0x90, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_MDB_INFO (0x10, Recl Info {packet.Payload.Length}B) -> [SEND] RES_MDB_INFO (0x90, 0x01)"); + break; + + case 0x11: // CMD_CANDBC_INFO + SendPacket(session, DctPacket.Create(0x91, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_CANDBC_INFO (0x11, CAN IDs {packet.Payload.Length}B) -> [SEND] RES_CANDBC_INFO (0x91, 0x01)"); + break; + + case 0x12: // CMD_SM_INFO + SendPacket(session, DctPacket.Create(0x92, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_SM_INFO (0x12, Sensor Module {packet.Payload.Length}B) -> [SEND] RES_SM_INFO (0x92, 0x01)"); + break; + + case 0x13: // CMD_MDB_SELECTED_ID + SendPacket(session, DctPacket.Create(0x93, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONFIG] CMD_MDB_SELECTED_ID (0x13, Monitored IDs {packet.Payload.Length}B) -> [SEND] RES_MDB_SELECTED_ID (0x93, 0x01)"); + break; + + case 0x14: // CMD_COMPENSATION_TYPE_DATA + SendPacket(session, DctPacket.Create(0x94, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CALIB] CMD_COMPENSATION_TYPE_DATA (0x14) -> [SEND] RES_COMPENSATION_TYPE_DATA (0x94, 0x01)"); + break; + + case 0x15: // CMD_DID_INFO + SendPacket(session, DctPacket.Create(0x95, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[DID] CMD_DID_INFO (0x15) -> [SEND] RES_DID_INFO (0x95, 0x01)"); + break; + + case 0x16: // CMD_REQ_DID_INFO + SendPacket(session, DctPacket.Create(0x96, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[DID] CMD_REQ_DID_INFO (0x16) -> [SEND] RES_REQ_DID_INFO (0x96, 0x01)"); + break; + + case 0x17: // CMD_SETUP_DID_INFO + SendPacket(session, DctPacket.Create(0x97, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[DID] CMD_SETUP_DID_INFO (0x17) -> [SEND] RES_SETUP_DID_INFO (0x97, 0x01)"); + break; + + case 0x18: // CMD_REQ_ALL_DID_INFO + SendPacket(session, DctPacket.Create(0x98, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[DID] CMD_REQ_ALL_DID_INFO (0x18) -> [SEND] RES_REQ_ALL_DID_INFO (0x98, 0x01)"); + break; + + case 0x20: // CMD_START_TRANSMIT_DATA + SendPacket(session, DctPacket.Create(0xA0, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONTROL] CMD_START_TRANSMIT_DATA (0x20) -> [SEND] RES_START_TRANSMIT_DATA (0xA0, 0x01) ACK 및 스트리밍 개시"); + OnStartSimRequested?.Invoke(); + break; + + case 0x21: // CMD_STOP_TRANSMIT_DATA + SendPacket(session, DctPacket.Create(0xA1, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[CONTROL] CMD_STOP_TRANSMIT_DATA (0x21) -> [SEND] RES_STOP_TRANSMIT_DATA (0xA1, 0x01) ACK 및 스트리밍 정지"); + OnStopSimRequested?.Invoke(); + break; + + case 0x23: // CMD_CAN_FRAME_ACK + TotalCanAcks++; + if (ShowRxPackets && TotalCanAcks % 20 == 1) + Log($"[📥 RX ACK <- {session.IpPort}] CMD_CAN_FRAME_ACK (0x23) 누적 {TotalCanAcks}건 수신"); + break; + + case 0x24: // CMD_SM_DATA_ACK + TotalSmAcks++; + if (ShowRxPackets && TotalSmAcks % 20 == 1) + Log($"[📥 RX ACK <- {session.IpPort}] CMD_SM_DATA_ACK (0x24) 누적 {TotalSmAcks}건 수신"); + break; + + case 0x25: // CMD_SAVED_LOGFILE_INFO + HandleSavedLogInfo(session, packet); + break; + + case 0x26: // CMD_SAVED_LOGFILE_COPY + HandleSavedLogCopy(session, packet); + break; + + case 0x31: // CMD_FW_INFO + HandleFirmwareInfo(session, packet); + break; + + case 0x32: // CMD_FW_KEY_VALUE (host response; no ACK) + LogRx(rxLog, $"[P3 ASK] CMD_FW_KEY_VALUE (0x32, {packet.Payload.Length}B) 수신"); + break; + + case 0x40: // CMD_XCP_USE + SendPacket(session, DctPacket.Create(0xC0, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[XCP] CMD_XCP_USE (0x40, Use: {packet.Payload.FirstOrDefault()}) -> [SEND] RES_XCP_USE (0xC0, 0x01)"); + break; + + case 0x41: // CMD_XCP_IFDATAS + if (packet.Payload.Length > 0) + { + lock (_xcpConfigLock) _xcpIsMsbFirst = packet.Payload[0] == 0x01; + } + SendPacket(session, DctPacket.Create(0xC1, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[XCP] CMD_XCP_IFDATAS (0x41, CAN IDs {packet.Payload.Length}B) -> [SEND] RES_XCP_IFDATAS (0xC1, 0x01)"); + break; + + case 0x42: // CMD_XCP_MEASUREMENTS + bool xcpConfigOk = TryApplyXcpMeasurements(packet.Payload, out int xcpItemCount); + SendPacket(session, DctPacket.Create(0xC2, new byte[] { xcpConfigOk ? (byte)0x01 : (byte)0x00 }, packet.Sequence)); + LogRx(rxLog, $"[XCP] CMD_XCP_MEASUREMENTS (0x42, Items {xcpItemCount}, {packet.Payload.Length}B) -> [SEND] RES_XCP_MEASUREMENTS (0xC2, 0x{(xcpConfigOk ? 1 : 0):X2})"); + break; + + case 0x50: // CMD_XCP_MEASUREMENT_DATA (Host ACK) + TotalXcpAcks++; + break; + + case 0x51: // CMD_XCP_CHARACTERISTIC_DATA + SendPacket(session, DctPacket.Create(0xD1, new byte[] { 0x01 }, packet.Sequence)); + LogRx(rxLog, $"[XCP] CMD_XCP_CHARACTERISTIC_DATA (0x51) -> [SEND] RES_XCP_CHARACTERISTIC_DATA (0xD1, 0x01)"); + break; + + case 0x52: // CMD_XCP_REQ_CHARACTERISTIC_DATA + SendPacket(session, DctPacket.Create(0xD2, new byte[] { 0x00, 0x00 }, packet.Sequence)); + LogRx(rxLog, $"[XCP] CMD_XCP_REQ_CHARACTERISTIC_DATA (0x52) -> [SEND] RES_XCP_REQ_CHARACTERISTIC_DATA (0xD2)"); + break; + + default: + LogRx(rxLog, $"[📥 RX <- {session.IpPort}] 0x{packet.CommandId:X2} ({cmdName}, Payload: {packet.Payload.Length}B)"); + break; + } + } + + private void LogRx(string rxDetail, string actionLog) + { + if (ShowRxPackets && !string.IsNullOrEmpty(rxDetail)) + { + Log(rxDetail); + } + Log(actionLog); + } + + private void HandleSavedLogInfo(ClientSession session, DctPacket packet) + { + if (packet.Payload.Length < 1) return; + byte sub = packet.Payload[0]; + if (sub == 0x01) + { + int count = _maintenanceLogNames.Length; + SendPacket(session, DctPacket.Create(0xA5, new byte[] { 0x01, (byte)(count >> 8), (byte)count }, packet.Sequence)); + Log($"[P3 LOG] 파일 수 {count} 응답"); + } + else if (sub == 0x02 && packet.Payload.Length >= 4) + { + int index = (packet.Payload[1] << 8) | packet.Payload[2]; + if (index < 0 || index >= _maintenanceLogNames.Length) return; + byte[] name = Encoding.ASCII.GetBytes(_maintenanceLogNames[index]); + var response = new byte[4 + name.Length]; + response[0] = 0x02; + response[1] = (byte)(index >> 8); + response[2] = (byte)index; + response[3] = (byte)name.Length; + Buffer.BlockCopy(name, 0, response, 4, name.Length); + SendPacket(session, DctPacket.Create(0xA5, response, packet.Sequence)); + } + } + + private void HandleSavedLogCopy(ClientSession session, DctPacket packet) + { + if (packet.Payload.Length < 3) return; + byte sub = packet.Payload[0]; + int index = (packet.Payload[1] << 8) | packet.Payload[2]; + if (index < 0 || index >= _maintenanceLogData.Length) return; + byte[] source = _maintenanceLogData[index]; + if (sub == 0x01) + { + int size = source.Length; + byte[] response = + { + 0x01, (byte)(index >> 8), (byte)index, + (byte)(size >> 24), (byte)(size >> 16), (byte)(size >> 8), (byte)size + }; + SendPacket(session, DctPacket.Create(0xA6, response, packet.Sequence)); + } + else if (sub == 0x02 && packet.Payload.Length >= 9) + { + long offset = ((long)packet.Payload[3] << 24) | ((long)packet.Payload[4] << 16) | + ((long)packet.Payload[5] << 8) | packet.Payload[6]; + int requested = (packet.Payload[7] << 8) | packet.Payload[8]; + if (offset < 0 || offset > source.Length || requested <= 0) return; + int length = Math.Min(requested, source.Length - (int)offset); + byte status = offset + length >= source.Length ? (byte)0x03 : (byte)0x01; + var response = new byte[6 + length]; + response[0] = 0x02; + response[1] = (byte)(index >> 8); + response[2] = (byte)index; + response[3] = (byte)(length >> 8); + response[4] = (byte)length; + response[5] = status; + Buffer.BlockCopy(source, (int)offset, response, 6, length); + SendPacket(session, DctPacket.Create(0xA6, response, packet.Sequence)); + } + } + + private void HandleFirmwareInfo(ClientSession session, DctPacket packet) + { + if (packet.Payload.Length < 1) return; + byte sub = packet.Payload[0]; + if (sub == 0x04) + { + SendPacket(session, DctPacket.Create(0xB1, new byte[] { 0x04, 0x00, 0x00 }, packet.Sequence)); + Log("[P3 FW] 저장 펌웨어 수 0 응답"); + } + else if (sub == 0x02 && packet.Payload.Length >= 7) + { + int slot = packet.Payload[1]; + int nameLength = packet.Payload[6]; + if (packet.Payload.Length < 7 + nameLength) return; + session.FirmwareUploadName = Encoding.ASCII.GetString(packet.Payload, 7, nameLength); + session.FirmwareReceivedBytes = 0; + SendPacket(session, DctPacket.Create(0xB1, new byte[] { 0x02, (byte)slot, 0x01 }, packet.Sequence)); + Log($"[P3 FW] '{session.FirmwareUploadName}' 메타데이터 승인"); + } + else if (sub == 0x05 && packet.Payload.Length >= 7) + { + int slot = packet.Payload[1]; + int dataLength = (packet.Payload[5] << 8) | packet.Payload[6]; + if (dataLength < 1 || dataLength > 512 || packet.Payload.Length != 7 + dataLength) return; + session.FirmwareReceivedBytes += dataLength; + SendPacket(session, DctPacket.Create(0xB1, new byte[] { 0x05, (byte)slot, 0x01 }, packet.Sequence)); + if (packet.Payload[4] == 0x02) + Log($"[P3 FW] '{session.FirmwareUploadName}' 테스트 수신 완료: {session.FirmwareReceivedBytes} bytes"); + } + } + + private bool TryApplyXcpMeasurements(byte[] payload, out int itemCount) + { + itemCount = 0; + if (payload == null || payload.Length < 2) return false; + itemCount = (payload[0] << 8) | payload[1]; + if (payload.Length != 2 + itemCount * 5) return false; + + var types = new byte[itemCount]; + for (int i = 0; i < itemCount; i++) + { + byte type = payload[2 + i * 5]; + if (GetXcpTypeSize(type) == 0) return false; + types[i] = type; + } + lock (_xcpConfigLock) _xcpMeasurementTypes = types; + return true; + } + + private static int GetXcpTypeSize(byte type) + { + switch (type) + { + case 0: case 1: return 1; + case 2: case 3: case 8: return 2; + case 4: case 5: case 9: case 11: return 4; + case 6: case 7: case 10: return 8; + default: return 0; + } + } + + private static void AppendXcpValue(List output, byte type, double value, bool isMsbFirst) + { + byte[] bytes; + switch (type) + { + case 0: bytes = new byte[] { (byte)Math.Max(0, Math.Min(255, Math.Round(value))) }; break; + case 1: bytes = new byte[] { unchecked((byte)(sbyte)Math.Max(sbyte.MinValue, Math.Min(sbyte.MaxValue, Math.Round(value)))) }; break; + case 2: bytes = BitConverter.GetBytes((ushort)Math.Max(0, Math.Min(ushort.MaxValue, Math.Round(value)))); break; + case 3: bytes = BitConverter.GetBytes((short)Math.Max(short.MinValue, Math.Min(short.MaxValue, Math.Round(value)))); break; + case 4: bytes = BitConverter.GetBytes((uint)Math.Max(0, Math.Min(uint.MaxValue, Math.Round(value)))); break; + case 5: bytes = BitConverter.GetBytes((int)Math.Max(int.MinValue, Math.Min(int.MaxValue, Math.Round(value)))); break; + case 6: bytes = BitConverter.GetBytes((ulong)Math.Max(0, Math.Round(value))); break; + case 7: bytes = BitConverter.GetBytes((long)Math.Round(value)); break; + case 8: bytes = BitConverter.GetBytes(FloatToHalf((float)value)); break; + case 9: case 11: bytes = BitConverter.GetBytes((float)value); break; + case 10: bytes = BitConverter.GetBytes(value); break; + default: throw new ArgumentOutOfRangeException(nameof(type)); + } + if (bytes.Length > 1 && (BitConverter.IsLittleEndian == isMsbFirst)) Array.Reverse(bytes); + output.AddRange(bytes); + } + + private static ushort FloatToHalf(float value) + { + uint bits = BitConverter.ToUInt32(BitConverter.GetBytes(value), 0); + uint sign = (bits >> 16) & 0x8000; + int exponent = (int)((bits >> 23) & 0xFF) - 127 + 15; + uint mantissa = bits & 0x7FFFFF; + if (exponent <= 0) return (ushort)sign; + if (exponent >= 31) return (ushort)(sign | 0x7C00); + return (ushort)(sign | ((uint)exponent << 10) | (mantissa >> 13)); + } + + private void SendPacket(ClientSession session, DctPacket pkt) + { + try + { + if (session.Client.Connected) + { + byte[] encoded = pkt.Encode(); + var stream = session.Client.GetStream(); + stream.Write(encoded, 0, encoded.Length); + session.SentPackets++; + } + } + catch { } + } + + public static string GetCommandName(byte cmd) + { + switch (cmd) + { + case 0x00: return "CMD_ENABLE"; + case 0x01: return "CMD_VERSION"; + case 0x02: return "CMD_CUR_STATUS"; + case 0x03: return "CMD_HWID"; + case 0x04: return "CMD_SYNC_TIME"; + case 0x05: return "CMD_POSSIBLE_TO_CONNECT"; + case 0x06: return "CMD_CARTYPE"; + case 0x07: return "CMD_CARTYPE_CONTROL"; + case 0x08: return "CMD_DB"; + case 0x09: return "CMD_DCT_ENV"; + case 0x0F: return "CMD_DISABLE"; + case 0x10: return "CMD_MDB_INFO"; + case 0x11: return "CMD_CANDBC_INFO"; + case 0x12: return "CMD_SM_INFO"; + case 0x13: return "CMD_MDB_SELECTED_ID"; + case 0x14: return "CMD_COMPENSATION_TYPE_DATA"; + case 0x15: return "CMD_DID_INFO"; + case 0x16: return "CMD_REQ_DID_INFO"; + case 0x17: return "CMD_SETUP_DID_INFO"; + case 0x18: return "CMD_REQ_ALL_DID_INFO"; + case 0x20: return "CMD_START_TRANSMIT_DATA"; + case 0x21: return "CMD_STOP_TRANSMIT_DATA"; + case 0x22: return "CMD_CAN_FRAME_VIA_DCT"; + case 0x23: return "CMD_CAN_FRAME_ACK"; + case 0x24: return "CMD_SM_DATA_ACK"; + case 0x25: return "CMD_SAVED_LOGFILE_INFO"; + case 0x26: return "CMD_SAVED_LOGFILE_COPY"; + case 0x27: return "CMD_MDB_CONSTANT_DATA"; + case 0x28: return "CMD_MDB_REQ_CONSTANT_DATA"; + case 0x31: return "CMD_FW_INFO"; + case 0x32: return "CMD_FW_KEY_VALUE"; + case 0x40: return "CMD_XCP_USE"; + case 0x41: return "CMD_XCP_IFDATAS"; + case 0x42: return "CMD_XCP_MEASUREMENTS"; + case 0x50: return "CMD_XCP_MEASUREMENT_DATA"; + case 0x51: return "CMD_XCP_CHARACTERISTIC_DATA"; + case 0x52: return "CMD_XCP_REQ_CHARACTERISTIC_DATA"; + default: return $"UNKNOWN_0x{cmd:X2}"; + } + } + + private void Log(string message) + { + OnLogMessage?.Invoke(message); + } + } +} diff --git a/DCT_test/DCT_test/LogFileReader.cs b/DCT_test/DCT_test/LogFileReader.cs new file mode 100644 index 0000000..e7b3b85 --- /dev/null +++ b/DCT_test/DCT_test/LogFileReader.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace DCT_test +{ + /// + /// 파일 로드 결과 데이터 구조체 + /// + public class LogFileReadResult + { + public bool Success { get; set; } + public string FileName { get; set; } + public double FileSizeMb { get; set; } + public int TotalRecords => Packets.Count; + + /// + /// 재생 시 전송될 바이너리 패킷 목록 (DctPacket.Create(cmd, payload, seq)) + /// + public List Packets { get; set; } = new List(); + + /// + /// UI '송신 중인 신호 목록' 테이블에 표시할 신호 항목 목록 + /// + public List InitialSignalItems { get; set; } = new List(); + + public string ErrorMessage { get; set; } + } + + /// + /// 로그 파일 리더 (사용자 구현 가이드 템플릿) + /// + public class LogFileReader + { + public LogFileReadResult Load(string filePath) + { + var result = new LogFileReadResult + { + FileName = Path.GetFileName(filePath) + }; + + if (!File.Exists(filePath)) + { + result.ErrorMessage = $"파일이 존재하지 않습니다: {filePath}"; + return result; + } + + try + { + result.FileSizeMb = new FileInfo(filePath).Length / (1024.0 * 1024.0); + + // ========================================================================= + // [사용자 구현 가이드 영역] + // 1. filePath로부터 파일(CSV, TXT, MDF4, 바이너리 등)을 읽습니다. + // 2. 파싱한 신호명들을 result.InitialSignalItems에 추가합니다. + // 예: result.InitialSignalItems.Add(new TxSignalItem { Source = "[FILE]", Name = "신호명", Value = "0.0", Unit = "단위" }); + // 3. 각 시간/주기별 데이터를 DctPacket으로 인코딩하여 result.Packets에 추가합니다. + // 예: result.Packets.Add(DctPacket.Create(0xA3, payloadBytes, seq++)); + // ========================================================================= + + // TODO: 사용자 정의 파일 파싱 및 패킷 조립 로직을 이곳에 구현하세요. + + result.Success = result.Packets.Count > 0; + if (!result.Success && string.IsNullOrEmpty(result.ErrorMessage)) + { + result.ErrorMessage = "패킷 데이터가 생성되지 않았습니다."; + } + } + catch (Exception ex) + { + result.Success = false; + result.ErrorMessage = $"파일 파싱 오류: {ex.Message}"; + } + + return result; + } + } +} diff --git a/DCT_test/DCT_test/MainWindow.xaml b/DCT_test/DCT_test/MainWindow.xaml new file mode 100644 index 0000000..8d67da6 --- /dev/null +++ b/DCT_test/DCT_test/MainWindow.xaml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + +