# DCT 테스트 서버 프로그램 구조 및 실차 로그 파일(log.txt) 연동 가이드 ## 현재 동작 및 검증 (2026-09-30) - 2026-10-06 로그 정밀도 보완: 수신 TXT의 `HH:mm:ss.fff`(밀리초)와 `HH:mm:ss.ffffff`(마이크로초), 한 파일 내 혼합 형식을 읽는다. 같은 밀리초의 모든 패킷은 파일 순서로 보존하고 마이크로초 간격·자정 경과도 유지한다. 실제 전송의 마이크로초 단위 스케줄링 정확도를 보장한다는 뜻은 아니다. 관련 검증은 Release 빌드 후 `Verify-LogPrecision.ps1`을 실행한다. - 서버 시작은 연결 대기만 수행한다. 로그 재생은 앱의 초기 설정 및 `0x20` 요청/A0 응답 후 시작한다. `0x21`은 연결을 유지한 채 재생을 정지하며, 재시작은 로그 처음부터다. - 로그 파일 재생 전용이다. 모의신호 모드·프로필·MDB/CAN/A2L 합성 값 생성 로직과 PC의 재생·일시정지·정지 버튼을 제거했다. 재생 옵션에는 배속·반복만 표시하며 시작·정지는 태블릿 앱의 요청으로 제어한다. 파일이 없으면 재생을 시작하지 않는다. - `송신 중인 신호 목록`을 제거했다. 하단은 연결된 클라이언트 목록과 전체 폭의 패킷 로그로 구성하며, 파일 로드 결과는 파일명·레코드 수·용량으로 확인한다. - 파일 재생은 ACK/송신 지연 시 기다리고, 전송 승인 실패 때문에 다음 레코드로 건너뛰지 않는다. TX 로그의 `record`와 `wire`는 해당 클라이언트에 대한 실제 패킷 쓰기 완료에 대응한다(앱 처리 완료와 다름). - `build_csharp.bat`로 Release 빌드, Windows PowerShell에서 `Verify-Transport.ps1`, `Verify-Regression.ps1`로 검증한다. - 아래의 예시 코드·과거 UI 설명보다 실제 구현 및 [운영 기능 문서](../docs/07_기능_및_제스처/01_기능_목록.md)의 NET-04 보완을 우선한다. 본 문서는 안드로이드 태블릿용 DCT 캘리브레이션 및 진단 계측 앱의 통신 테스트를 위해 제작된 C# WPF 기반 DCT 테스트 서버(DCT_test.exe)의 전체 소프트웨어 아키텍처와, 실제 차량 및 제어기에서 수집된 대규모 실차 계측 데이터(log.txt)를 프로그램과 연결하기 위한 코드 작성 가이드를 기술합니다. --- ## 목차 1. 프로그램 개요 및 실행 방법 2. 테스트 프로그램 전체 아키텍처 및 모듈 구조 3. 핵심 소스코드 파일 및 역할 명세 4. 전송 패킷 바이너리 프로토콜 명세 5. log.txt 실차 대용량 데이터 연동 메커니즘 6. 실차 로그 파서 코드 작성 가이드 및 C# 템플릿 7. 수동 검증 절차 및 시나리오 --- ## 1. 프로그램 개요 및 실행 방법 ### 1.1 프로그램 목적 - 실제 차량이나 전용 하드웨어 인터페이스 장비 없이도 안드로이드 태블릿 앱이 Wi-Fi Direct P2P 그룹의 TCP 포트 6000을 통해 실시간 계측 신호를 수신하도록 지원하는 테스트 도구입니다. - 실제 차량 시험에서 획득한 대용량 실차 데이터(log.txt)를 읽어 단말에 재전송(Replay)합니다. 신호 값을 임의로 생성하지 않습니다. ### 1.2 실행 방법 - dct_test 폴더 또는 최상위 경로의 `Run_DCT_test.bat` 배치 파일을 실행합니다. - 기본 동작 사양: - 전송 간격: 로그에 기록된 시간 간격과 선택한 배속에 따릅니다. 모의신호 전용 Hz 선택은 제거했습니다. - 데이터 소스: 로그 파일 재생 (File Playback) 전용 - 배속: 0.5x / 1x / 2x / 5x, 기본 1x. 반복 재생은 기본 활성화입니다. --- ## 2. 테스트 프로그램 전체 아키텍처 및 모듈 구조 파일 재생 워커와 WPF UI 갱신을 분리합니다. 워커는 로그 시간에 따라 순서대로 전송하고, ACK·송신 지연 시 실제 쓰기 완료까지 기다립니다. ```mermaid graph TD subgraph PresentationLayer ["WPF UI 계층"] UI["MainWindow.xaml / MainWindow.xaml.cs"] Clients["연결된 클라이언트 목록 (GridClients)"] Log["실시간 패킷 모니터링 콘솔 (TextLogConsole)"] Ctrl["재생 옵션 (배속 / 반복), 시작·정지는 태블릿 앱 요청"] end subgraph DataSourceLayer ["데이터 소스 계층"] LogReader["로그 파일 파서 (LogFileReader.cs)"] LogFile["실제 차량 주행/수신 로그 파일 (log.txt)"] LogFile --> LogReader end subgraph ProtocolLayer ["바이너리 프로토콜 계층"] Packet["DctPacket.cs / DctPacketType.cs"] Framing["헤더(0xA1) + 길이(BE) + 커맨드 + 페이로드 + 체크섬 + 푸터(0x7E)"] Packet --> Framing end subgraph NetworkLayer ["소켓 통신 계층"] Server["DctServer.cs"] TcpServer["Loopback TCP (포트 6000) - Wi-Fi Direct 브리지 후 프로토콜 처리"] P2PHost["WifiDirectHost.cs - Windows P2P 광고/수락 및 소켓 브리지"] Server --> TcpServer Server --> P2PHost end subgraph TargetClient ["외부 수신 단말"] Android["안드로이드 태블릿 앱 (DCT App)"] end LogReader -->|파일 리플레이 모드| Packet Packet --> Server P2PHost -->|Wi-Fi Direct TCP| Android TcpServer -->|브리지된 DCT 패킷| P2PHost Server -->|100ms 비동기 분리| UI ``` --- ## 3. 핵심 소스코드 파일 및 역할 명세 | 파일 경로 | 주요 역할 및 상세 기능 | | :--- | :--- | | `dct_test/DCT_test/MainWindow.xaml` | WPF UI 레이아웃 정의 (서버 제어 패널, 로그 파일 드롭존, 재생 제어부, 연결된 클라이언트 목록, 실시간 로그 콘솔) | | `dct_test/DCT_test/MainWindow.xaml.cs` | UI 이벤트 및 로그 재생 제어
• `TelemetryStreamWorker`: `LogPlaybackClock` 기반 로그 시간·배속·반복 처리와 100ms 주기 비동기 UI 갱신
• 앱의 시작·정지 요청으로 재생/취소하며 재시작은 파일 처음부터
• `LoadLogFile`: 드래그 앤 드롭 파일 수신 및 LogFileReader 결과 적재 | | `dct_test/DCT_test/LogFileReader.cs` | **실제 차량 계측 로그 파일(log.txt)을 읽어 DctPacket 바이너리 패킷으로 변환하는 전담 파서**
사용자가 보유한 실차 데이터 포맷에 맞추어 직접 파싱 코드를 작성할 수 있는 핵심 확장 지점 | | `dct_test/DCT_test/DctTcpServer.cs` | 루프백 TCP 6000 포트의 DCT 프로토콜 처리 및 패킷 브로드캐스트 | | `dct_test/DCT_test/WifiDirectHost.cs` | Windows Wi-Fi Direct 광고/연결 수락, P2P StreamSocket과 루프백 DCT 서버 간 브리지 | | `dct_test/DCT_test/DctPacket.cs` | PC 레거시 및 안드로이드 태블릿 앱 규격과 100% 호환되는 바이너리 프레임 인코딩/디코딩, 8비트 Inverted Sum 체크섬 계산, 원시 바이트 스트림 파서(`ParseStream`) 내장 | --- ## 4. 전송 패킷 바이너리 프로토콜 명세 `LogFileReader`에서 `log.txt`를 읽어 생성해야 하는 표준 `DctPacket`의 세부 바이트 구조입니다. ### 4.1 패킷 프레임 공통 구조 (6 바이트 오버헤드) ```text [0xA1] [Length: 2B (Big-Endian)] [Command: 1B] [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 Big-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 리스트, 파일 메타데이터) UI->>UI: 5. _loadedLogRecords 리스트에 패킷 적재 및 파일명·레코드 수·용량 표시 User->>App: 6. Wi-Fi Direct 연결 및 신호 재생 시작 App->>Server: 7. 초기 설정 후 0x20 요청 Server->>UI: A0 응답 완료 후 로그 재생 시작 요청 UI->>Worker: TelemetryStreamWorker 가동 (로그 시간 및 배속) loop 파일 패킷 브로드캐스트 Worker->>Server: 8. BroadcastLogPacketAsync로 레코드 송신 완료 대기 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(); // 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); 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(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < headers.Length; i++) { colMap[headers[i].Trim()] = i; } // 헬퍼 함수: 특정 신호 열이 존재하면 파싱, 없으면 기본값 반환 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. 로그 파일` 영역에서 재생할 파일을 지정합니다. - 준비한 실차 대용량 `log.txt` 파일을 드롭존 영역에 드래그 앤 드롭하거나 `[파일 찾아보기]` 버튼으로 지정합니다. - `현재 로드된 파일: [log.txt] (N 레코드, M MB)` 텍스트로 로드 결과를 확인합니다. 2. **서버 시작 및 패킷 스트리밍**: - `[서버 시작]` 버튼을 클릭하여 Wi-Fi Direct 광고와 P2P TCP 6000 포트를 시작합니다. - 태블릿 앱에서 연결한 뒤 `신호 재생`을 시작합니다. PC에는 재생·일시정지·정지 버튼이 없으며 앱의 시작·정지 요청에 따라 자동으로 동작합니다. - 하단 패킷 로그의 파일 TX 기록에서 `record`와 실제 쓰기 완료 시각 `wire`를 확인합니다. 연결된 클라이언트 목록에서 접속 정보와 송수신 패킷 수를 확인합니다. 3. **태블릿 앱 연동 및 다채널 데이터 렌더링 검증**: - 안드로이드 태블릿 앱의 Wi-Fi Direct 목록에서 PC를 선택합니다. - 차트 화면에서 `Incar_Snr`, `Speed`, `ALDC_VBVoltVal`, `BlrwLevel` 등 여러 신호를 등록합니다. - `log.txt`에 기록된 실제 주행 거동(도심 주행 가감속, 회생제동 전압, 공조 냉각 등)이 왜곡 없이 태블릿 화면에 실시간으로 재생되는지 확인합니다. ## Wi-Fi Direct 연결 테스트 서버는 Windows Wi-Fi Direct 광고를 사용해 태블릿과 P2P 그룹을 구성합니다. 일반 공유기/LAN TCP 접속은 서버에서 받지 않으며, DCT TCP 처리는 Wi-Fi Direct 소켓을 루프백(127.0.0.1)으로 브리지한 뒤 수행합니다. PC 무선 어댑터가 Wi-Fi Direct GO/Client를 지원해야 하고, 최초 빌드 시 `Microsoft.Windows.SDK.Contracts 10.0.22621.755` NuGet 패키지를 복원해야 합니다. 태블릿에서는 Wi-Fi Direct 탭에서 PC를 검색해 선택합니다.