Files
V2GProtocol_CSharp/V2GDecoder.cs
Arin(asus) 8a718f5d4f Add V2G Transfer Protocol decoder/encoder
Implements C# console application for decoding ISO 15118-2, DIN SPEC 70121, and SAP protocol messages from binary data. Features include:
- V2G Transfer Protocol header parsing
- EXI, DIN, and SAP message type support
- Network packet analysis (Ethernet/IPv6/TCP)
- Hex dump utilities
- Raw data parsing from hex files
- Sample raw data for testing

Successfully tested with sample V2G EXI message data.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-07 13:29:17 +09:00

298 lines
10 KiB
C#

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>();
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<byte>();
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();
}
}
}