Browse Source

Convert DCT_test from submodule to regular folder

master
seuuung 2 months ago
parent
commit
a62cebd17f
  1. 1
      DCT_test
  2. 3
      DCT_test/DCT_test.slnx
  3. 6
      DCT_test/DCT_test/App.config
  4. 9
      DCT_test/DCT_test/App.xaml
  5. 17
      DCT_test/DCT_test/App.xaml.cs
  6. 27
      DCT_test/DCT_test/ClientSession.cs
  7. 102
      DCT_test/DCT_test/DCT_test.csproj
  8. 136
      DCT_test/DCT_test/DctPacket.cs
  9. 764
      DCT_test/DCT_test/DctTcpServer.cs
  10. 78
      DCT_test/DCT_test/LogFileReader.cs
  11. 242
      DCT_test/DCT_test/MainWindow.xaml
  12. 710
      DCT_test/DCT_test/MainWindow.xaml.cs
  13. 52
      DCT_test/DCT_test/Properties/AssemblyInfo.cs
  14. 71
      DCT_test/DCT_test/Properties/Resources.Designer.cs
  15. 117
      DCT_test/DCT_test/Properties/Resources.resx
  16. 30
      DCT_test/DCT_test/Properties/Settings.Designer.cs
  17. 7
      DCT_test/DCT_test/Properties/Settings.settings
  18. 450
      DCT_test/README.md
  19. 2
      DCT_test/Run_DCT_test.bat
  20. 9
      DCT_test/build_csharp.bat

1
DCT_test

@ -1 +0,0 @@
Subproject commit 74d2da3be96ca194382d4eb0eddf6145933a4587

3
DCT_test/DCT_test.slnx

@ -0,0 +1,3 @@
<Solution>
<Project Path="DCT_test/DCT_test.csproj" Id="2a257dc9-45c2-4539-9992-57340a291c74" />
</Solution>

6
DCT_test/DCT_test/App.config

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

9
DCT_test/DCT_test/App.xaml

@ -0,0 +1,9 @@
<Application x:Class="DCT_test.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DCT_test"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

17
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
{
/// <summary>
/// App.xaml에 대한 상호 작용 논리
/// </summary>
public partial class App : Application
{
}
}

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

102
DCT_test/DCT_test/DCT_test.csproj

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2A257DC9-45C2-4539-9992-57340A291C74}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>DCT_test</RootNamespace>
<AssemblyName>DCT_test</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="DctPacket.cs" />
<Compile Include="LogFileReader.cs" />
<Compile Include="ClientSession.cs" />
<Compile Include="DctTcpServer.cs" />
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

136
DCT_test/DCT_test/DctPacket.cs

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
namespace DCT_test
{
/// <summary>
/// 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 누적합)
/// </summary>
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<DctPacket> ParseStream(List<byte> buffer)
{
var packets = new List<DctPacket>();
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;
}
}
}

764
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<string, ClientSession> _clients = new ConcurrentDictionary<string, ClientSession>();
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<string> OnLogMessage;
public event Action<ClientSession> OnClientConnected;
public event Action<string> OnClientDisconnected;
public event Action OnClientListChanged;
public event Action OnStartSimRequested;
public event Action OnStopSimRequested;
public ICollection<ClientSession> ActiveClients => _clients.Values;
/// <summary>
/// 마지막 0x41/0x42 설정과 같은 count/type/endian으로 PC 호환 0xD0 payload를 생성한다.
/// rawValues가 부족한 나머지 항목은 0으로 채운다.
/// </summary>
public byte[] BuildXcpDaqPayload(params double[] rawValues)
{
byte[] types;
bool isMsbFirst;
lock (_xcpConfigLock)
{
types = (byte[])_xcpMeasurementTypes.Clone();
isMsbFirst = _xcpIsMsbFirst;
}
var payload = new List<byte>(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<string> GetActiveIpAddresses()
{
var result = new List<string>();
try
{
var wifiIps = new List<string>();
var ethIps = new List<string>();
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>();
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<byte> 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);
}
}
}

78
DCT_test/DCT_test/LogFileReader.cs

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace DCT_test
{
/// <summary>
/// 파일 로드 결과 데이터 구조체
/// </summary>
public class LogFileReadResult
{
public bool Success { get; set; }
public string FileName { get; set; }
public double FileSizeMb { get; set; }
public int TotalRecords => Packets.Count;
/// <summary>
/// 재생 시 전송될 바이너리 패킷 목록 (DctPacket.Create(cmd, payload, seq))
/// </summary>
public List<DctPacket> Packets { get; set; } = new List<DctPacket>();
/// <summary>
/// UI '송신 중인 신호 목록' 테이블에 표시할 신호 항목 목록
/// </summary>
public List<TxSignalItem> InitialSignalItems { get; set; } = new List<TxSignalItem>();
public string ErrorMessage { get; set; }
}
/// <summary>
/// 로그 파일 리더 (사용자 구현 가이드 템플릿)
/// </summary>
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;
}
}
}

242
DCT_test/DCT_test/MainWindow.xaml

@ -0,0 +1,242 @@
<Window x:Class="DCT_test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DCT_test"
mc:Ignorable="d"
Title="DCT Hardware &amp; Wi-Fi Telemetry Test Server"
Height="820" Width="1120" MinHeight="650" MinWidth="920"
Background="#F1F5F9"
AllowDrop="True" Drop="Window_Drop" DragEnter="Window_DragEnter">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 1. Network & Server Control GroupBox -->
<GroupBox Grid.Row="0" Header=" 1. 네트워크 및 서버 제어 (Network &amp; Server Control) "
FontWeight="Bold" Foreground="#1E293B" Padding="8" Margin="0,0,0,8"
Background="White" BorderBrush="#CBD5E1">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="PC Wi-Fi IP:" FontWeight="SemiBold" VerticalAlignment="Center" Margin="4,0,8,0"/>
<ComboBox x:Name="ComboIp" Width="170" Height="26" VerticalContentAlignment="Center"
FontFamily="Consolas" FontSize="12" Margin="0,0,8,0"/>
<Button x:Name="BtnCopyIp" Content="IP 복사" Width="70" Height="26" Click="BtnCopyIp_Click" Margin="0,0,16,0"/>
<TextBlock Text="포트:" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,6,0"/>
<TextBox x:Name="TextPort" Text="6000" Width="60" Height="26" VerticalContentAlignment="Center"
FontFamily="Consolas" FontSize="12" Margin="0,0,16,0"/>
<Button x:Name="BtnToggleServer" Content="▶ 서버 시작 (Start Server)" Width="190" Height="30"
FontWeight="Bold" Background="#16A34A" Foreground="White" Click="BtnToggleServer_Click" Margin="0,0,16,0"/>
<TextBlock x:Name="TextServerStatus" Text="🔴 서버 중지됨 (Stopped)" FontWeight="Bold"
Foreground="#DC2626" VerticalAlignment="Center" FontSize="13"/>
</StackPanel>
</GroupBox>
<!-- 2. Data Source Mode Selection (모의신호 vs 파일재생) -->
<GroupBox Grid.Row="1" Header=" 2. 데이터 소스 및 모드 선택 (Data Source Mode) "
FontWeight="Bold" Foreground="#1E293B" Padding="8" Margin="0,0,0,8"
Background="White" BorderBrush="#CBD5E1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Mode Radio Buttons -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="4,0,0,8">
<RadioButton x:Name="RadioModeSim" Content="🔘 1. 실시간 모의신호 생성 모드 (Real-time Simulation)"
IsChecked="True" FontWeight="Bold" Foreground="#2563EB" VerticalAlignment="Center"
Margin="0,0,24,0" Checked="RadioMode_Checked"/>
<RadioButton x:Name="RadioModeFile" Content="📁 2. 로그 파일 재생 모드 (File Playback)"
IsChecked="False" FontWeight="Bold" Foreground="#475569" VerticalAlignment="Center"
Checked="RadioMode_Checked"/>
</StackPanel>
<!-- File Drop Zone (Enabled only in File mode) -->
<Grid Grid.Row="1" x:Name="PanelFileZone" Opacity="0.5" IsEnabled="False">
<Grid.RowDefinitions>
<RowDefinition Height="42"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="#94A3B8" BorderThickness="1.5" CornerRadius="4"
Background="#F8FAFC" Margin="0,0,0,6"
AllowDrop="True" Drop="DropZone_Drop" DragEnter="DropZone_DragEnter">
<TextBlock x:Name="TextDropTitle"
Text="📂 여기에 재생할 로그 파일(.csv, .txt, .dpc)을 끌어다 놓으세요 (Drag &amp; Drop)"
FontWeight="SemiBold" FontSize="11.5" Foreground="#64748B"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
<DockPanel Grid.Row="1" LastChildFill="True">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Button x:Name="BtnBrowse" Content="📁 로그 파일 찾기..." Width="120" Height="26" Click="BtnBrowse_Click" Margin="0,0,4,0"/>
<Button x:Name="BtnClearFile" Content="초기화" Width="60" Height="26" Click="BtnClearFile_Click"/>
</StackPanel>
<TextBlock x:Name="TextLoadedFile" Text="현재 모드: [ 실시간 모의신호 생성 모드 ]"
Foreground="#2563EB" FontWeight="SemiBold" VerticalAlignment="Center" FontSize="11.5"/>
</DockPanel>
</Grid>
</Grid>
</GroupBox>
<!-- 3. Telemetry & Playback Controls GroupBox -->
<GroupBox Grid.Row="2" Header=" 3. 모의신호 프로필 및 재생 제어 (Signal Profiles &amp; Playback) "
FontWeight="Bold" Foreground="#1E293B" Padding="8,6" Margin="0,0,0,8"
Background="White" BorderBrush="#CBD5E1">
<DockPanel LastChildFill="True" VerticalAlignment="Center">
<!-- Right Dock: Playback Action Buttons -->
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Button x:Name="BtnPlay" Content="▶ 재생" Width="75" Height="26" Background="#2563EB" Foreground="White" FontWeight="Bold" Click="BtnPlay_Click" Margin="0,0,4,0"/>
<Button x:Name="BtnPause" Content="⏸ 일시정지" Width="80" Height="26" Click="BtnPause_Click" Margin="0,0,4,0"/>
<Button x:Name="BtnStop" Content="⏹ 정지" Width="65" Height="26" Click="BtnStop_Click"/>
</StackPanel>
<!-- Left Controls: Profile, Rate, Speed, Loop -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="모의신호 프로필:" FontWeight="SemiBold" VerticalAlignment="Center" Margin="4,0,6,0"/>
<ComboBox x:Name="ComboProfile" Width="330" Height="26" SelectedIndex="0" Margin="0,0,12,0" SelectionChanged="ComboProfile_SelectionChanged">
<ComboBoxItem Content="1. [MDB + CAN] MDB 실차공조 &amp; CAN DBC 동시 스트리밍"/>
<ComboBoxItem Content="2. [A2L + CAN] A2L XCP 계측 &amp; CAN DBC 동시 스트리밍"/>
</ComboBox>
<TextBlock Text="주기:" VerticalAlignment="Center" Margin="0,0,4,0" FontWeight="Normal"/>
<ComboBox x:Name="ComboRate" Width="135" Height="24" SelectedIndex="2" Margin="0,0,8,0" SelectionChanged="ComboRate_SelectionChanged">
<ComboBoxItem Content="10 Hz (100ms)"/>
<ComboBoxItem Content="20 Hz (50ms)"/>
<ComboBoxItem Content="30 Hz (33ms, 최적)"/>
<ComboBoxItem Content="50 Hz (20ms)"/>
<ComboBoxItem Content="100 Hz (10ms)"/>
</ComboBox>
<TextBlock Text="배속:" VerticalAlignment="Center" Margin="0,0,4,0" FontWeight="Normal"/>
<ComboBox x:Name="ComboSpeed" Width="65" Height="24" SelectedIndex="1" Margin="0,0,8,0" SelectionChanged="ComboSpeed_SelectionChanged">
<ComboBoxItem Content="0.5 x"/>
<ComboBoxItem Content="1.0 x"/>
<ComboBoxItem Content="2.0 x"/>
<ComboBoxItem Content="5.0 x"/>
</ComboBox>
<CheckBox x:Name="CheckLoop" Content="반복 (Loop)" IsChecked="True" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="Normal"/>
</StackPanel>
</DockPanel>
</GroupBox>
<!-- 4. Split: Active Transmitted Signals & Clients (Left) & Real-time Log Console (Right) -->
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="420"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Left: Split into Connected Clients (Top) & Active Tx Signals (Bottom) -->
<Grid Grid.Column="0" Margin="0,0,6,0">
<Grid.RowDefinitions>
<RowDefinition Height="110"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Connected Clients -->
<GroupBox Grid.Row="0" Header=" 👥 연결된 클라이언트 (Connected Clients) "
FontWeight="Bold" Foreground="#1E293B" Padding="4" Margin="0,0,0,6"
Background="White" BorderBrush="#CBD5E1">
<DataGrid x:Name="GridClients" AutoGenerateColumns="False" IsReadOnly="True"
HeadersVisibility="Column" GridLinesVisibility="All" Background="White"
RowHeaderWidth="0">
<DataGrid.Columns>
<DataGridTextColumn Header="클라이언트 IP:Port" Binding="{Binding IpPort}" Width="150"/>
<DataGridTextColumn Header="접속시간" Binding="{Binding ConnectTime}" Width="70"/>
<DataGridTextColumn Header="송신(Tx)" Binding="{Binding SentPackets}" Width="70"/>
<DataGridTextColumn Header="수신(Rx)" Binding="{Binding ReceivedPackets}" Width="70"/>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
<!-- Active Transmitted Signals List -->
<GroupBox Grid.Row="1" Header=" 📡 송신 중인 신호 목록 (Active Transmitted Signals) "
FontWeight="Bold" Foreground="#1E293B" Padding="4"
Background="White" BorderBrush="#CBD5E1">
<DataGrid x:Name="GridTxSignals" AutoGenerateColumns="False" IsReadOnly="True"
HeadersVisibility="Column" GridLinesVisibility="Horizontal" Background="White"
RowHeaderWidth="0" AlternatingRowBackground="#F8FAFC">
<DataGrid.Columns>
<DataGridTextColumn Header="소스" Binding="{Binding Source}" Width="65">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="#2563EB"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="신호 이름 (Signal Name)" Binding="{Binding Name}" Width="*">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="#0F172A"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="현재 송신값" Binding="{Binding Value}" Width="85">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="#059669"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="단위" Binding="{Binding Unit}" Width="50">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#64748B"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</GroupBox>
</Grid>
<!-- Right: Real-time Packet Console -->
<GroupBox Grid.Column="1" Header=" 실시간 통신 패킷 로그 (Real-time Packet Console) "
FontWeight="Bold" Foreground="#1E293B" Padding="4" Margin="6,0,0,0"
Background="White" BorderBrush="#CBD5E1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="0,0,0,4" LastChildFill="True">
<!-- Right Dock: Clear Button (Declared first for correct WPF docking) -->
<Button x:Name="BtnClearLog" Content="로그 지우기" Width="80" Height="24" DockPanel.Dock="Right" Click="BtnClearLog_Click"/>
<!-- Left Checkboxes -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<CheckBox x:Name="CheckAutoScroll" Content="자동스크롤" IsChecked="True" VerticalAlignment="Center" Margin="0,0,8,0"/>
<CheckBox x:Name="CheckShowRx" Content="📥 수신(Rx)" IsChecked="True" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="Bold" Foreground="#1E40AF"/>
<CheckBox x:Name="CheckShowTx" Content="📤 송신(Tx)" IsChecked="True" VerticalAlignment="Center" Margin="0,0,8,0"/>
<CheckBox x:Name="CheckShowHex" Content="HEX 덤프" IsChecked="False" VerticalAlignment="Center"/>
</StackPanel>
</DockPanel>
<TextBox x:Name="TextLogConsole" Grid.Row="1"
FontFamily="Consolas" FontSize="11"
Background="#0F172A" Foreground="#F8FAFC"
IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
TextWrapping="NoWrap"/>
</Grid>
</GroupBox>
</Grid>
</Grid>
</Window>

710
DCT_test/DCT_test/MainWindow.xaml.cs

@ -0,0 +1,710 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;
namespace DCT_test
{
public class TxSignalItem : INotifyPropertyChanged
{
public string Source { get; set; }
public string Name { get; set; }
private string _value;
public string Value
{
get => _value;
set { if (_value != value) { _value = value; OnPropertyChanged(); } }
}
public string Unit { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public partial class MainWindow : Window
{
private readonly DctTcpServer _server = new DctTcpServer();
private readonly ObservableCollection<TxSignalItem> _txSignals = new ObservableCollection<TxSignalItem>();
private readonly List<DctPacket> _loadedLogRecords = new List<DctPacket>();
private readonly LogFileReader _fileReader = new LogFileReader();
private CancellationTokenSource _streamCts;
private bool _isPlaying = false;
private bool _isPaused = false;
private volatile int _sendRateHz = 30; // 30Hz (태블릿 ChartBatchRenderer 33ms 최적화)
private float _playbackSpeed = 1.0f;
private int _selectedProfile = 0; // 0: MDB+CAN, 1: A2L+CAN
private bool _isFileMode = false;
public MainWindow()
{
InitializeComponent();
InitializeServerEvents();
RefreshIpAddresses();
if (GridTxSignals != null) GridTxSignals.ItemsSource = _txSignals;
UpdateSignalsForProfile(0);
AppendLog("DCT C# WPF 테스트 서버 가동 준비 완료.");
}
private void InitializeServerEvents()
{
_server.OnLogMessage += msg => Dispatcher.Invoke(() => AppendLog(msg));
_server.OnClientListChanged += () => Dispatcher.Invoke(RefreshClientsGrid);
_server.OnStartSimRequested += () => Dispatcher.Invoke(StartTelemetryStreaming);
_server.OnStopSimRequested += () => Dispatcher.Invoke(StopTelemetryStreaming);
}
private void RefreshIpAddresses()
{
ComboIp.Items.Clear();
try
{
var ips = DctTcpServer.GetActiveIpAddresses();
foreach (var ip in ips)
{
ComboIp.Items.Add(ip);
}
if (ComboIp.Items.Count > 0)
{
ComboIp.SelectedIndex = 0;
}
}
catch
{
ComboIp.Items.Add("127.0.0.1");
ComboIp.SelectedIndex = 0;
}
}
private void RefreshClientsGrid()
{
GridClients.ItemsSource = null;
GridClients.ItemsSource = _server.ActiveClients.ToList();
}
private void AppendLog(string message)
{
string logLine = $"[{DateTime.Now:HH:mm:ss.fff}] {message}\r\n";
if (TextLogConsole.Text.Length > 40000)
{
TextLogConsole.Text = TextLogConsole.Text.Substring(15000);
}
TextLogConsole.AppendText(logLine);
if (CheckAutoScroll.IsChecked == true)
{
TextLogConsole.ScrollToEnd();
}
}
private void RadioMode_Checked(object sender, RoutedEventArgs e)
{
if (RadioModeSim == null || RadioModeFile == null) return;
_isFileMode = RadioModeFile.IsChecked == true;
if (_isFileMode)
{
PanelFileZone.Opacity = 1.0;
PanelFileZone.IsEnabled = true;
ComboProfile.IsEnabled = false;
TextLoadedFile.Text = _loadedLogRecords.Count > 0
? $"현재 모드: [ 로그 파일 재생 모드 ({_loadedLogRecords.Count} 레코드) ]"
: "현재 모드: [ 로그 파일 재생 모드 - 파일 선택 필요 ]";
TextLoadedFile.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(217, 119, 6));
AppendLog("모드 전환: [로그 파일 재생 모드] 활성화 (모의신호 선택 비활성화)");
}
else
{
PanelFileZone.Opacity = 0.5;
PanelFileZone.IsEnabled = false;
ComboProfile.IsEnabled = true;
TextLoadedFile.Text = "현재 모드: [ 실시간 모의신호 생성 모드 ]";
TextLoadedFile.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(37, 99, 235));
UpdateSignalsForProfile(ComboProfile.SelectedIndex);
AppendLog("모드 전환: [실시간 모의신호 생성 모드] 활성화");
}
}
private void UpdateSignalsForProfile(int profile)
{
_selectedProfile = profile;
_txSignals.Clear();
if (profile == 0) // 1. [MDB + CAN]
{
// MDB Signals
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "Incar_Snr", Value = "23.50", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "TIN", Value = "23.50", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "Amb_Snr", Value = "30.00", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "Evap_Snr", Value = "4.50", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "Speed", Value = "60.0", Unit = "KPH" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "Water", Value = "85.0", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "DrTempSet", Value = "22.0", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "PsTempSet", Value = "22.0", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "BlrwLevel", Value = "4", Unit = "단" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "BlrwVolt", Value = "13.5", Unit = "V" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "AutoFlag", Value = "1", Unit = "ON" });
_txSignals.Add(new TxSignalItem { Source = "[MDB]", Name = "Humidity", Value = "50", Unit = "%" });
// CAN DBC Signals
_txSignals.Add(new TxSignalItem { Source = "[CAN]", Name = "ALDC_VBVoltVal", Value = "13.80", Unit = "V" });
_txSignals.Add(new TxSignalItem { Source = "[CAN]", Name = "ALDC_HSActVoltVal", Value = "14.00", Unit = "V" });
_txSignals.Add(new TxSignalItem { Source = "[CAN]", Name = "ALDC_LSCmdVoltVal", Value = "12.00", Unit = "V" });
}
else // 2. [A2L + CAN]
{
// A2L XCP DAQ Signals
_txSignals.Add(new TxSignalItem { Source = "[A2L]", Name = "s16_Auto_ClimDtcDrVal_SWAP_MEASURMENT", Value = "24.00", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[A2L]", Name = "s16_Auto_ClimDtcPsVal_SWAP_MEASURMENT", Value = "23.00", Unit = "'C" });
_txSignals.Add(new TxSignalItem { Source = "[A2L]", Name = "u16_DrTempActrFbAd_SWAP_MEASURMENT", Value = "512", Unit = "cnt" });
_txSignals.Add(new TxSignalItem { Source = "[A2L]", Name = "u8_Acon_AconAutoCtrl_SWAP_MEASURMENT", Value = "1", Unit = "raw" });
// CAN DBC Signals
_txSignals.Add(new TxSignalItem { Source = "[CAN]", Name = "ALDC_VBVoltVal", Value = "13.50", Unit = "V" });
_txSignals.Add(new TxSignalItem { Source = "[CAN]", Name = "ALDC_HSActVoltVal", Value = "14.20", Unit = "V" });
_txSignals.Add(new TxSignalItem { Source = "[CAN]", Name = "ALDC_LSCmdVoltVal", Value = "12.00", Unit = "V" });
}
}
private void ComboProfile_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ComboProfile != null && ComboProfile.SelectedIndex >= 0 && !_isFileMode)
{
UpdateSignalsForProfile(ComboProfile.SelectedIndex);
}
}
private void BtnToggleServer_Click(object sender, RoutedEventArgs e)
{
if (!_server.IsRunning)
{
if (!int.TryParse(TextPort.Text.Trim(), out int port)) port = 6000;
if (_server.Start(port))
{
BtnToggleServer.Content = "⏹ 서버 중지 (Stop Server)";
BtnToggleServer.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
TextServerStatus.Text = $"🟢 서버 동작 중 (Port {port})";
TextServerStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74));
// 서버 가동 시 IP/포트 변경 비활성화
ComboIp.IsEnabled = false;
TextPort.IsEnabled = false;
BtnCopyIp.IsEnabled = true;
StartTelemetryStreaming();
}
}
else
{
_server.Stop();
StopTelemetryStreaming();
BtnToggleServer.Content = "▶ 서버 시작 (Start Server)";
BtnToggleServer.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74));
TextServerStatus.Text = "🔴 서버 중지됨 (Stopped)";
TextServerStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
// 서버 중지 시 IP/포트 변경 다시 활성화
ComboIp.IsEnabled = true;
TextPort.IsEnabled = true;
}
}
private void BtnCopyIp_Click(object sender, RoutedEventArgs e)
{
string ip = ComboIp.SelectedItem?.ToString();
if (!string.IsNullOrEmpty(ip))
{
Clipboard.SetText(ip);
MessageBox.Show($"PC IP 주소가 클립보드에 복사되었습니다:\n{ip}", "복사 완료", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private void BtnBrowse_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFileDialog
{
Title = "재생할 로그 파일 선택",
Filter = "로그 파일 (*.csv;*.txt;*.dpc)|*.csv;*.txt;*.dpc|모든 파일 (*.*)|*.*"
};
if (dlg.ShowDialog() == true)
{
LoadLogFile(dlg.FileName);
}
}
private void BtnClearFile_Click(object sender, RoutedEventArgs e)
{
_loadedLogRecords.Clear();
TextLoadedFile.Text = "현재 모드: [ 로그 파일 재생 모드 - 파일 선택 필요 ]";
TextLoadedFile.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(217, 119, 6));
TextDropTitle.Text = "📂 여기에 재생할 로그 파일(.csv, .txt, .dpc)을 끌어다 놓으세요 (Drag & Drop)";
AppendLog("로드된 로그 파일이 초기화되었습니다.");
}
private void Window_DragEnter(object sender, DragEventArgs e)
{
if (_isFileMode && e.Data.GetDataPresent(DataFormats.FileDrop))
e.Effects = DragDropEffects.Copy;
}
private void Window_Drop(object sender, DragEventArgs e)
{
if (!_isFileMode) return;
if (e.Data.GetDataPresent(DataFormats.FileDrop))
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files != null && files.Length > 0)
{
LoadLogFile(files[0]);
}
}
}
private void DropZone_DragEnter(object sender, DragEventArgs e) => Window_DragEnter(sender, e);
private void DropZone_Drop(object sender, DragEventArgs e) => Window_Drop(sender, e);
private void LoadLogFile(string filePath)
{
var result = _fileReader.Load(filePath);
_loadedLogRecords.Clear();
if (result.Success)
{
_loadedLogRecords.AddRange(result.Packets);
// 파일 모드 신호 테이블 갱신
if (_isFileMode)
{
_txSignals.Clear();
foreach (var item in result.InitialSignalItems)
{
_txSignals.Add(item);
}
}
TextLoadedFile.Text = $"현재 로드된 파일: [{result.FileName}] ({result.TotalRecords} 레코드, {result.FileSizeMb:F2} MB)";
TextLoadedFile.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74));
TextDropTitle.Text = $"✅ 로드됨: {result.FileName} ({result.TotalRecords} 레코드)";
AppendLog($"[FILE] 파일 '{result.FileName}' 로드 완료 ({result.TotalRecords} 레코드, {result.FileSizeMb:F2} MB)");
}
else
{
TextLoadedFile.Text = $"파일 로드 실패: {result.ErrorMessage}";
TextLoadedFile.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(220, 38, 38));
AppendLog($"[ERROR] {result.ErrorMessage}");
}
}
private void ComboRate_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
int[] rates = { 10, 20, 30, 50, 100 };
if (ComboRate.SelectedIndex >= 0 && ComboRate.SelectedIndex < rates.Length)
{
_sendRateHz = rates[ComboRate.SelectedIndex];
}
}
private void ComboSpeed_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
float[] speeds = { 0.5f, 1.0f, 2.0f, 5.0f };
if (ComboSpeed.SelectedIndex >= 0 && ComboSpeed.SelectedIndex < speeds.Length)
{
_playbackSpeed = speeds[ComboSpeed.SelectedIndex];
}
}
private void BtnPlay_Click(object sender, RoutedEventArgs e)
{
StartTelemetryStreaming();
}
private void BtnPause_Click(object sender, RoutedEventArgs e)
{
_isPaused = !_isPaused;
BtnPause.Content = _isPaused ? "▶ 재개" : "⏸ 일시정지";
AppendLog(_isPaused ? "스트리밍 일시정지" : "스트리밍 재개");
}
private void BtnStop_Click(object sender, RoutedEventArgs e)
{
StopTelemetryStreaming();
}
private void StartTelemetryStreaming()
{
if (_isPlaying)
{
_isPaused = false;
BtnPause.Content = "⏸ 일시정지";
return;
}
_isPlaying = true;
_isPaused = false;
_streamCts = new CancellationTokenSource();
BtnPlay.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(22, 163, 74));
BtnPlay.Content = "▶ 재생 중";
Task.Run(() => TelemetryStreamWorker(_streamCts.Token));
AppendLog("실시간 텔레메트리 스트리밍 전송 시작.");
}
private void StopTelemetryStreaming()
{
_isPlaying = false;
_isPaused = false;
_streamCts?.Cancel();
BtnPlay.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(37, 99, 235));
BtnPlay.Content = "▶ 재생";
BtnPause.Content = "⏸ 일시정지";
}
private async Task TelemetryStreamWorker(CancellationToken ct)
{
ushort seq = 0;
float simTime = 0.0f;
int fileIdx = 0;
var sw = System.Diagnostics.Stopwatch.StartNew();
long nextTickMs = sw.ElapsedMilliseconds;
long lastUiUpdateMs = 0;
long lastSimTickMs = sw.ElapsedMilliseconds;
while (!ct.IsCancellationRequested && _isPlaying)
{
if (_isPaused)
{
await Task.Delay(50, ct);
nextTickMs = sw.ElapsedMilliseconds;
lastSimTickMs = sw.ElapsedMilliseconds;
continue;
}
int rate = Math.Max(1, _sendRateHz);
double targetIntervalMs = 1000.0 / rate;
long nowMs = sw.ElapsedMilliseconds;
long dtMs = Math.Max(1, nowMs - lastSimTickMs);
lastSimTickMs = nowMs;
simTime += (float)(dtMs / 1000.0 * _playbackSpeed);
float v1 = 0, v2 = 0, v3 = 0, v4 = 0, vEvap = 0;
float vWater = 85.0f;
float drTempSet = 22.0f, psTempSet = 22.0f;
byte blowerLevel = 4;
float blowerVolt = 12.8f;
byte autoFlag = 1;
float lsCmdVolt = 12.0f, hsActVolt = 14.0f;
int profile = _selectedProfile;
bool isFileMode = _isFileMode;
if (isFileMode && _loadedLogRecords.Count > 0) // File playback
{
if (fileIdx >= _loadedLogRecords.Count)
{
bool loop = true;
Dispatcher.Invoke(() => loop = CheckLoop.IsChecked == true);
if (loop) fileIdx = 0;
else { Dispatcher.Invoke(StopTelemetryStreaming); break; }
}
var filePkt = _loadedLogRecords[fileIdx++];
_server.BroadcastPacket(filePkt);
}
else if (profile == 0) // 1. [MDB + CAN] 실제 차량 주행 및 DATC 공조 물리 연동
{
// 기본 주행 사이클: 약 24초 주기의 현실적인 도심/간선 주행 패턴 (가속 -> 정속 -> 감속/회생제동)
float driveCycle = (float)(simTime % 24.0f);
// 가속/감속 상태 인자 (accelFactor: -1.0 [회생제동/감속] ~ +1.0 [가속])
float accelFactor = (float)Math.Sin(driveCycle * (2.0 * Math.PI / 24.0));
// 1) 차속 (Speed, KPH): 차량 관성(Mass Inertia) 기반 S-Curve 가감속 및 주행 (15 ~ 95 KPH)
float baseSpeed = 55.0f + 40.0f * (float)Math.Sin((driveCycle - 6.0f) * (2.0 * Math.PI / 24.0));
float speedJitter = 0.6f * (float)Math.Sin(simTime * 4.2f);
v4 = Math.Max(0.0f, Math.Min(160.0f, baseSpeed + speedJitter)); // Speed
// 2) 배터리 전압 (ALDC_VBVoltVal, V): 실차 회생제동/가속 부하 연동
// 회생제동(accelFactor < -0.2f) 시 LDC 발전 전압 상승(14.2 ~ 14.5V), 가속 시 방전 부하(13.4 ~ 13.7V)
v3 = 13.90f - 0.45f * accelFactor + 0.1f * (float)Math.Sin(simTime * 1.5f);
// 3) 냉각수온 (Water, 'C): 주행 부하 반응 (83.0 ~ 91.0'C 완만한 수온 곡선)
vWater = 87.0f + 3.5f * (float)Math.Sin((driveCycle - 8.0f) * (2.0 * Math.PI / 24.0));
// 4) 실내온도 (Incar_Snr, 'C): 공조 냉각 열역학 곡선 (21.5 ~ 24.5'C)
v1 = 23.0f + 1.5f * (float)Math.Cos(simTime * 0.15f);
// 5) 에바온도 (Evap_Snr, 'C): 컴프레셔 압축-팽창 사이클 (2.8 ~ 6.2'C, 주기 약 14초)
vEvap = 4.5f + 1.7f * (float)Math.Sin(simTime * 0.45f);
// 6) 외기온도 (Amb_Snr, 'C): 외부 기온 (29.5 ~ 31.5'C)
v2 = 30.5f + 1.0f * (float)Math.Sin(simTime * 0.08f);
// 7) 풍량단수 (BlrwLevel): 실내온도 및 주행 상태 연동 직각 스텝 (약 3~4초 주기)
float blrScore = (v1 - 21.5f) * 1.5f + (accelFactor * 0.5f);
blowerLevel = (byte)Math.Max(2, Math.Min(6, (int)Math.Round(3.5f + blrScore)));
blowerVolt = 11.2f + blowerLevel * 0.45f; // 풍량단수에 연동되는 구동 전압
// 8) 설정온도 (DrTempSet): 다이얼 조작 모의 (22.0'C <-> 21.5'C <-> 22.5'C, 약 7초 주기)
float tempDialCycle = (float)(simTime % 21.0f);
drTempSet = (tempDialCycle < 7.0f) ? 22.0f : ((tempDialCycle < 14.0f) ? 21.5f : 22.5f);
psTempSet = (tempDialCycle < 10.5f) ? 22.0f : 23.0f;
// 9) AUTO 플래그 (AutoFlag): 8초 주기 사각 펄스
autoFlag = (byte)((simTime % 16.0f) < 12.0f ? 1 : 0);
// 10) CAN LDC 저전압 지령 (ALDC_LSCmdVoltVal): 회생제동/충전 모드 스텝
lsCmdVolt = (accelFactor < -0.3f) ? 14.2f : ((accelFactor > 0.3f) ? 12.0f : 13.5f);
hsActVolt = 14.1f + 0.1f * (float)Math.Sin(simTime * 0.5f);
short incarRaw = (short)Math.Round(v1 * 10.0f);
short ambRaw = (short)Math.Round(v2 * 10.0f);
short evapRaw = (short)Math.Round(vEvap * 10.0f);
// 1) 0xA3 Type 0x01 (MDB Stream) - Record 161 (DATC Climate: 41 Signals = Exactly 68 Bytes)
byte[] mdb161 = new byte[2 + 68];
mdb161[0] = 0x01; // Type: MDB
mdb161[1] = 161; // ReclID: 161
int off = 2;
mdb161[off + 0] = autoFlag; // AutoFlag (Offset 0: 펄스 상태)
mdb161[off + 1] = 0; // SpecSel (Offset 1)
mdb161[off + 2] = blowerLevel; // AutoBlrStep (Offset 2)
mdb161[off + 3] = (byte)Math.Round(drTempSet / 0.5f); // DrTempSet (Offset 3: 다이얼 스텝 21.5/22.0/22.5'C)
mdb161[off + 4] = (byte)Math.Round(psTempSet / 0.5f); // PsTempSet (Offset 4: 다이얼 스텝)
mdb161[off + 5] = blowerLevel; // BlrwLevel (Offset 5: 6->5->4->3->2단 직각 스텝!)
mdb161[off + 6] = (byte)Math.Round(blowerVolt / 0.1f); // BlrwVolt (Offset 6: 단수 비례 전압 스텝)
// Incar_Snr (Offset 7..8)
mdb161[off + 7] = (byte)(incarRaw & 0xFF); mdb161[off + 8] = (byte)((incarRaw >> 8) & 0xFF);
// Incar_Flt (Offset 9..10)
mdb161[off + 9] = (byte)(incarRaw & 0xFF); mdb161[off + 10] = (byte)((incarRaw >> 8) & 0xFF);
// TIN (Offset 11..12)
mdb161[off + 11] = (byte)(incarRaw & 0xFF); mdb161[off + 12] = (byte)((incarRaw >> 8) & 0xFF);
// Amb_Snr (Offset 13..14)
mdb161[off + 13] = (byte)(ambRaw & 0xFF); mdb161[off + 14] = (byte)((ambRaw >> 8) & 0xFF);
// Amb_Flt (Offset 15..16)
mdb161[off + 15] = (byte)(ambRaw & 0xFF); mdb161[off + 16] = (byte)((ambRaw >> 8) & 0xFF);
// Evap_Snr (Offset 17..18)
mdb161[off + 17] = (byte)(evapRaw & 0xFF); mdb161[off + 18] = (byte)((evapRaw >> 8) & 0xFF);
// Evap_Flt (Offset 19..20)
mdb161[off + 19] = (byte)(evapRaw & 0xFF); mdb161[off + 20] = (byte)((evapRaw >> 8) & 0xFF);
var mdbPkt161 = DctPacket.Create(0xA3, mdb161, seq);
_server.BroadcastPacket(mdbPkt161);
// 2) 0xA3 Type 0x01 (MDB Stream) - Record 160 (Speed, Water, Humidity: 29 Signals = Exactly 30 Bytes)
byte[] mdb160 = new byte[2 + 30];
mdb160[0] = 0x01; // Type: MDB
mdb160[1] = 160; // ReclID: 160
int off160 = 2;
mdb160[off160 + 21] = 50; // Humidity (Offset 21)
mdb160[off160 + 25] = (byte)Math.Max(0, Math.Min(255, (int)Math.Round(v4))); // Speed (Offset 25: 실차 가감속 프로파일)
mdb160[off160 + 26] = (byte)Math.Max(0, Math.Min(255, (int)Math.Round((vWater + 48.0f) / 0.75f))); // Water (Offset 26: 85'C)
var mdbPkt160 = DctPacket.Create(0xA3, mdb160, seq);
_server.BroadcastPacket(mdbPkt160);
// 3) 0xA3 Type 0x02 (CAN DBC Stream) - ALDC_01_100ms (CAN ID: 548 = 0x0224, DLC 32)
byte[] canData = new byte[32];
canData[15] = (byte)Math.Round(lsCmdVolt / 0.1f); // ALDC_LSCmdVoltVal (Bit 120 -> Byte 15: 12.0/13.5/14.0V 직각 제어 스텝)
short hsRaw = (short)Math.Round(hsActVolt / 0.1f); // ALDC_HSActVoltVal (Bit 136 -> Byte 17..18)
canData[17] = (byte)(hsRaw & 0xFF); canData[18] = (byte)((hsRaw >> 8) & 0xFF);
// ALDC_VBVoltVal (Bit 226, 8 bits: Byte 28 bits 2..7 + Byte 29 bits 0..1)
uint vbRaw = (uint)Math.Round(v3 / 0.1f);
canData[28] = (byte)(((uint)canData[28] & 0x03u) | (((uint)vbRaw & 0x3Fu) << 2));
canData[29] = (byte)(((uint)canData[29] & 0xFCu) | (((uint)vbRaw >> 6) & 0x03u));
byte[] canPayload = new byte[4 + 3 + 32];
canPayload[0] = 0x02; // Type: CAN DBC
canPayload[1] = 0x01; // CurPage: 1
canPayload[2] = 0x01; // TotPage: 1
canPayload[3] = 0x01; // FrameCount: 1
canPayload[4] = 0x24; // ID Low
canPayload[5] = 0x02; // ID High
canPayload[6] = 32; // DLC: 32 bytes
Array.Copy(canData, 0, canPayload, 7, 32);
var canPkt = DctPacket.Create(0xA3, canPayload, seq);
_server.BroadcastPacket(canPkt);
}
else // 2. [A2L + CAN] 실차 주행 및 XCP DAQ 연동
{
float driveCycle = (float)(simTime % 24.0f);
float accelFactor = (float)Math.Sin(driveCycle * (2.0 * Math.PI / 24.0));
// 토출온도 연속 아날로그 제어 (18.5 ~ 24.5'C)
v1 = 21.5f + 3.0f * (float)Math.Cos(simTime * 0.18f); // s16_Auto_ClimDtcDrVal ('C)
v2 = 21.0f + 2.5f * (float)Math.Cos(simTime * 0.15f); // s16_Auto_ClimDtcPsVal ('C)
v3 = 13.90f - 0.45f * accelFactor + 0.1f * (float)Math.Sin(simTime * 1.5f); // ALDC_VBVoltVal (V)
v4 = 520.0f + 180.0f * (float)Math.Sin(simTime * 0.35f); // u16_DrTempActrFbAd (340 ~ 700 cnt)
autoFlag = (byte)((simTime % 14.0f) < 10.0f ? 1 : 0); // 에어컨 자동 제어 플래그 사각 펄스
lsCmdVolt = (accelFactor < -0.3f) ? 14.2f : ((accelFactor > 0.3f) ? 12.0f : 13.5f);
hsActVolt = 14.1f + 0.1f * (float)Math.Sin(simTime * 0.5f);
// 1) 0xD0: 마지막 0x41/0x42의 count/type/endian 설정과 동일하게 생성
short drRaw = (short)Math.Round(v1 * 10.0f);
short psRaw = (short)Math.Round(v2 * 10.0f);
short actrRaw = (short)Math.Round(v4);
byte[] xcpPayload = _server.BuildXcpDaqPayload(drRaw, psRaw, actrRaw, autoFlag);
_server.BroadcastPacket(DctPacket.Create(0xD0, xcpPayload, seq));
// 2) 0xA3 Type 0x02 (CAN DBC Stream) - ALDC_01_100ms
byte[] canData = new byte[32];
canData[15] = (byte)Math.Round(lsCmdVolt / 0.1f); // ALDC_LSCmdVoltVal
short hsRaw = (short)Math.Round(hsActVolt / 0.1f); // ALDC_HSActVoltVal
canData[17] = (byte)(hsRaw & 0xFF); canData[18] = (byte)((hsRaw >> 8) & 0xFF);
// ALDC_VBVoltVal (Bit 226)
uint vbRaw = (uint)Math.Round(v3 / 0.1f);
canData[28] = (byte)(((uint)canData[28] & 0x03u) | (((uint)vbRaw & 0x3Fu) << 2));
canData[29] = (byte)(((uint)canData[29] & 0xFCu) | (((uint)vbRaw >> 6) & 0x03u));
byte[] canPayload = new byte[4 + 3 + 32];
canPayload[0] = 0x02; canPayload[1] = 0x01; canPayload[2] = 0x01; canPayload[3] = 0x01;
canPayload[4] = 0x24; canPayload[5] = 0x02; canPayload[6] = 32;
Array.Copy(canData, 0, canPayload, 7, 32);
_server.BroadcastPacket(DctPacket.Create(0xA3, canPayload, seq));
// 3) MDB Record 160 for unified Speed & Water
float vSpeedSim = Math.Max(0.0f, Math.Min(160.0f, 55.0f + 40.0f * (float)Math.Sin((driveCycle - 6.0f) * (2.0 * Math.PI / 24.0))));
byte[] mdb160 = new byte[2 + 30];
mdb160[0] = 0x01; mdb160[1] = 160;
mdb160[2 + 25] = (byte)Math.Max(0, Math.Min(255, (int)Math.Round(vSpeedSim)));
mdb160[2 + 26] = (byte)Math.Max(0, Math.Min(255, (int)Math.Round((85.0f + 48.0f) / 0.75f)));
_server.BroadcastPacket(DctPacket.Create(0xA3, mdb160, seq));
}
// UI 테이블 및 로그 갱신: 송신 지터(Jitter) 0화를 위해 100ms 주기로 비동기(BeginInvoke) 분리
if (nowMs - lastUiUpdateMs >= 100 || lastUiUpdateMs == 0)
{
lastUiUpdateMs = nowMs;
int pProfile = profile;
ushort pSeq = seq;
float pV1 = v1, pV2 = v2, pV3 = v3, pV4 = v4, pVEvap = vEvap;
float pWater = vWater, pDrSet = drTempSet, pPsSet = psTempSet, pBlrVolt = blowerVolt, pHsVolt = hsActVolt, pLsVolt = lsCmdVolt;
byte pBlrLevel = blowerLevel, pAuto = autoFlag;
int activeClientCount = _server.ActiveClients.Count;
ulong totalCanAcks = _server.TotalCanAcks;
ulong totalXcpAcks = _server.TotalXcpAcks;
_ = Dispatcher.BeginInvoke(new Action(() =>
{
bool cShowTx = CheckShowTx.IsChecked == true;
bool cShowHex = CheckShowHex.IsChecked == true;
_server.ShowRxPackets = CheckShowRx.IsChecked == true;
_server.ShowHexDump = cShowHex;
if (pProfile == 0) // 1. [MDB + CAN]
{
if (_txSignals.Count >= 15)
{
_txSignals[0].Value = $"{pV1:F2}";
_txSignals[1].Value = $"{pV1:F2}";
_txSignals[2].Value = $"{pV2:F2}";
_txSignals[3].Value = $"{pVEvap:F2}";
_txSignals[4].Value = $"{pV4:F1}";
_txSignals[5].Value = $"{pWater:F1}";
_txSignals[6].Value = $"{pDrSet:F1}";
_txSignals[7].Value = $"{pPsSet:F1}";
_txSignals[8].Value = $"{pBlrLevel}";
_txSignals[9].Value = $"{pBlrVolt:F1}";
_txSignals[10].Value = $"{pAuto}";
_txSignals[11].Value = "50";
_txSignals[12].Value = $"{pV3:F2}";
_txSignals[13].Value = $"{pHsVolt:F2}";
_txSignals[14].Value = $"{pLsVolt:F2}";
}
}
else // 2. [A2L + CAN]
{
if (_txSignals.Count >= 7)
{
_txSignals[0].Value = $"{pV1:F2}";
_txSignals[1].Value = $"{pV2:F2}";
_txSignals[2].Value = $"{pV4:F0}";
_txSignals[3].Value = $"{pAuto}";
_txSignals[4].Value = $"{pV3:F2}";
_txSignals[5].Value = $"{pHsVolt:F2}";
_txSignals[6].Value = $"{pLsVolt:F2}";
}
}
if (cShowTx && (pSeq % 5 == 0 || activeClientCount > 0))
{
string clientInfo = activeClientCount > 0 ? $"-> {activeClientCount} device(s)" : "(Idle)";
string txLog = (pProfile == 0)
? $"[📤 TX (MDB+CAN 30Hz) {clientInfo}] #{pSeq:D5} | Incar: {pV1:F2}'C | Speed: {pV4:F1}KPH | Blrw: {pBlrLevel}단 | VBatt: {pV3:F2}V (ACK: CAN={totalCanAcks})"
: $"[📤 TX (A2L+CAN 30Hz) {clientInfo}] #{pSeq:D5} | DrVal: {pV1:F2}'C | DrFb: {pV4:F0}cnt | VBatt: {pV3:F2}V (ACK: XCP={totalXcpAcks})";
if (cShowHex)
{
byte[] mdbSample = new byte[2 + 68];
mdbSample[0] = 0x01; mdbSample[1] = 161;
byte[] mdbEnc = DctPacket.Create(0xA3, mdbSample, pSeq).Encode();
string hex = BitConverter.ToString(mdbEnc).Replace("-", " ");
txLog += $"\r\n └ [0xA3 Frame]: {hex}";
}
AppendLog(txLog);
}
if (pSeq % 10 == 0)
{
RefreshClientsGrid();
}
}));
}
seq++;
// 고해상도 정밀 타이머 슬립 (Windows 타이머 지터 누적 제거)
nextTickMs += (long)targetIntervalMs;
long sleepMs = nextTickMs - sw.ElapsedMilliseconds;
if (sleepMs > 2)
{
await Task.Delay((int)sleepMs, ct);
}
else if (sleepMs < -100)
{
// 지연 누적 방지 리셋
nextTickMs = sw.ElapsedMilliseconds;
}
else
{
await Task.Yield();
}
}
}
private void BtnClearLog_Click(object sender, RoutedEventArgs e)
{
TextLogConsole.Clear();
}
protected override void OnClosed(EventArgs e)
{
_server.Stop();
StopTelemetryStreaming();
base.OnClosed(e);
}
}
}

52
DCT_test/DCT_test/Properties/AssemblyInfo.cs

@ -0,0 +1,52 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
// 이러한 특성 값을 변경하세요.
[assembly: AssemblyTitle("DCT_test")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("DCT_test")]
[assembly: AssemblyCopyright("Copyright © 2026")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
[assembly: ComVisible(false)]
//지역화 가능 애플리케이션 빌드를 시작하려면 다음을 설정하세요.
//.csproj 파일의 <UICulture>CultureYouAreCodingWith</UICulture>
//<PropertyGroup> 내부. 예를 들어 미국 영어를 사용하는 경우
//사용하는 경우 <UICulture>를 en-US로 설정합니다. 그런 다음 아래
//NeutralResourceLanguage 특성의 주석 처리를 제거합니다. 아래 줄의 "en-US"를 업데이트하여
//프로젝트 파일의 UICulture 설정과 일치시킵니다.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //테마별 리소스 사전의 위치
//(페이지 또는 응용 프로그램 리소스 사진에
// 리소스가 없는 경우에 사용됨)
ResourceDictionaryLocation.SourceAssembly //제네릭 리소스 사전의 위치
//(페이지 또는 응용 프로그램 리소스 사진에
// 리소스가 없는 경우에 사용됨)
)]
// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
//
// 주 버전
// 부 버전
// 빌드 번호
// 수정 버전
//
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

71
DCT_test/DCT_test/Properties/Resources.Designer.cs

@ -0,0 +1,71 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 이 코드는 도구를 사용하여 생성되었습니다.
// 런타임 버전:4.0.30319.42000
//
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
// 이러한 변경 내용이 손실됩니다.
// </auto-generated>
//------------------------------------------------------------------------------
namespace DCT_test.Properties
{
/// <summary>
/// 지역화된 문자열 등을 찾기 위한 강력한 형식의 리소스 클래스입니다.
/// </summary>
// 이 클래스는 ResGen 또는 Visual Studio와 같은 도구를 통해 StronglyTypedResourceBuilder
// 클래스에서 자동으로 생성되었습니다.
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여
// ResGen을 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources
{
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources()
{
}
/// <summary>
/// 이 클래스에서 사용하는 캐시된 ResourceManager 인스턴스를 반환합니다.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if ((resourceMan == null))
{
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DCT_test.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 이 강력한 형식의 리소스 클래스를 사용하여 모든 리소스 조회에 대해 현재 스레드의 CurrentUICulture 속성을
/// 재정의합니다.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture
{
get
{
return resourceCulture;
}
set
{
resourceCulture = value;
}
}
}
}

117
DCT_test/DCT_test/Properties/Resources.resx

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

30
DCT_test/DCT_test/Properties/Settings.Designer.cs

@ -0,0 +1,30 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace DCT_test.Properties
{
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
{
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default
{
get
{
return defaultInstance;
}
}
}
}

7
DCT_test/DCT_test/Properties/Settings.settings

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

450
DCT_test/README.md

@ -0,0 +1,450 @@
# DCT 테스트 서버 프로그램 구조 및 실차 로그 파일(log.txt) 연동 가이드
본 문서는 안드로이드 태블릿용 DCT 캘리브레이션 및 진단 계측 앱의 통신 테스트를 위해 제작된 C# WPF 기반 DCT 테스트 서버(DCT_test.exe)의 전체 소프트웨어 아키텍처와, 실제 차량 및 제어기에서 수집된 대규모 실차 계측 데이터(log.txt)를 프로그램과 연결하기 위한 코드 작성 가이드를 기술합니다.
---
## 목차
1. 프로그램 개요 및 실행 방법
2. 테스트 프로그램 전체 아키텍처 및 모듈 구조
3. 핵심 소스코드 파일 및 역할 명세
4. 전송 패킷 바이너리 프로토콜 명세
5. log.txt 실차 대용량 데이터 연동 메커니즘
6. 실차 로그 파서 코드 작성 가이드 및 C# 템플릿
7. 수동 검증 절차 및 시나리오
---
## 1. 프로그램 개요 및 실행 방법
### 1.1 프로그램 목적
- 실제 차량이나 전용 하드웨어 인터페이스 장비 없이도 안드로이드 태블릿 앱이 Wi-Fi TCP(포트 6000)를 통해 실시간 계측 신호를 수신하고, UDP 비콘(포트 6005)을 통해 PC 서버를 자동 탐색(Discovery)할 수 있도록 지원하는 테스트 도구입니다.
- 기본 탑재된 실제 차량 주행 거동 물리 시뮬레이션(가속, 감속, 회생제동, 공조 AUTO 연동)뿐만 아니라, 실제 차량 시험에서 획득한 대용량 실차 데이터(log.txt)를 읽어 실시간으로 단말에 재전송(Replay)하는 기능을 제공합니다.
### 1.2 실행 방법
- dct_test 폴더 또는 최상위 경로의 `Run_DCT_test.bat` 배치 파일을 실행합니다.
- 기본 동작 사양:
- 전송 주기: 30 Hz (33ms, 최적) - 안드로이드 앱의 30Hz 배치 렌더러와 1:1 동기화
- 기본 프로필: 1. [MDB + CAN] MDB 실차공조 및 CAN DBC 동시 스트리밍
- 재생 모드: 실차 거동 모의 (Simulation) 기본 선택
---
## 2. 테스트 프로그램 전체 아키텍처 및 모듈 구조
테스트 프로그램은 계층형 아키텍처(Layered Architecture)로 설계되어 있으며, 고속 통신 패킷 브로드캐스트 스레드와 WPF UI 렌더링 스레드가 완전히 분리되어 송신 지터(Jitter)를 0ms 수준으로 제어합니다.
```mermaid
graph TD
subgraph PresentationLayer ["WPF UI 계층"]
UI["MainWindow.xaml / MainWindow.xaml.cs"]
Grid["송신 신호 모니터링 테이블 (GridTxSignals)"]
Log["실시간 패킷 모니터링 콘솔 (TextLogConsole)"]
Ctrl["재생 제어부 (재생 / 일시정지 / 정지 / 주기 / 배속)"]
end
subgraph DataSourceLayer ["데이터 소스 계층"]
SimEngine["실제 차량 주행 물리 시뮬레이터 (관성 가감속/회생제동/수온/DATC)"]
LogReader["로그 파일 파서 (LogFileReader.cs)"]
LogFile["실제 차량 주행/수신 로그 파일 (log.txt)"]
LogFile --> LogReader
end
subgraph ProtocolLayer ["바이너리 프로토콜 계층"]
Packet["DctPacket.cs / DctPacketType.cs"]
Framing["헤더(0xA1) + 커맨드 + 시퀀스 + 길이 + 페이로드 + 체크섬 + 푸터(0x7E)"]
Packet --> Framing
end
subgraph NetworkLayer ["소켓 통신 계층"]
Server["DctServer.cs"]
TcpServer["TCP Server (포트 6000) - 다중 클라이언트 관리 및 브로드캐스트"]
UdpBeacon["UDP Beacon (포트 6005) - LAN 단말 자동 검색용 DCT_BEACON 송출"]
Server --> TcpServer
Server --> UdpBeacon
end
subgraph TargetClient ["외부 수신 단말"]
Android["안드로이드 태블릿 앱 (DCT App)"]
end
SimEngine -->|시뮬레이션 모드| Packet
LogReader -->|파일 리플레이 모드| Packet
Packet --> Server
TcpServer -->|TCP 패킷 스트리밍| Android
UdpBeacon -.->|LAN 비콘 디스커버리| Android
Server -->|100ms 비동기 분리| UI
```
---
## 3. 핵심 소스코드 파일 및 역할 명세
| 파일 경로 | 주요 역할 및 상세 기능 |
| :--- | :--- |
| `dct_test/DCT_test/MainWindow.xaml` | WPF UI 레이아웃 정의 (서버 제어 패널, 데이터 소스 라디오 버튼, 파일 드롭존, 재생 제어부, 송신 신호 그리드, 실시간 로그 콘솔) |
| `dct_test/DCT_test/MainWindow.xaml.cs` | UI 이벤트 처리 및 전송 타이머 제어<br>• `TelemetryStreamWorker`: 고해상도 Stopwatch 기반 정밀 틱 타이머(30Hz) 및 100ms 주기 비동기 UI 분리 루프<br>• `LoadLogFile`: 드래그 앤 드롭 파일 수신 및 LogFileReader 결과 적재 |
| `dct_test/DCT_test/LogFileReader.cs` | **실제 차량 계측 로그 파일(log.txt)을 읽어 DctPacket 바이너리 패킷으로 변환하는 전담 파서**<br>사용자가 보유한 실차 데이터 포맷에 맞추어 직접 파싱 코드를 작성할 수 있는 핵심 확장 지점 |
| `dct_test/DCT_test/DctServer.cs` | 비동기 TCP 6000 포트 소켓 서버 (태블릿 연결 세션 유지 및 패킷 브로드캐스트), Keep-Alive ACK(0x80) 자동 응답, UDP 6005 비콘 브로드캐스터 |
| `dct_test/DCT_test/DctPacket.cs` | PC 레거시 및 안드로이드 태블릿 앱 규격과 100% 호환되는 바이너리 프레임 인코딩/디코딩, 8비트 Inverted Sum 체크섬 계산, 원시 바이트 스트림 파서(`ParseStream`) 내장 |
---
## 4. 전송 패킷 바이너리 프로토콜 명세
`LogFileReader`에서 `log.txt`를 읽어 생성해야 하는 표준 `DctPacket`의 세부 바이트 구조입니다.
### 4.1 패킷 프레임 공통 구조 (7 바이트 오버헤드)
```text
[0xA1] [Command: 1B] [Seq: 2B (Little-Endian)] [Length: 2B (Little-Endian)] [Payload: N Bytes] [Checksum: 1B] [0x7E]
```
- **시작 바이트**: `0xA1`
- **종료 바이트**: `0x7E`
- **Checksum 계산 공식**: `(byte)((~sum) & 0xFF)` (헤더 0xA1부터 페이로드 마지막 바이트까지의 단순 누적합에 대한 1의 보수)
### 4.2 MDB 스트림 패킷 (`Command = 0xA3`, Type = `0x01`)
- **페이로드 공통 접두사**: `[Type = 0x01] [ReclID = 160 또는 161]`
- **Record 161 (DATC 공조 계측 신호: 총 41개 신호 = 페이로드 70 바이트)**:
- `Offset + 0`: AutoFlag (1 바이트, 1=AUTO, 0=MANUAL)
- `Offset + 1`: SpecSel (1 바이트)
- `Offset + 2`: AutoBlrStep (1 바이트, 1~8단)
- `Offset + 3`: DrTempSet (1 바이트, 값 / 0.5f, 예: 22.0도 -> 44)
- `Offset + 4`: PsTempSet (1 바이트, 값 / 0.5f)
- `Offset + 5`: BlrwLevel (1 바이트, 1~8단)
- `Offset + 6`: BlrwVolt (1 바이트, 값 / 0.1f)
- `Offset + 7..8`: Incar_Snr (2 바이트 signed short Little-Endian, 값 * 10, 예: 23.5도 -> 235)
- `Offset + 9..10`: Incar_Flt (2 바이트 signed short)
- `Offset + 11..12`: TIN (2 바이트 signed short)
- `Offset + 13..14`: Amb_Snr (2 바이트 signed short, 값 * 10)
- `Offset + 15..16`: Amb_Flt (2 바이트 signed short)
- `Offset + 17..18`: Evap_Snr (2 바이트 signed short, 값 * 10)
- `Offset + 19..20`: Evap_Flt (2 바이트 signed short)
- **Record 160 (차속, 수온, 습도 등 차체/엔진 신호: 총 29개 신호 = 페이로드 32 바이트)**:
- `Offset + 21`: Humidity (1 바이트, 0~100%)
- `Offset + 25`: Speed (1 바이트 unsigned char, 0~255 KPH)
- `Offset + 26`: Water (1 바이트, (수온도 + 48.0f) / 0.75f)
### 4.3 CAN DBC 스트림 패킷 (`Command = 0xA3`, Type = `0x02`)
- **CAN ID 548 (ALDC_01_100ms, DLC 32 바이트)**:
- `페이로드 헤더 (7 바이트)`: `[Type: 0x02] [CurPage: 0x01] [TotPage: 0x01] [FrameCount: 0x01] [ID_Low: 0x24] [ID_High: 0x02] [DLC: 32]`
- `CAN Data Byte 15`: ALDC_LSCmdVoltVal (1 바이트, 값 / 0.1f)
- `CAN Data Byte 17..18`: ALDC_HSActVoltVal (2 바이트 signed short, 값 / 0.1f)
- `CAN Data Byte 28~29 (Bit 226, 8 bits)`: ALDC_VBVoltVal (배터리 전압, 값 / 0.1f)
### 4.4 A2L XCP DAQ 스트림 패킷 (`Command = 0xD0`)
- **페이로드**: XCP DAQ ODT 버퍼 (실제 설정된 DAQ 리스트에 따른 바이너리 바이트열)
---
## 5. log.txt 실차 대용량 데이터 연동 메커니즘
### 5.1 실차 데이터 특성 이해
실차 시험 계측 로그(`log.txt`)는 단순 몇 개의 신호만 추출된 파일이 아닙니다. 실제 환경에서는 다음 두 가지 형태 중 하나로 수집됩니다:
1. **원시 통신 패킷 스트림 로그 (Raw Captured Packet Log)**:
- 실제 ECU나 통신 모듈(CAN, MDB, XCP)로부터 수신된 원시 바이너리 프레임(`0xA1 ... 0x7E`)이 헥사 텍스트(Hex Dump) 또는 바이너리로 연속 기록된 형태.
- 단일 파일 내에 Record 160(29개 신호), Record 161(41개 신호), CAN DBC(수십 개 신호), XCP DAQ 등 수백 개의 실차 파라미터가 프레임 단위로 온전히 포함되어 있습니다.
2. **다채널 시계열 계측 로그 (Multi-Channel Time-Series CSV/TXT Log)**:
- 시험 장비에서 수십 개에서 수백 개의 센서 신호(열, Columns)가 타임스탬프와 함께 나열된 텍스트 형태.
- 예: `Time, Incar_Snr, Amb_Snr, Evap_Snr, Speed, Water, RPM, Throttle, BlrwLevel, VBatt, DrTempSet, ...` (수십~수백 개 신호)
### 5.2 프로그램과 연결되는 전체 데이터 파이프라인
프로그램 내부에서 로그 파일이 안드로이드 태블릿 앱까지 전달되는 흐름은 다음과 같습니다:
```mermaid
sequenceDiagram
autonumber
participant User as "사용자"
participant UI as "MainWindow (WPF)"
participant Reader as "LogFileReader.cs"
participant Worker as "TelemetryStreamWorker"
participant Server as "DctServer (TCP 6000)"
participant App as "안드로이드 태블릿 앱"
User->>UI: 1. log.txt 파일을 UI 드롭존에 드래그 앤 드롭 (또는 파일 찾아보기)
UI->>Reader: 2. LoadLogFile(filePath) 호출 -> _fileReader.Load(filePath)
Note over Reader: 3. log.txt 파싱 및 데이터 변환 (DctPacket 생성 및 신호 메타데이터 등록)
Reader-->>UI: 4. LogFileReadResult 반환 (Packets 리스트, InitialSignalItems 목록)
UI->>UI: 5. _loadedLogRecords 리스트에 패킷 적재 및 좌측 신호 테이블 갱신
User->>UI: 6. 데이터 소스를 [실차 로그 파일 리플레이]로 선택 후 [재생] 클릭
UI->>Worker: 7. TelemetryStreamWorker 가동 (설정된 전송 주기, 기본 30Hz)
loop 파일 패킷 브로드캐스트
Worker->>Server: 8. _server.BroadcastPacket(_loadedLogRecords[idx++])
Server->>App: 9. TCP 6000 소켓을 통해 태블릿으로 바이너리 패킷 송출
App->>App: 10. 태블릿 차트 및 수치 그리드에 실차 전체 신호 렌더링
end
```
---
## 6. 실차 로그 파서 코드 작성 가이드 및 C# 템플릿
사용자가 보유한 `log.txt`의 형식에 맞추어 `dct_test/DCT_test/LogFileReader.cs` 파일에 구현할 수 있는 두 가지 대표적인 완성형 코드 템플릿입니다.
### 6.1 케이스 A: 수신된 원시 패킷 Hex 로그 파싱 템플릿
실제 수신된 프레임이 헥사 텍스트(예: `A1 00 46 A3 01 A1 ... 7E`) 형태로 기록된 로그 파일을 처리하는 경우입니다. `DctPacket.ParseStream`을 활용하면 수백 개의 신호가 포함된 원시 프레임이 바이트 손실 없이 그대로 복원됩니다.
```csharp
using System;
using System.Collections.Generic;
using System.IO;
namespace DCT_test
{
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);
var byteBuffer = new List<byte>();
// 1. log.txt로부터 Hex 텍스트 라인을 읽어 바이트 버퍼 구성
using (var reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
line = line.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWith("#")) continue;
// 공백, 쉼표, 콜론 등으로 분리된 Hex 바이트 문자열 파싱
string[] hexTokens = line.Split(new[] { ' ', ',', '\t', '-' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in hexTokens)
{
if (byte.TryParse(token, System.Globalization.NumberStyles.HexNumber, null, out byte b))
{
byteBuffer.Add(b);
}
}
}
}
// 2. DctPacket.ParseStream을 호출하여 전체 원시 패킷(MDB 160/161, CAN, XCP 등) 자동 추출
result.Packets = DctPacket.ParseStream(byteBuffer);
// 3. UI 신호 목록에 표시할 정보 구성
result.InitialSignalItems.Add(new TxSignalItem { Source = "[FILE]", Name = "Total Frames", Value = $"{result.Packets.Count}", Unit = "pkts" });
result.InitialSignalItems.Add(new TxSignalItem { Source = "[FILE]", Name = "Raw Bytes", Value = $"{byteBuffer.Count}", Unit = "bytes" });
result.Success = result.Packets.Count > 0;
if (!result.Success)
{
result.ErrorMessage = "유효한 DCT 패킷(0xA1...0x7E)을 찾을 수 없습니다.";
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = $"파일 파싱 오류: {ex.Message}";
}
return result;
}
}
}
```
---
### 6.2 케이스 B: 수십~수백 개 신호가 포함된 다채널 텍스트/CSV 로그 파싱 템플릿
타임스탬프와 함께 수십 개의 센서 및 제어 파라미터가 기록된 실차 주행 로그를 MDB 161(공조 41종) 및 MDB 160(주행 29종), CAN DBC 프레임으로 동적 매핑하여 전송하는 구조입니다.
```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
namespace DCT_test
{
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);
using (var reader = new StreamReader(filePath))
{
// 1. 헤더 행을 읽어 수십~수백 개의 전체 신호 열(Column) 인덱스 매핑 구성
string headerLine = reader.ReadLine();
if (string.IsNullOrEmpty(headerLine))
{
result.ErrorMessage = "파일 헤더가 비어 있습니다.";
return result;
}
string[] headers = headerLine.Split(new[] { ',', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
var colMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < headers.Length; i++)
{
colMap[headers[i].Trim()] = i;
}
// 2. UI '송신 중인 신호 목록'에 파일에 포함된 주요 신호들을 동적으로 등록
foreach (var h in headers)
{
result.InitialSignalItems.Add(new TxSignalItem
{
Source = "[FILE]",
Name = h.Trim(),
Value = "0.0",
Unit = ""
});
}
// 헬퍼 함수: 특정 신호 열이 존재하면 파싱, 없으면 기본값 반환
float GetVal(string[] tokens, string sigName, float defVal = 0.0f)
{
if (colMap.TryGetValue(sigName, out int idx) && idx < tokens.Length)
{
if (float.TryParse(tokens[idx], NumberStyles.Float, CultureInfo.InvariantCulture, out float v))
return v;
}
return defVal;
}
// 3. 데이터 행을 순차적으로 읽어 MDB / CAN 바이너리 패킷 조립
string line;
ushort seq = 0;
while ((line = reader.ReadLine()) != null)
{
line = line.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWith("#")) continue;
string[] tokens = line.Split(new[] { ',', '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length < 2) continue;
// A. 실차 MDB Record 161 (DATC 공조 41종 신호 패킹: 70 바이트)
byte[] mdb161 = new byte[2 + 68];
mdb161[0] = 0x01; // Type: MDB
mdb161[1] = 161; // ReclID: 161
int off161 = 2;
byte autoFlag = (byte)GetVal(tokens, "AutoFlag", 1.0f);
byte blowerLevel = (byte)GetVal(tokens, "BlrwLevel", 4.0f);
float drTempSet = GetVal(tokens, "DrTempSet", 22.0f);
float psTempSet = GetVal(tokens, "PsTempSet", 22.0f);
float blowerVolt = GetVal(tokens, "BlrwVolt", 12.8f);
mdb161[off161 + 0] = autoFlag;
mdb161[off161 + 2] = blowerLevel;
mdb161[off161 + 3] = (byte)Math.Round(drTempSet / 0.5f);
mdb161[off161 + 4] = (byte)Math.Round(psTempSet / 0.5f);
mdb161[off161 + 5] = blowerLevel;
mdb161[off161 + 6] = (byte)Math.Round(blowerVolt / 0.1f);
// 온도 신호들 (0.1도 스케일링 short)
short incarRaw = (short)Math.Round(GetVal(tokens, "Incar_Snr", 23.0f) * 10.0f);
short ambRaw = (short)Math.Round(GetVal(tokens, "Amb_Snr", 30.0f) * 10.0f);
short evapRaw = (short)Math.Round(GetVal(tokens, "Evap_Snr", 4.5f) * 10.0f);
mdb161[off161 + 7] = (byte)(incarRaw & 0xFF); mdb161[off161 + 8] = (byte)((incarRaw >> 8) & 0xFF);
mdb161[off161 + 9] = (byte)(incarRaw & 0xFF); mdb161[off161 + 10] = (byte)((incarRaw >> 8) & 0xFF);
mdb161[off161 + 11] = (byte)(incarRaw & 0xFF); mdb161[off161 + 12] = (byte)((incarRaw >> 8) & 0xFF);
mdb161[off161 + 13] = (byte)(ambRaw & 0xFF); mdb161[off161 + 14] = (byte)((ambRaw >> 8) & 0xFF);
mdb161[off161 + 15] = (byte)(ambRaw & 0xFF); mdb161[off161 + 16] = (byte)((ambRaw >> 8) & 0xFF);
mdb161[off161 + 17] = (byte)(evapRaw & 0xFF); mdb161[off161 + 18] = (byte)((evapRaw >> 8) & 0xFF);
mdb161[off161 + 19] = (byte)(evapRaw & 0xFF); mdb161[off161 + 20] = (byte)((evapRaw >> 8) & 0xFF);
result.Packets.Add(DctPacket.Create(0xA3, mdb161, seq));
// B. 실차 MDB Record 160 (차속, 수온, 습도 패킹: 32 바이트)
byte[] mdb160 = new byte[2 + 30];
mdb160[0] = 0x01;
mdb160[1] = 160;
int off160 = 2;
float speed = GetVal(tokens, "Speed", 0.0f);
float water = GetVal(tokens, "Water", 85.0f);
float humidity = GetVal(tokens, "Humidity", 50.0f);
mdb160[off160 + 21] = (byte)Math.Max(0, Math.Min(100, (int)Math.Round(humidity)));
mdb160[off160 + 25] = (byte)Math.Max(0, Math.Min(255, (int)Math.Round(speed)));
mdb160[off160 + 26] = (byte)Math.Max(0, Math.Min(255, (int)Math.Round((water + 48.0f) / 0.75f)));
result.Packets.Add(DctPacket.Create(0xA3, mdb160, seq));
// C. 실차 CAN DBC ALDC_01_100ms 패킷 (배터리 전압 및 지령 전압 패킹: 39 바이트)
byte[] canData = new byte[32];
float lsVolt = GetVal(tokens, "ALDC_LSCmdVoltVal", 13.5f);
float vBatt = GetVal(tokens, "ALDC_VBVoltVal", 13.8f);
canData[15] = (byte)Math.Round(lsVolt / 0.1f);
uint vbRaw = (uint)Math.Round(vBatt / 0.1f);
canData[28] = (byte)(((uint)canData[28] & 0x03u) | (((uint)vbRaw & 0x3Fu) << 2));
canData[29] = (byte)(((uint)canData[29] & 0xFCu) | (((uint)vbRaw >> 6) & 0x03u));
byte[] canPayload = new byte[4 + 3 + 32];
canPayload[0] = 0x02; canPayload[1] = 0x01; canPayload[2] = 0x01; canPayload[3] = 0x01;
canPayload[4] = 0x24; canPayload[5] = 0x02; canPayload[6] = 32;
Array.Copy(canData, 0, canPayload, 7, 32);
result.Packets.Add(DctPacket.Create(0xA3, canPayload, seq));
seq++;
}
}
result.Success = result.Packets.Count > 0;
if (!result.Success)
{
result.ErrorMessage = "파싱된 패킷 레코드가 0개입니다. 파일 데이터 구조를 확인하세요.";
}
}
catch (Exception ex)
{
result.Success = false;
result.ErrorMessage = $"파일 파싱 오류: {ex.Message}";
}
return result;
}
}
}
```
---
## 7. 수동 검증 절차 및 시나리오
1. **파일 로드 및 메타데이터 인식 확인**:
- `Run_DCT_test.bat`를 실행합니다.
- 상단 `2. 데이터 소스 모드 선택`에서 `실차 로그 파일 리플레이 (File Replay)` 라디오 버튼을 선택합니다.
- 준비한 실차 대용량 `log.txt` 파일을 드롭존 영역에 드래그 앤 드롭하거나 `[파일 찾아보기]` 버튼으로 지정합니다.
- `현재 로드된 파일: [log.txt] (N 레코드, M MB)` 텍스트가 표시되고 좌측 테이블에 파일 신호 목록이 갱신되는지 확인합니다.
2. **서버 시작 및 패킷 스트리밍**:
- `[서버 시작]` 버튼을 클릭하여 TCP 6000 포트 수신 대기 상태로 전환합니다.
- `[재생]` 버튼을 클릭합니다.
- 우측 로그 콘솔에 `[TX (FILE)]` 로그가 순차적으로 출력되며 좌측 수치 테이블의 값들이 실제 주행 데이터 흐름에 맞게 갱신되는지 확인합니다.
3. **태블릿 앱 연동 및 다채널 데이터 렌더링 검증**:
- 안드로이드 태블릿 앱을 실행하여 PC의 IP로 접속합니다.
- 차트 화면에서 `Incar_Snr`, `Speed`, `ALDC_VBVoltVal`, `BlrwLevel` 등 여러 신호를 등록합니다.
- `log.txt`에 기록된 실제 주행 거동(도심 주행 가감속, 회생제동 전압, 공조 냉각 등)이 왜곡 없이 태블릿 화면에 실시간으로 재생되는지 확인합니다.

2
DCT_test/Run_DCT_test.bat

@ -0,0 +1,2 @@
@echo off
start "" "%~dp0DCT_test\bin\Release\DCT_test.exe"

9
DCT_test/build_csharp.bat

@ -0,0 +1,9 @@
@echo off
chcp 65001 > nul
echo [C# WPF] DCT_test 빌드 시작...
"C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\amd64\MSBuild.exe" "%~dp0DCT_test\DCT_test.csproj" -p:Configuration=Release -v:minimal
if %ERRORLEVEL% EQU 0 (
echo [SUCCESS] DCT_test.exe C# 빌드가 성공적으로 완료되었습니다!
) else (
echo [ERROR] C# 빌드 실패!
)
Loading…
Cancel
Save