You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
451 lines
24 KiB
451 lines
24 KiB
|
2 months ago
|
# 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`에 기록된 실제 주행 거동(도심 주행 가감속, 회생제동 전압, 공조 냉각 등)이 왜곡 없이 태블릿 화면에 실시간으로 재생되는지 확인합니다.
|