# V2G EXI 구현 가이드 ## 📋 개요 V2G (Vehicle-to-Grid) EXI 인코딩/디코딩을 구현하기 위한 완전한 개발자 가이드입니다. 바이트별 구조, 비트 단위 분석, 개발 순서를 포함합니다. ## 🎯 구현 목표 - **100% VC2022 C 호환성**: 바이트 단위 완전 동일 - **멀티플랫폼**: C, VC2022, .NET Core - **18개 V2G 메시지 타입 완전 지원** ## 📊 EXI 바이너리 구조 완전 분석 ### test5.exi 바이트별 분석 (CurrentDemandReq) ``` 바이트 위치: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ... 헥스 데이터: 80 98 02 10 50 90 8C 0C 0C 0E 0C 50 D1 00 32 01 ... 총 크기: 43 바이트 (순수 EXI, 네트워크 헤더 없음) ``` #### 바이트 0: EXI 헤더 (0x80) ``` 비트 구조: 10000000 의미: EXI 매직 넘버, 모든 EXI 파일은 이 바이트로 시작 구현: writeEXIHeader() 함수에서 생성 ``` #### 바이트 1: Document Choice (0x98) ``` 비트 구조: 10011000 분해: 1 (1비트) = V2G_Message 존재 플래그 0011000 (7비트) = Document choice 76 (decimal) 의미: V2G_Message를 포함하는 EXI Document 구현: encodeNBitUnsignedInteger(stream, 7, 76) ``` #### 바이트 2-3: Grammar States (0x02 0x10) ``` 바이트 2 (0x02): 00000010 0 (1비트) = Grammar 256: Header START_ELEMENT 0 (1비트) = Grammar 257: Body START_ELEMENT 000010 (6비트) = 패딩 + 다음 바이트로 이어짐 바이트 3 (0x10): 00010000 00010000 (8비트) = SessionID 인코딩 시작 ``` #### 바이트 4-11: SessionID (BINARY_HEX, 8바이트) ``` 원본 문자열: "ABB00081" ASCII 바이트: 41 42 42 30 30 30 38 31 EXI 압축 결과: 50 90 8C 0C 0C 0E 0C 50 압축 과정: - 길이 인코딩: encodeUnsignedInteger16(8) - 바이트 데이터 인코딩: encodeBytes() - BINARY_HEX 타입으로 처리 ``` #### 바이트 12: Body Choice + Grammar Start (0xD1) ``` 비트 구조: 11010001 분해: 110100 (6비트) = Body choice 13 (CurrentDemandReq) 01 (2비트) = Grammar 273, 274 상태 전환 ``` #### 바이트 13-15: DC_EVStatus (0x00 0x32 0x01) ``` 구조: EVReady: true (1비트) EVErrorCode: 0 (가변길이) EVRESSSOC: 100 (가변길이, 100%) 비트 패킹: 0x00: EVReady=1, EVErrorCode=0 시작 0x32: EVErrorCode 완료, EVRESSSOC 시작 0x01: EVRESSSOC=100 완료 ``` #### 바이트 16-18: EVTargetCurrent (0x86 0x00 0x20) ``` PhysicalValue 구조: Multiplier: 0 (3비트, offset +3 → 인코딩값 3) Unit: 3 (A, 3비트) Value: 1 (16비트 signed integer) 비트 분해: START_ELEMENT(Multiplier) + CHARACTERS + 값 3 + EE START_ELEMENT(Unit) + CHARACTERS + 값 3 + EE START_ELEMENT(Value) + CHARACTERS + 값 1 + EE END_ELEMENT ``` #### 바이트 19-22: EVMaximumVoltageLimit (0x18 0x81 0xAE 0x06) ``` PhysicalValue: Multiplier: 0 → 인코딩값 3 Unit: 4 (V) Value: 471 실제 값: 471 * 10^0 = 471V ``` #### 바이트 23-26: EVMaximumCurrentLimit (0x01 0x86 0x0C 0x80) ``` PhysicalValue: Multiplier: 0 → 인코딩값 3 Unit: 3 (A) Value: 100 실제 값: 100 * 10^0 = 100A ``` #### 바이트 27-29: EVMaximumPowerLimit (0x61 0x40 0xC8) ``` PhysicalValue: Multiplier: 3 → 인코딩값 6 Unit: 5 (W) Value: 50 실제 값: 50 * 10^3 = 50,000W = 50kW ``` #### 바이트 30: ChargingComplete (0x01) ``` Grammar 278: 2비트 choice = 1 (ChargingComplete 선택) Boolean 값: true 비트 구조: 00000001 ``` #### 바이트 31-33: RemainingTimeToFullSoC (0x03 0x08 0x00) ``` PhysicalValue: Multiplier: 0 Unit: 2 (s, 초) Value: 0 실제 값: 0초 (이미 완전 충전) ``` #### 바이트 34-36: RemainingTimeToBulkSoC (0x00 0x61 0x00) ``` PhysicalValue: Multiplier: 0 Unit: 2 (s) Value: 0 실제 값: 0초 (벌크 충전도 완료) ``` #### 바이트 37-40: EVTargetVoltage (0x00 0x18 0x81 0x98) ``` PhysicalValue: Multiplier: 0 Unit: 4 (V) Value: 460 실제 값: 460V (목표 전압) ``` #### 바이트 41-42: END_ELEMENT + 패딩 (0x06 0x00) ``` Grammar 3: END_ELEMENT (1비트) 나머지: 0으로 패딩하여 바이트 경계 맞춤 ``` ## 🔧 구현 단계별 가이드 ### 1단계: BitStream 구현 #### 핵심 구조체 ```c typedef struct { uint8_t* data; // 바이트 배열 size_t* pos; // 현재 위치 포인터 uint8_t buffer; // 비트 버퍼 (0-255) uint8_t capacity; // 남은 비트 수 (0-8) } bitstream_t; ``` #### writeBits 함수 (핵심) ```c int writeBits(bitstream_t* stream, size_t nbits, uint32_t val) { if (nbits <= stream->capacity) { // 단순 케이스: 현재 버퍼에 맞음 val = val << (stream->capacity - nbits); stream->buffer = stream->buffer | ((uint8_t)val); stream->capacity -= nbits; if (stream->capacity == 0) { stream->data[(*stream->pos)++] = stream->buffer; stream->buffer = 0; stream->capacity = 8; } } else { // 복잡 케이스: 바이트 경계 넘어감 while (nbits > stream->capacity) { nbits -= stream->capacity; uint8_t val1 = (uint8_t)(val >> nbits); stream->buffer |= val1; stream->data[(*stream->pos)++] = stream->buffer; val &= (((uint32_t)1 << nbits) - 1); stream->buffer = 0; stream->capacity = 8; } if (nbits > 0) { val = val << (stream->capacity - nbits); stream->buffer |= (uint8_t)val; stream->capacity -= nbits; } } return 0; } ``` ### 2단계: 기본 인코딩 함수들 #### Boolean 인코딩 ```c int encodeBoolean(bitstream_t* stream, int b) { return writeBits(stream, 1, b ? 1 : 0); } ``` #### N비트 부호없는 정수 ```c int encodeNBitUnsignedInteger(bitstream_t* stream, size_t nbits, uint32_t val) { return writeBits(stream, nbits, val); } ``` #### 가변길이 부호없는 정수 ```c int encodeUnsignedInteger16(bitstream_t* stream, uint16_t val) { if (val < 128) { return writeBits(stream, 8, val); } else { writeBits(stream, 8, 0x80 | (val >> 8)); return writeBits(stream, 8, val & 0xFF); } } ``` #### 16비트 signed integer (핵심) ```c int encodeInteger16(bitstream_t* stream, int16_t val) { // 부호 비트 (1비트) if (val < 0) { encodeBoolean(stream, 1); val = (int16_t)((-val) - 1); } else { encodeBoolean(stream, 0); } // 크기를 가변길이 부호없는 정수로 인코딩 return encodeUnsignedInteger16(stream, (uint16_t)val); } ``` ### 3단계: Grammar State 구현 #### Grammar State 매핑 ```c typedef enum { GRAMMAR_256 = 256, // V2G_Message Header GRAMMAR_257 = 257, // V2G_Message Body GRAMMAR_273 = 273, // CurrentDemandReq start GRAMMAR_274 = 274, // EVTargetCurrent GRAMMAR_275 = 275, // Optional elements (3-bit) GRAMMAR_276 = 276, // After EVMaxVoltage (3-bit) GRAMMAR_277 = 277, // After EVMaxCurrent (2-bit) GRAMMAR_278 = 278, // After EVMaxPower (2-bit) GRAMMAR_280 = 280, // After ChargingComplete (2-bit) GRAMMAR_281 = 281, // After RemainingTimeFullSoC (2-bit) GRAMMAR_282 = 282, // After RemainingTimeBulkSoC (1-bit) GRAMMAR_3 = 3 // END_ELEMENT } GrammarState; ``` #### Choice 비트 수 계산 ```c int getChoiceBits(GrammarState state) { switch(state) { case GRAMMAR_275: return 3; // 5개 선택지 (0-4) case GRAMMAR_276: return 3; // 4개 선택지 (0-3) case GRAMMAR_277: return 2; // 3개 선택지 (0-2) case GRAMMAR_278: return 2; // 2개 선택지 (0-1) case GRAMMAR_280: return 2; // 3개 선택지 (0-2) case GRAMMAR_281: return 2; // 3개 선택지 (0-2) case GRAMMAR_282: return 1; // 2개 선택지 (0-1) default: return 1; // 기본값 } } ``` ### 4단계: PhysicalValue 인코딩 #### 완전한 구현 ```c int encode_PhysicalValue(bitstream_t* stream, PhysicalValue* pv) { // START_ELEMENT(Multiplier) encodeNBitUnsignedInteger(stream, 1, 0); encodeNBitUnsignedInteger(stream, 1, 0); // CHARACTERS[3BIT] encodeNBitUnsignedInteger(stream, 3, (uint32_t)(pv->Multiplier + 3)); encodeNBitUnsignedInteger(stream, 1, 0); // EE // START_ELEMENT(Unit) encodeNBitUnsignedInteger(stream, 1, 0); encodeNBitUnsignedInteger(stream, 1, 0); // CHARACTERS[ENUM] encodeNBitUnsignedInteger(stream, 3, pv->Unit); encodeNBitUnsignedInteger(stream, 1, 0); // EE // START_ELEMENT(Value) encodeNBitUnsignedInteger(stream, 1, 0); encodeNBitUnsignedInteger(stream, 1, 0); // CHARACTERS[INTEGER] encodeInteger16(stream, pv->Value); encodeNBitUnsignedInteger(stream, 1, 0); // EE // END_ELEMENT encodeNBitUnsignedInteger(stream, 1, 0); return 0; } ``` ### 5단계: 메시지별 구현 #### CurrentDemandReq 완전 구현 ```c int encode_CurrentDemandReq(bitstream_t* stream, CurrentDemandReqType* req) { int grammar = 273; while (grammar != 3) { switch(grammar) { case 273: // DC_EVStatus (mandatory) encodeNBitUnsignedInteger(stream, 1, 0); encode_DC_EVStatus(stream, &req->DC_EVStatus); grammar = 274; break; case 274: // EVTargetCurrent (mandatory) encodeNBitUnsignedInteger(stream, 1, 0); encode_PhysicalValue(stream, &req->EVTargetCurrent); grammar = 275; break; case 275: // 3비트 choice (5개 옵션) if (req->EVMaximumVoltageLimit_isUsed) { encodeNBitUnsignedInteger(stream, 3, 0); encode_PhysicalValue(stream, &req->EVMaximumVoltageLimit); grammar = 276; } else if (req->EVMaximumCurrentLimit_isUsed) { encodeNBitUnsignedInteger(stream, 3, 1); encode_PhysicalValue(stream, &req->EVMaximumCurrentLimit); grammar = 277; } else if (req->EVMaximumPowerLimit_isUsed) { encodeNBitUnsignedInteger(stream, 3, 2); encode_PhysicalValue(stream, &req->EVMaximumPowerLimit); grammar = 278; } else { // ChargingComplete (기본값) encodeNBitUnsignedInteger(stream, 3, 4); encodeNBitUnsignedInteger(stream, 1, 0); // CHARACTERS[BOOLEAN] encodeBoolean(stream, req->ChargingComplete); encodeNBitUnsignedInteger(stream, 1, 0); // EE grammar = 280; } break; case 278: // 2비트 choice - BulkCharging 무시 // ChargingComplete로 바로 이동 encodeNBitUnsignedInteger(stream, 2, 1); encodeNBitUnsignedInteger(stream, 1, 0); encodeBoolean(stream, req->ChargingComplete); encodeNBitUnsignedInteger(stream, 1, 0); grammar = 280; break; // ... 다른 grammar states case 3: // END_ELEMENT encodeNBitUnsignedInteger(stream, 1, 0); break; } } return 0; } ``` ## 🔄 디코딩 구현 가이드 ### 1단계: BitStream 읽기 #### readBits 함수 ```c int readBits(bitstream_t* stream, size_t nbits, uint32_t* val) { *val = 0; while (nbits > 0) { if (stream->capacity == 0) { if (*stream->pos >= stream->size) return -1; stream->buffer = stream->data[(*stream->pos)++]; stream->capacity = 8; } int bitsToRead = (nbits < stream->capacity) ? nbits : stream->capacity; *val = (*val << bitsToRead) | ((stream->buffer >> (stream->capacity - bitsToRead)) & ((1 << bitsToRead) - 1)); stream->capacity -= bitsToRead; nbits -= bitsToRead; } return 0; } ``` ### 2단계: 타입별 디코딩 #### Integer16 디코딩 ```c int decodeInteger16(bitstream_t* stream, int16_t* val) { uint32_t sign; readBits(stream, 1, &sign); uint16_t magnitude; decodeUnsignedInteger16(stream, &magnitude); if (sign) { *val = -((int16_t)magnitude + 1); } else { *val = (int16_t)magnitude; } return 0; } ``` ### 3단계: Grammar 기반 디코딩 #### 동적 Grammar 처리 ```c int decode_CurrentDemandReq(bitstream_t* stream, CurrentDemandReqType* req) { int grammar = 273; while (grammar != 3) { switch(grammar) { case 275: { uint32_t choice; readBits(stream, 3, &choice); // 3비트 choice switch(choice) { case 0: req->EVMaximumVoltageLimit_isUsed = 1; decode_PhysicalValue(stream, &req->EVMaximumVoltageLimit); grammar = 276; break; case 1: req->EVMaximumCurrentLimit_isUsed = 1; decode_PhysicalValue(stream, &req->EVMaximumCurrentLimit); grammar = 277; break; // ... 다른 choices } break; } } } return 0; } ``` ## 🛠️ 구현 우선순위 ### Phase 1: 핵심 인프라 (필수) 1. **BitStream 구현**: writeBits/readBits 완벽 구현 2. **기본 타입**: Boolean, Integer16, UnsignedInteger 3. **EXI 헤더**: writeEXIHeader/readEXIHeader 4. **Grammar States**: 상태 머신 기본 구조 ### Phase 2: CurrentDemand 메시지 (검증용) 1. **CurrentDemandReq**: 가장 복잡한 메시지, 테스트 데이터 있음 2. **CurrentDemandRes**: Response 메시지 3. **test5.exi 완벽 재현**: 43바이트 바이너리 매칭 ### Phase 3: 추가 메시지 타입 1. **세션 관리**: SessionSetup, ServiceDiscovery 2. **DC 충전**: CableCheck, PreCharge, PowerDelivery 3. **확장 기능**: Authorization, ChargingStatus ## 🔍 디버깅 가이드 ### 바이트별 검증 방법 ```c void debug_compare_bytes(uint8_t* expected, uint8_t* actual, size_t size) { printf("위치 예상 실제 차이\n"); for (size_t i = 0; i < size; i++) { char diff = (expected[i] == actual[i]) ? ' ' : '*'; printf("%02zu: %02X %02X %c\n", i, expected[i], actual[i], diff); if (expected[i] != actual[i]) { printf(" 비트: %08b vs %08b\n", expected[i], actual[i]); } } } ``` ### Grammar State 추적 ```c #define DEBUG_GRAMMAR(state, choice, bits) \ printf("Grammar %d: choice %d (%d-bit)\n", state, choice, bits) ``` ### 성공 조건 1. **test5.exi 완전 매칭**: 43바이트 바이너리 동일 2. **라운드트립 테스트**: EXI → XML → EXI 완벽 복원 3. **모든 메시지 타입**: 18개 V2G 메시지 지원 --- 이 가이드를 따라 구현하면 VC2022 C 구현과 100% 호환되는 V2G EXI 코덱을 개발할 수 있습니다.