using System; using System.IO; using System.Text; using System.Linq; using System.Collections.Generic; namespace V2GProtocol { public class V2GDecoder { private const byte V2G_TP_VERSION = 0x01; private const byte V2G_TP_INVERSE_VERSION = 0xFE; public enum V2GPayloadType : ushort { EXI_Encoded_V2G_Message = 0x8001, DIN_Message = 0x9000, SAP_Message = 0x9001 } public class V2GMessage { public byte Version { get; set; } public byte InverseVersion { get; set; } public V2GPayloadType PayloadType { get; set; } public uint PayloadLength { get; set; } public byte[] Payload { get; set; } public bool IsValid { get; set; } public string DecodedContent { get; set; } } public static V2GMessage DecodeMessage(byte[] data) { var message = new V2GMessage(); if (data.Length < 8) { message.IsValid = false; return message; } int offset = 0; // V2G Transfer Protocol Header (8 bytes) message.Version = data[offset++]; message.InverseVersion = data[offset++]; message.PayloadType = (V2GPayloadType)((data[offset] << 8) | data[offset + 1]); offset += 2; message.PayloadLength = (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); offset += 4; // Validate header message.IsValid = (message.Version == V2G_TP_VERSION && message.InverseVersion == V2G_TP_INVERSE_VERSION); if (message.IsValid && offset + message.PayloadLength <= data.Length) { message.Payload = new byte[message.PayloadLength]; Array.Copy(data, offset, message.Payload, 0, (int)message.PayloadLength); // Try to decode payload based on type message.DecodedContent = DecodePayload(message.Payload, message.PayloadType); } return message; } private static string DecodePayload(byte[] payload, V2GPayloadType payloadType) { var sb = new StringBuilder(); switch (payloadType) { case V2GPayloadType.EXI_Encoded_V2G_Message: sb.AppendLine("EXI Encoded V2G Message:"); sb.AppendLine(DecodeEXI(payload)); break; case V2GPayloadType.DIN_Message: sb.AppendLine("DIN Message:"); sb.AppendLine(DecodeDIN(payload)); break; case V2GPayloadType.SAP_Message: sb.AppendLine("SAP Message:"); sb.AppendLine(DecodeSAP(payload)); break; default: sb.AppendLine($"Unknown payload type: 0x{(ushort)payloadType:X4}"); sb.AppendLine("Raw payload:"); sb.AppendLine(BytesToHex(payload)); break; } return sb.ToString(); } private static string DecodeEXI(byte[] payload) { var sb = new StringBuilder(); // EXI 매직 바이트 체크 ($EXI) if (payload.Length >= 4 && payload[0] == 0x24 && payload[1] == 0x45 && payload[2] == 0x58 && payload[3] == 0x49) { sb.AppendLine("EXI Magic Cookie found: $EXI"); // EXI 헤더 파싱 시도 int offset = 4; if (offset < payload.Length) { byte distinguishingBits = payload[offset++]; sb.AppendLine($"EXI Distinguishing Bits: 0x{distinguishingBits:X2}"); // EXI 바디 데이터 if (offset < payload.Length) { sb.AppendLine("EXI Body:"); sb.AppendLine(TryDecodeXMLContent(payload, offset)); } } } else { // EXI 매직 바이트가 없으면 직접 EXI 디코딩 시도 sb.AppendLine("No EXI magic cookie, attempting direct EXI decode:"); sb.AppendLine(TryDecodeXMLContent(payload, 0)); } sb.AppendLine("\nRaw EXI Data:"); sb.AppendLine(BytesToHex(payload)); return sb.ToString(); } private static string DecodeDIN(byte[] payload) { var sb = new StringBuilder(); sb.AppendLine("DIN SPEC 70121 Message"); sb.AppendLine(TryDecodeXMLContent(payload, 0)); sb.AppendLine("\nRaw DIN Data:"); sb.AppendLine(BytesToHex(payload)); return sb.ToString(); } private static string DecodeSAP(byte[] payload) { var sb = new StringBuilder(); sb.AppendLine("SAP Message"); if (payload.Length >= 20) { // SAP 헤더 파싱 sb.AppendLine("SAP Header:"); sb.AppendLine($" Message Type: 0x{payload[0]:X2}"); sb.AppendLine($" Session ID: {BitConverter.ToUInt64(payload, 1)}"); sb.AppendLine($" Protocol: 0x{payload[9]:X2}"); } sb.AppendLine("\nRaw SAP Data:"); sb.AppendLine(BytesToHex(payload)); return sb.ToString(); } private static string TryDecodeXMLContent(byte[] payload, int offset) { var sb = new StringBuilder(); // ASCII 텍스트로 디코드 시도 try { string asciiText = Encoding.ASCII.GetString(payload, offset, payload.Length - offset); if (ContainsXMLLikeContent(asciiText)) { sb.AppendLine("Possible XML content (ASCII):"); sb.AppendLine(asciiText); } } catch { } // UTF-8 디코드 시도 try { string utf8Text = Encoding.UTF8.GetString(payload, offset, payload.Length - offset); if (ContainsXMLLikeContent(utf8Text)) { sb.AppendLine("Possible XML content (UTF-8):"); sb.AppendLine(utf8Text); } } catch { } // V2G 메시지 키워드 검색 sb.AppendLine(SearchV2GKeywords(payload, offset)); return sb.ToString(); } private static bool ContainsXMLLikeContent(string text) { return text.Contains("<") && text.Contains(">") || text.Contains("SessionSetup") || text.Contains("ChargeParameter") || text.Contains("PowerDelivery") || text.Contains("V2G_Message"); } private static string SearchV2GKeywords(byte[] payload, int offset) { var sb = new StringBuilder(); var keywords = new string[] { "SessionSetupReq", "SessionSetupRes", "ServiceDiscoveryReq", "ServiceDiscoveryRes", "ServiceDetailReq", "ServiceDetailRes", "PaymentServiceSelectionReq", "PaymentServiceSelectionRes", "ChargeParameterDiscoveryReq", "ChargeParameterDiscoveryRes", "PowerDeliveryReq", "PowerDeliveryRes", "ChargingStatusReq", "ChargingStatusRes", "MeteringReceiptReq", "MeteringReceiptRes", "SessionStopReq", "SessionStopRes", "V2G_Message", "Header", "Body" }; var found = new List(); string dataAsString = Encoding.ASCII.GetString(payload, offset, payload.Length - offset); foreach (var keyword in keywords) { if (dataAsString.Contains(keyword, StringComparison.OrdinalIgnoreCase)) { found.Add(keyword); } } if (found.Any()) { sb.AppendLine("Found V2G keywords:"); foreach (var keyword in found) { sb.AppendLine($" - {keyword}"); } } return sb.ToString(); } public static string BytesToHex(byte[] bytes) { var sb = new StringBuilder(); for (int i = 0; i < bytes.Length; i += 16) { sb.Append($"{i:X4} "); // Hex bytes for (int j = 0; j < 16; j++) { if (i + j < bytes.Length) sb.Append($"{bytes[i + j]:X2} "); else sb.Append(" "); } sb.Append(" "); // ASCII representation for (int j = 0; j < 16 && i + j < bytes.Length; j++) { byte b = bytes[i + j]; sb.Append(b >= 32 && b <= 126 ? (char)b : '.'); } sb.AppendLine(); } return sb.ToString(); } public static byte[] ParseHexFile(string filePath) { var bytes = new List(); foreach (string line in File.ReadAllLines(filePath)) { var parts = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { // Skip line numbers and ASCII part if (part.Contains("→") || part.Length != 2) continue; if (byte.TryParse(part, System.Globalization.NumberStyles.HexNumber, null, out byte b)) { bytes.Add(b); } } } return bytes.ToArray(); } } }