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>
298 lines
10 KiB
C#
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();
|
|
}
|
|
}
|
|
} |