# DCT 테스트 서버 프로그램 구조 및 실차 로그 파일(log.txt) 연동 가이드
## 현재 동작 및 검증 (2026-09-30)
- 2026-10-12 PC 핫스팟 AP 모드를 추가했다. 현재 앱의 `192.168.4.1:5000` 고정 endpoint와 Direct/기본 `6000` 모드를 유지한다. 아래 수동 IP 설정이 필요하다.
## 외부 공유기/AP 모드 (2026-10-12 후속)
Android 후속 변경: 새 앱은 Wi-Fi `⋮ → IP / 포트 설정 → PC가 표시한 동일 IPv4/포트 → 저장 후 연결` 을 지원한다. 기본192.168.4.1:5000은 유지하며 일반 LAN 대역을 허용한다. IP/포트 설정 APK는 SM-X920에2026-10-12 10:35:45 KST 데이터 보존 설치·앱 실행 확인(686ms). 실제 PC/앱 통합 검증은 대기이며 [Android 검증 ](../docs/07_기능_및_제스처/04_검증_체크리스트.md )을 따른다. 아래 PC 변경 당시 고정 앱 제약과 구분한다.
현재 사용할 구성은 **PC의 현재 IP와 입력 포트**다. Android 앱 수정은 다른 채팅에서 진행하며 이 변경은 PC 테스트 프로그램만 수정했다. 외부 AP에서는 핫스팟 설정 도구를 표시하지 않는다. 기존 Direct·PC 핫스팟 선택도 보존한다.
1. PC와 태블릿을 같은 공유기의 일반 LAN/Wi-Fi에 연결한다. 기존 IP/DHCP/공유기 주소를 그대로 사용한다. 게스트 네트워크/AP isolation/무선-LAN 격리는 사용하지 않는다.
2. 테스트 서버에서 **외부 공유기/AP**를 선택하고 IP 목록에서 공유기에 연결된 PC의 현재 IPv4 주소를 선택한다. 여러 어댑터가 있으면 태블릿이 접근 가능한 주소를 선택한다. 목록은 모드를 선택할 때 갱신한다.
3. 포트를 입력한다(기본6000, 허용1~65535). 예: PC가192.168.0.20이면192.168.0.20:6000에서 대기한다. 다른 프로그램이 해당 주소/포트를 사용 중이거나 IP가 사라지면 시작 실패를 표시한다.
4. PC 방화벽에서 새 실행 파일/선택한 TCP 포트의 수신을 같은 공유기 네트워크에 허용한다. 기존 핫스팟 전용 규칙은 외부 AP 주소/포트에 자동 적용되지 않는다. 물리 어댑터/IP/방화벽은 자동 변경하지 않는다.
5. 로그 선택 → 서버 시작. 상태와 로그에서 실제 IP:포트를 확인하고, 다른 채팅에서 수정 중인 앱에 동일 IP/포트를 설정한다. 로그와 맞는 프로젝트로 계측 시작/중지·기록을 확인한다. PC는 선택 주소에만 바인딩하며 P2P 광고/UDP 탐색은 실행하지 않는다. 실행 중 IP/포트/모드 잠금, 중지 후 다시 편집할 수 있다. IP 복사 버튼도 제공한다.
새 빌드는 `DCT_test/bin/CurrentIpAp/DCT_test.exe` 다. 기존 프로그램을 종료한 뒤 새 파일을 실행한다. **고정192.168.4.1:5000 앱을 이 PC 변경만으로 임의 IP에 접속시킬 수는 없다.** 실제 무선·앱 통합은 별도 검증이며 [현재 PC 검증 ](../docs/07_기능_및_제스처/04_검증_체크리스트.md )을 따른다. 아래 핫스팟 모드의 고정 주소는 그 모드에서만 유지한다.
## PC 자체 핫스팟 AP 모드 (2026-10-12)
1. 모드에서 **PC 핫스팟 AP** → ** ① Windows 핫스팟 설정**을 선택하고 Windows 모바일 핫스팟을 켠다. SSID/암호·핫스팟 생성은 Windows 설정에서 관리한다. 서버 시작은 핫스팟을 자동 생성하지 않는다.
2. ** ② AP IP 설정 (관리자)**에서 UAC 승인. 실행 파일 옆 `Configure-HotspotAp.ps1` 은 활성 Microsoft Wi-Fi Direct Virtual Adapter가 정확히 하나이고 기본 `192.168.137.1` 또는 기존 `192.168.4.1` 주소를 가진 경우만 `192.168.4.1/24` 별칭을 추가한다. 물리 어댑터/기존 주소는 보존하며 다중 어댑터·주소 충돌·다른 기본 대역·DHCP 클라이언트 설정이면 거부한다. 방화벽은 해당 실행 파일/TCP5000/로컬192.168.4.1/원격192.168.4.0/24만 허용한다.
3. 태블릿 **시스템 Wi-Fi 설정**에서 PC SSID에 연결하고 IP를 **수동/고정**으로 설정한다: `192.168.4.2` , 접두사 `24` , 게이트웨이/DNS `192.168.4.1` . 다른 태블릿은 중복 없는 .3~.254를 사용한다. 인터넷 없음은 정상이며 Windows DHCP의 기본 주소는 현재 앱과 호환되지 않는다.
4. PC에서 로그 선택 → **서버 시작** , 앱에서 **현재 AP로 통신 연결** . 앱에서 새 AP 요청을 하면 DHCP 주소를 받을 수 있으므로 수동 설정된 시스템 연결 경로를 사용한다. AP 서버는 `192.168.4.1:5000` 에만 바인딩하고 P2P 광고/브리지·UDP 탐색을 시작하지 않는다. 주소 누락·포트 점유는 시작 실패로 표시한다.
5. 로그와 맞는 프로젝트로 계측 시작/중지·기록을 확인한다. 설정/ACK·배속·반복·STOP 후 처음부터 재생은 기존과 같다. Direct 모드는 기존 광고/브리지와 편집 가능한 포트(기본6000)를 유지한다. 서버 실행 중 모드/IP 설정은 잠긴다.
서버 종료는 사용자 핫스팟/IP/방화벽을 삭제하지 않는다. 핫스팟 재시작 후 주소가 사라지면 IP 도구를 재실행한다. 복구 시 핫스팟 어댑터에 추가한 `192.168.4.1` 주소와 `DCT-Test-Hotspot-AP-5000` 규칙만 제거하고 태블릿 IP를 DHCP로 복원한다. 지원 드라이버/인터넷 공유 조건·UAC·실제 무선/태블릿 연결은 별도 확인하며 [NET-04 검증 ](../docs/07_기능_및_제스처/04_검증_체크리스트.md )을 따른다. Android 소스는 변경하지 않았다.
- 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 이벤트 및 로그 재생 제어< br > • `TelemetryStreamWorker` : `LogPlaybackClock` 기반 로그 시간·배속·반복 처리와 100ms 주기 비동기 UI 갱신< br > • 앱의 시작·정지 요청으로 재생/취소하며 재시작은 파일 처음부터< br > • `LoadLogFile` : 드래그 앤 드롭 파일 수신 및 LogFileReader 결과 적재 |
| `dct_test/DCT_test/LogFileReader.cs` | **실제 차량 계측 로그 파일(log.txt)을 읽어 DctPacket 바이너리 패킷으로 변환하는 전담 파서** < br > 사용자가 보유한 실차 데이터 포맷에 맞추어 직접 파싱 코드를 작성할 수 있는 핵심 확장 지점 |
| `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< 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);
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;
}
// 헬퍼 함수: 특정 신호 열이 존재하면 파싱, 없으면 기본값 반환
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를 검색해 선택합니다.