20 changed files with 2832 additions and 1 deletions
@ -0,0 +1,3 @@ |
|||||
|
<Solution> |
||||
|
<Project Path="DCT_test/DCT_test.csproj" Id="2a257dc9-45c2-4539-9992-57340a291c74" /> |
||||
|
</Solution> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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 |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 & 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 & 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 & 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 & 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 실차공조 & CAN DBC 동시 스트리밍"/> |
||||
|
<ComboBoxItem Content="2. [A2L + CAN] A2L XCP 계측 & 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> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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")] |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
<?xml version='1.0' encoding='utf-8'?> |
||||
|
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)"> |
||||
|
<Profiles> |
||||
|
<Profile Name="(Default)" /> |
||||
|
</Profiles> |
||||
|
<Settings /> |
||||
|
</SettingsFile> |
||||
@ -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`에 기록된 실제 주행 거동(도심 주행 가감속, 회생제동 전압, 공조 냉각 등)이 왜곡 없이 태블릿 화면에 실시간으로 재생되는지 확인합니다. |
||||
@ -0,0 +1,2 @@ |
|||||
|
@echo off |
||||
|
start "" "%~dp0DCT_test\bin\Release\DCT_test.exe" |
||||
@ -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…
Reference in new issue