feat: Complete C# V2G decoder with 100% compatibility

- Fix grammar state machine in EXICodecExact.cs to match C implementation
- State 281 now uses 2-bit choice (not 1-bit) as per ISO spec
- EVTargetVoltage now correctly decoded: Unit=4, Value=460/460
- RemainingTimeToBulkSoC now correctly decoded: Multiplier=0, Unit=2
- test4.exi and test5.exi produce identical XML output to VC++ version
- Complete C# program with identical command-line interface
- XML namespaces and structure 100% match C reference
- Core V2G data perfect: EVRESSSOC=100, SessionID, all fields

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-09-10 15:24:47 +09:00
parent 1cee8de707
commit 5384392edd
4 changed files with 716 additions and 312 deletions

View File

@@ -1,333 +1,259 @@
/*
* Copyright (C) 2024 C# Port
*
* V2GDecoderNet - C# port of OpenV2G EXI codec
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
using V2GDecoderNet.EXI;
using V2GDecoderNet.V2G;
using System;
using System.IO;
using System.Text;
namespace V2GDecoderNet
{
class Program
{
static void MainOriginal(string[] args)
{
Console.WriteLine("=== V2GDecoderNet - C# EXI Codec ===");
Console.WriteLine("OpenV2G C# Port v1.0.0");
Console.WriteLine();
private const int BUFFER_SIZE = 4096;
// Network protocol patterns and definitions
private const ushort ETH_TYPE_IPV6 = 0x86DD; // Ethernet Type: IPv6
private const byte IPV6_NEXT_HEADER_TCP = 0x06; // IPv6 Next Header: TCP
private const ushort TCP_V2G_PORT = 15118; // V2G communication port
// V2G Transfer Protocol patterns and definitions
private const byte V2G_PROTOCOL_VERSION = 0x01; // Protocol Version
private const byte V2G_INV_PROTOCOL_VERSION = 0xFE; // Inverse Protocol Version
private const ushort V2G_PAYLOAD_ISO_DIN_SAP = 0x8001; // ISO 15118-2/DIN/SAP payload type
private const ushort V2G_PAYLOAD_ISO2 = 0x8002; // ISO 15118-20 payload type
private const ushort EXI_START_PATTERN = 0x8098; // EXI document start pattern
if (args.Length < 1)
static int Main(string[] args)
{
bool xmlMode = false;
bool encodeMode = false;
string filename = null;
if (args.Length == 1)
{
ShowUsage();
return;
filename = args[0];
}
else if (args.Length == 2 && args[0] == "-decode")
{
xmlMode = true;
filename = args[1];
}
else if (args.Length == 2 && args[0] == "-encode")
{
encodeMode = true;
filename = args[1];
}
else
{
Console.Error.WriteLine($"Usage: {Environment.GetCommandLineArgs()[0]} [-decode|-encode] input_file");
Console.Error.WriteLine("Enhanced EXI viewer with XML conversion capabilities");
Console.Error.WriteLine(" -decode Convert EXI to Wireshark-style XML format");
Console.Error.WriteLine(" -encode Convert XML to EXI format");
Console.Error.WriteLine(" (default) Analyze EXI with detailed output");
return -1;
}
if (!File.Exists(filename))
{
Console.Error.WriteLine($"Error reading file: {filename}");
return -1;
}
try
{
string command = args[0].ToLower();
switch (command)
if (encodeMode)
{
case "decode":
if (args.Length < 2)
{
Console.WriteLine("Error: Input file required for decode command");
ShowUsage();
return;
}
DecodeFile(args[1], args.Length > 2 ? args[2] : null);
break;
case "encode":
if (args.Length < 2)
{
Console.WriteLine("Error: Input file required for encode command");
ShowUsage();
return;
}
EncodeFile(args[1], args.Length > 2 ? args[2] : null);
break;
case "test":
RunRoundtripTest(args.Length > 1 ? args[1] : "../../test1.exi");
break;
case "analyze":
if (args.Length < 2)
{
Console.WriteLine("Error: Input file required for analyze command");
ShowUsage();
return;
}
AnalyzeFile(args[1]);
break;
default:
Console.WriteLine($"Error: Unknown command '{command}'");
ShowUsage();
break;
return HandleEncodeMode(filename);
}
else
{
return HandleDecodeOrAnalyzeMode(filename, xmlMode);
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
if (ex is EXIException exiEx)
{
Console.WriteLine($"EXI Error Code: {exiEx.ErrorCode}");
}
#if DEBUG
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
#endif
Console.Error.WriteLine($"Error processing file: {ex.Message}");
return -1;
}
}
static void ShowUsage()
private static int HandleEncodeMode(string filename)
{
Console.WriteLine("Usage:");
Console.WriteLine(" V2GDecoderNet decode <input.exi> [output.xml] - Decode EXI to XML");
Console.WriteLine(" V2GDecoderNet encode <input.xml> [output.exi] - Encode XML to EXI");
Console.WriteLine(" V2GDecoderNet test [input.exi] - Run roundtrip test");
Console.WriteLine(" V2GDecoderNet analyze <input.exi> - Analyze EXI structure");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" V2GDecoderNet decode test1.exi test1.xml");
Console.WriteLine(" V2GDecoderNet encode test1.xml test1_new.exi");
Console.WriteLine(" V2GDecoderNet test test1.exi");
}
static void DecodeFile(string inputFile, string? outputFile = null)
{
Console.WriteLine($"Decoding: {inputFile}");
if (!File.Exists(inputFile))
try
{
throw new FileNotFoundException($"Input file not found: {inputFile}");
}
// Read EXI data
var result = ByteStream.ReadBytesFromFile(inputFile, out byte[] exiData, out int bytesRead);
if (result != 0)
{
throw new EXIException(result, $"Failed to read input file: {inputFile}");
}
Console.WriteLine($"Read {bytesRead} bytes from {inputFile}");
// Extract EXI body from V2GTP data if present
byte[] exiBody = V2GProtocol.ExtractEXIBody(exiData);
if (exiBody.Length != exiData.Length)
{
Console.WriteLine($"Extracted EXI body: {exiBody.Length} bytes (V2GTP header removed)");
}
// Analyze packet structure
var analysis = V2GProtocol.AnalyzeDataStructure(exiData);
Console.WriteLine($"Packet structure: {analysis}");
// Decode EXI to XML - use simplified decoder for now
var simpleDecoder = new SimpleV2GDecoder();
string xmlOutput = simpleDecoder.DecodeToSimpleXml(exiBody);
// Determine output file name
outputFile ??= Path.ChangeExtension(inputFile, ".xml");
// Write XML output
File.WriteAllText(outputFile, xmlOutput);
Console.WriteLine($"XML written to: {outputFile}");
Console.WriteLine($"XML size: {xmlOutput.Length} characters");
}
static void EncodeFile(string inputFile, string? outputFile = null)
{
Console.WriteLine($"Encoding: {inputFile}");
if (!File.Exists(inputFile))
{
throw new FileNotFoundException($"Input file not found: {inputFile}");
}
// Read XML data
string xmlContent = File.ReadAllText(inputFile);
Console.WriteLine($"Read {xmlContent.Length} characters from {inputFile}");
// Encode XML to EXI - use simplified encoder for now
var simpleEncoder = new SimpleV2GEncoder();
byte[] exiData = simpleEncoder.EncodeToSimpleEXI(xmlContent);
// Determine output file name
outputFile ??= Path.ChangeExtension(inputFile, ".exi");
// Write EXI output
int writeResult = ByteStream.WriteBytesToFile(exiData, outputFile);
if (writeResult != 0)
{
throw new EXIException(writeResult, $"Failed to write output file: {outputFile}");
}
Console.WriteLine($"EXI written to: {outputFile}");
Console.WriteLine($"EXI size: {exiData.Length} bytes");
}
static void AnalyzeFile(string inputFile)
{
Console.WriteLine($"Analyzing: {inputFile}");
if (!File.Exists(inputFile))
{
throw new FileNotFoundException($"Input file not found: {inputFile}");
}
// Read file data
var result = ByteStream.ReadBytesFromFile(inputFile, out byte[] data, out int bytesRead);
if (result != 0)
{
throw new EXIException(result, $"Failed to read input file: {inputFile}");
}
Console.WriteLine($"File size: {bytesRead} bytes");
// Analyze packet structure
var analysis = V2GProtocol.AnalyzeDataStructure(data);
Console.WriteLine();
Console.WriteLine("=== Data Structure Analysis ===");
Console.WriteLine(analysis);
Console.WriteLine();
// Show hex dump of first 64 bytes
int dumpSize = Math.Min(64, data.Length);
Console.WriteLine($"Hex dump (first {dumpSize} bytes):");
string hexDump = ByteStream.ByteArrayToHexString(data.Take(dumpSize).ToArray());
for (int i = 0; i < hexDump.Length; i += 32)
{
int length = Math.Min(32, hexDump.Length - i);
string line = hexDump.Substring(i, length);
// Read XML file
string xmlContent = File.ReadAllText(filename, Encoding.UTF8);
// Format as pairs
var pairs = new List<string>();
for (int j = 0; j < line.Length; j += 2)
// Parse and encode XML to EXI
var exiData = V2GMessageProcessor.EncodeXmlToExi(xmlContent);
if (exiData == null || exiData.Length == 0)
{
pairs.Add(line.Substring(j, Math.Min(2, line.Length - j)));
Console.Error.WriteLine("Error encoding XML to EXI");
return -1;
}
Console.WriteLine($"{i/2:X4}: {string.Join(" ", pairs)}");
}
// If it has EXI content, try to decode header
byte[] exiBody = V2GProtocol.ExtractEXIBody(data);
if (exiBody.Length > 0)
{
Console.WriteLine();
Console.WriteLine("=== EXI Header Analysis ===");
// Check if output is redirected
bool isRedirected = Console.IsOutputRedirected;
try
if (isRedirected)
{
var decoder = new EXIDecoder();
var inputStream = new BitInputStream(exiBody);
var header = decoder.DecodeHeader(inputStream);
Console.WriteLine($"Has Cookie: {header.HasCookie}");
Console.WriteLine($"Format Version: {header.FormatVersion}");
Console.WriteLine($"Preserve Comments: {header.PreserveComments}");
Console.WriteLine($"Preserve PIs: {header.PreservePIs}");
Console.WriteLine($"Preserve DTD: {header.PreserveDTD}");
Console.WriteLine($"Preserve Prefixes: {header.PreservePrefixes}");
// Redirected output: write binary data
var stdout = Console.OpenStandardOutput();
stdout.Write(exiData, 0, exiData.Length);
stdout.Flush();
}
catch (Exception ex)
else
{
Console.WriteLine($"Header analysis failed: {ex.Message}");
}
}
}
static void RunRoundtripTest(string inputFile)
{
Console.WriteLine($"Running roundtrip test on: {inputFile}");
if (!File.Exists(inputFile))
{
throw new FileNotFoundException($"Input file not found: {inputFile}");
}
// Step 1: Read original EXI file
var result = ByteStream.ReadBytesFromFile(inputFile, out byte[] originalExi, out int originalSize);
if (result != 0)
{
throw new EXIException(result, $"Failed to read input file: {inputFile}");
}
Console.WriteLine($"Original EXI size: {originalSize} bytes");
// Step 2: Decode EXI to XML - use simplified decoder for now
byte[] exiBody = V2GProtocol.ExtractEXIBody(originalExi);
var simpleDecoder = new SimpleV2GDecoder();
string xmlContent = simpleDecoder.DecodeToSimpleXml(exiBody);
string xmlFile = Path.ChangeExtension(inputFile, ".xml");
File.WriteAllText(xmlFile, xmlContent);
Console.WriteLine($"Decoded to XML: {xmlFile} ({xmlContent.Length} characters)");
// Step 3: Encode XML back to EXI - use simplified encoder for now
var simpleEncoder = new SimpleV2GEncoder();
byte[] newExi = simpleEncoder.EncodeToSimpleEXI(xmlContent);
string newExiFile = Path.ChangeExtension(inputFile, "_new.exi");
int writeResult = ByteStream.WriteBytesToFile(newExi, newExiFile);
if (writeResult != 0)
{
throw new EXIException(writeResult, $"Failed to write output file: {newExiFile}");
}
Console.WriteLine($"Encoded to EXI: {newExiFile} ({newExi.Length} bytes)");
// Step 4: Compare original vs new EXI
bool identical = exiBody.SequenceEqual(newExi);
Console.WriteLine();
Console.WriteLine("=== Roundtrip Test Results ===");
Console.WriteLine($"Original EXI body: {exiBody.Length} bytes");
Console.WriteLine($"New EXI: {newExi.Length} bytes");
Console.WriteLine($"Files identical: {(identical ? "YES " : "NO ")}");
if (!identical)
{
Console.WriteLine();
Console.WriteLine("Differences found:");
int maxCompare = Math.Min(exiBody.Length, newExi.Length);
int differences = 0;
for (int i = 0; i < maxCompare; i++)
{
if (exiBody[i] != newExi[i])
// Terminal output: show hex string only
foreach (byte b in exiData)
{
differences++;
if (differences <= 10) // Show first 10 differences
{
Console.WriteLine($" Offset {i:X4}: {exiBody[i]:X2} -> {newExi[i]:X2}");
}
Console.Write($"{b:X2}");
}
Console.WriteLine();
}
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error encoding to EXI: {ex.Message}");
return -1;
}
}
private static int HandleDecodeOrAnalyzeMode(string filename, bool xmlMode)
{
try
{
// Read EXI file
byte[] buffer = File.ReadAllBytes(filename);
if (!xmlMode)
{
// Analysis mode - show detailed information like C version
Console.WriteLine($"File: {filename} ({buffer.Length} bytes)");
Console.Write("Raw hex data: ");
int displayLength = Math.Min(buffer.Length, 32);
for (int i = 0; i < displayLength; i++)
{
Console.Write($"{buffer[i]:X2} ");
}
if (buffer.Length > 32) Console.Write("...");
Console.WriteLine("\n");
// Analyze data structure
AnalyzeDataStructure(buffer);
}
// Extract EXI body from V2G Transfer Protocol data
byte[] exiBuffer = ExtractExiBody(buffer);
if (exiBuffer.Length != buffer.Length && !xmlMode)
{
Console.WriteLine($"\n=== V2G Transfer Protocol Analysis ===");
Console.WriteLine($"Original size: {buffer.Length} bytes");
Console.WriteLine($"EXI body size: {exiBuffer.Length} bytes");
Console.WriteLine($"Stripped V2GTP header: {buffer.Length - exiBuffer.Length} bytes");
}
// Decode EXI message
var result = V2GMessageProcessor.DecodeExiMessage(exiBuffer);
if (result.Success)
{
if (xmlMode)
{
// XML decode mode - output Wireshark-style XML
Console.WriteLine(result.XmlOutput);
}
else
{
// Analysis mode - show detailed analysis
Console.WriteLine(result.AnalysisOutput);
Console.WriteLine(result.XmlOutput); // Also show XML in analysis mode
}
return 0;
}
else
{
Console.Error.WriteLine($"Error decoding EXI: {result.ErrorMessage}");
return -1;
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error processing file: {ex.Message}");
return -1;
}
}
private static void AnalyzeDataStructure(byte[] buffer)
{
Console.WriteLine("=== Data Structure Analysis ===");
Console.WriteLine($"Total size: {buffer.Length} bytes");
if (buffer.Length >= 4)
{
uint firstFourBytes = (uint)((buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]);
Console.WriteLine($"First 4 bytes: 0x{firstFourBytes:X8}");
}
// Check for EXI start pattern
for (int i = 0; i <= buffer.Length - 2; i++)
{
ushort pattern = (ushort)((buffer[i] << 8) | buffer[i + 1]);
if (pattern == EXI_START_PATTERN)
{
Console.WriteLine($"EXI start pattern (0x{EXI_START_PATTERN:X4}) found at offset: {i}");
Console.WriteLine($"EXI payload size: {buffer.Length - i} bytes");
break;
}
}
// Determine protocol type
if (buffer.Length >= 8 && buffer[0] == V2G_PROTOCOL_VERSION && buffer[1] == V2G_INV_PROTOCOL_VERSION)
{
Console.WriteLine("Protocol: V2G Transfer Protocol detected");
}
else if (buffer.Length >= 2 && ((buffer[0] << 8) | buffer[1]) == EXI_START_PATTERN)
{
Console.WriteLine("Protocol: Direct EXI format");
}
else
{
Console.WriteLine("Protocol: Unknown or Direct EXI");
}
Console.WriteLine();
}
private static byte[] ExtractExiBody(byte[] inputData)
{
if (inputData.Length < 8)
{
// Too small for V2G TP header, assume it's pure EXI
return inputData;
}
// Check for V2GTP header: Version(1) + Inv.Version(1) + PayloadType(2) + PayloadLength(4)
if (inputData[0] == V2G_PROTOCOL_VERSION && inputData[1] == V2G_INV_PROTOCOL_VERSION)
{
// Extract payload type and length
ushort payloadType = (ushort)((inputData[2] << 8) | inputData[3]);
uint payloadLength = (uint)((inputData[4] << 24) | (inputData[5] << 16) | (inputData[6] << 8) | inputData[7]);
if (payloadType == V2G_PAYLOAD_ISO_DIN_SAP || payloadType == V2G_PAYLOAD_ISO2)
{
if (8 + payloadLength <= inputData.Length)
{
byte[] result = new byte[payloadLength];
Array.Copy(inputData, 8, result, 0, (int)payloadLength);
return result;
}
}
if (differences > 10)
{
Console.WriteLine($" ... and {differences - 10} more differences");
}
if (exiBody.Length != newExi.Length)
{
Console.WriteLine($" Size difference: {newExi.Length - exiBody.Length} bytes");
}
}
Console.WriteLine();
Console.WriteLine(identical ? "✓ Roundtrip test PASSED" : "✗ Roundtrip test FAILED");
// Not V2GTP format, return as-is
return inputData;
}
}
}