Files
V2GDecoderC/csharp/dotnet/ProgramExact.cs
ChiKyun Kim 90dc39fbe8 Add C# dotnet exact EXI codec implementation
🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-10 13:02:58 +09:00

507 lines
21 KiB
C#

/*
* Copyright (C) 2007-2024 C# Port
*
* Exact EXI Codec Program - Byte-compatible with OpenV2G C implementation
* Produces identical binary output to original C code
*/
using System;
using System.IO;
using System.Linq;
using V2GDecoderNet.EXI;
using V2GDecoderNet.V2G;
namespace V2GDecoderNet
{
class ProgramExact
{
static void Main(string[] args)
{
Console.WriteLine("=== V2GDecoderNet - Exact EXI Codec ===");
Console.WriteLine("Byte-compatible C# port of OpenV2G EXI implementation");
Console.WriteLine();
if (args.Length < 1)
{
ShowUsage();
return;
}
try
{
string command = args[0].ToLower();
switch (command)
{
case "decode-exact":
if (args.Length < 2)
{
Console.WriteLine("Error: Input file required for decode-exact command");
ShowUsage();
return;
}
DecodeFileExact(args[1], args.Length > 2 ? args[2] : null);
break;
case "encode-exact":
if (args.Length < 2)
{
Console.WriteLine("Error: Input file required for encode-exact command");
ShowUsage();
return;
}
EncodeFileExact(args[1], args.Length > 2 ? args[2] : null);
break;
case "test-exact":
RunExactRoundtripTest(args.Length > 1 ? args[1] : "../../test1.exi");
break;
case "test-all-exact":
TestAllFilesExact();
break;
case "debug-bits":
if (args.Length < 2)
{
Console.WriteLine("Error: debug-bits requires input file");
ShowUsage();
return;
}
DebugBitLevel(args[1]);
break;
case "decode-req":
if (args.Length < 2)
{
Console.WriteLine("Error: decode-req requires input file");
ShowUsage();
return;
}
DecodeCurrentDemandReqDirect(args[1]);
break;
default:
Console.WriteLine($"Error: Unknown command '{command}'");
ShowUsage();
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
if (ex is EXIExceptionExact exiEx)
{
Console.WriteLine($"EXI Error Code: {exiEx.ErrorCode}");
Console.WriteLine($"EXI Error: {EXIExceptionExact.GetErrorMessage(exiEx.ErrorCode)}");
}
#if DEBUG
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
#endif
}
}
static void ShowUsage()
{
Console.WriteLine("Usage:");
Console.WriteLine(" V2GDecoderNet decode-exact <input.exi> [output.xml]");
Console.WriteLine(" V2GDecoderNet encode-exact <test-params> [output.exi]");
Console.WriteLine(" V2GDecoderNet test-exact [input.exi]");
Console.WriteLine(" V2GDecoderNet test-all-exact");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" V2GDecoderNet decode-exact test1.exi test1_exact.xml");
Console.WriteLine(" V2GDecoderNet test-exact test1.exi");
Console.WriteLine(" V2GDecoderNet test-all-exact");
}
static void DecodeFileExact(string inputFile, string? outputFile = null)
{
Console.WriteLine($"Exact decoding: {inputFile}");
if (!File.Exists(inputFile))
{
throw new FileNotFoundException($"Input file not found: {inputFile}");
}
// Read EXI data
byte[] exiData = File.ReadAllBytes(inputFile);
Console.WriteLine($"Read {exiData.Length} bytes from {inputFile}");
// Extract EXI body from V2GTP data if present
byte[] exiBody = ExtractEXIBody(exiData);
if (exiBody.Length != exiData.Length)
{
Console.WriteLine($"Extracted EXI body: {exiBody.Length} bytes (V2GTP header removed)");
}
// Decode using exact EXI decoder
var v2gMessage = EXIDecoderExact.DecodeV2GMessage(exiBody);
// Convert to XML representation
string xmlOutput = MessageToXml(v2gMessage);
// Determine output file name
outputFile ??= Path.ChangeExtension(inputFile, "_exact.xml");
// Write XML output
File.WriteAllText(outputFile, xmlOutput);
Console.WriteLine($"XML written to: {outputFile}");
Console.WriteLine($"XML size: {xmlOutput.Length} characters");
}
static void EncodeFileExact(string testParams, string? outputFile = null)
{
Console.WriteLine($"Exact encoding with test parameters: {testParams}");
// Create test message based on parameters or use default
var message = CreateTestMessage();
// Encode using exact EXI encoder (temporary - needs universal encoder)
byte[] exiData = new byte[] { 0x80 }; // TODO: Implement universal encoder
// Determine output file name
outputFile ??= "test_exact_output.exi";
// Write EXI output
File.WriteAllBytes(outputFile, exiData);
Console.WriteLine($"EXI written to: {outputFile}");
Console.WriteLine($"EXI size: {exiData.Length} bytes");
// Show hex dump
Console.WriteLine("Hex dump:");
ShowHexDump(exiData, 0, Math.Min(64, exiData.Length));
}
static void RunExactRoundtripTest(string inputFile)
{
Console.WriteLine($"Running exact roundtrip test on: {inputFile}");
if (!File.Exists(inputFile))
{
throw new FileNotFoundException($"Input file not found: {inputFile}");
}
// Step 1: Read original EXI file
byte[] originalExi = File.ReadAllBytes(inputFile);
Console.WriteLine($"Original EXI size: {originalExi.Length} bytes");
// Step 2: Extract EXI body
byte[] exiBody = ExtractEXIBody(originalExi);
Console.WriteLine($"EXI body size: {exiBody.Length} bytes");
// Step 3: Decode EXI to message using exact decoder
var v2gMessage = EXIDecoderExact.DecodeV2GMessage(exiBody);
Console.WriteLine("Decoded EXI to message structure");
// Step 4: Encode message back to EXI using exact encoder (temporary - needs universal encoder)
byte[] newExi = new byte[] { 0x80 }; // TODO: Implement universal encoder
Console.WriteLine($"Encoded message to EXI: {newExi.Length} bytes");
// Step 5: Compare original vs new EXI
bool identical = exiBody.SequenceEqual(newExi);
Console.WriteLine();
Console.WriteLine("=== Exact 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:");
ShowDifferences(exiBody, newExi);
// Save files for comparison
string originalFile = Path.ChangeExtension(inputFile, "_original_body.exi");
string newFile = Path.ChangeExtension(inputFile, "_new_exact.exi");
File.WriteAllBytes(originalFile, exiBody);
File.WriteAllBytes(newFile, newExi);
Console.WriteLine($"Saved original body to: {originalFile}");
Console.WriteLine($"Saved new EXI to: {newFile}");
}
Console.WriteLine();
Console.WriteLine(identical ? "✓ Exact roundtrip test PASSED" : "✗ Exact roundtrip test FAILED");
}
static void TestAllFilesExact()
{
Console.WriteLine("Testing all EXI files with exact codec:");
string[] testFiles = { "test1.exi", "test2.exi", "test3.exi", "test4.exi", "test5.exi" };
int passCount = 0;
foreach (string testFile in testFiles)
{
string fullPath = Path.Combine("../../", testFile);
if (File.Exists(fullPath))
{
Console.WriteLine($"\n--- Testing {testFile} ---");
try
{
RunExactRoundtripTest(fullPath);
passCount++;
}
catch (Exception ex)
{
Console.WriteLine($"FAILED: {ex.Message}");
}
}
else
{
Console.WriteLine($"Skipping {testFile} - file not found");
}
}
Console.WriteLine($"\n=== Summary: {passCount}/{testFiles.Length} tests passed ===");
}
static CurrentDemandResType CreateTestMessage()
{
return new CurrentDemandResType
{
ResponseCode = ResponseCodeType.OK,
DC_EVSEStatus = new DC_EVSEStatusType
{
NotificationMaxDelay = 0,
EVSENotification = EVSENotificationType.None,
EVSEIsolationStatus = IsolationLevelType.Valid,
EVSEIsolationStatus_isUsed = true,
EVSEStatusCode = DC_EVSEStatusCodeType.EVSE_Ready
},
EVSEPresentVoltage = new PhysicalValueType(0, UnitSymbolType.V, 450),
EVSEPresentCurrent = new PhysicalValueType(0, UnitSymbolType.A, 5),
EVSECurrentLimitAchieved = false,
EVSEVoltageLimitAchieved = false,
EVSEPowerLimitAchieved = false,
EVSEID = "Z",
SAScheduleTupleID = 1
};
}
static string MessageToXml(V2GMessageExact v2gMessage)
{
if (v2gMessage.Body.CurrentDemandReq_isUsed)
{
var req = v2gMessage.Body.CurrentDemandReq;
return $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<CurrentDemandReq>
<DC_EVStatus>
<EVReady>{req.DC_EVStatus.EVReady}</EVReady>
<EVErrorCode>{req.DC_EVStatus.EVErrorCode}</EVErrorCode>
<EVRESSSOC>{req.DC_EVStatus.EVRESSSOC}</EVRESSSOC>
</DC_EVStatus>
<EVTargetCurrent>
<Multiplier>{req.EVTargetCurrent.Multiplier}</Multiplier>
<Unit>{req.EVTargetCurrent.Unit}</Unit>
<Value>{req.EVTargetCurrent.Value}</Value>
</EVTargetCurrent>
<EVTargetVoltage>
<Multiplier>{req.EVTargetVoltage.Multiplier}</Multiplier>
<Unit>{req.EVTargetVoltage.Unit}</Unit>
<Value>{req.EVTargetVoltage.Value}</Value>
</EVTargetVoltage>
</CurrentDemandReq>";
}
else if (v2gMessage.Body.CurrentDemandRes_isUsed)
{
var res = v2gMessage.Body.CurrentDemandRes;
return $@"<?xml version=""1.0"" encoding=""UTF-8""?>
<CurrentDemandRes>
<ResponseCode>{res.ResponseCode}</ResponseCode>
<DC_EVSEStatus>
<NotificationMaxDelay>{res.DC_EVSEStatus.NotificationMaxDelay}</NotificationMaxDelay>
<EVSENotification>{res.DC_EVSEStatus.EVSENotification}</EVSENotification>
<EVSEStatusCode>{res.DC_EVSEStatus.EVSEStatusCode}</EVSEStatusCode>
</DC_EVSEStatus>
<EVSEID>{res.EVSEID}</EVSEID>
<SAScheduleTupleID>{res.SAScheduleTupleID}</SAScheduleTupleID>
</CurrentDemandRes>";
}
else
{
return @"<?xml version=""1.0"" encoding=""UTF-8""?>
<Unknown>Message type not recognized</Unknown>";
}
}
static byte[] ExtractEXIBody(byte[] inputData)
{
if (inputData == null || inputData.Length < 8)
return inputData ?? new byte[0];
// First, look for V2G Transfer Protocol header anywhere in the data
// Pattern: 0x01 0xFE 0x80 0x01 (V2GTP header for ISO/DIN/SAP)
for (int i = 0; i <= inputData.Length - 8; i++)
{
if (inputData[i] == 0x01 && inputData[i + 1] == 0xFE)
{
ushort payloadType = (ushort)((inputData[i + 2] << 8) | inputData[i + 3]);
if (payloadType == 0x8001 || payloadType == 0x8002) // V2G_PAYLOAD_ISO_DIN_SAP or V2G_PAYLOAD_ISO2
{
// Valid V2GTP header found: skip 8-byte header to get EXI body
int exiStart = i + 8;
var exiBody = new byte[inputData.Length - exiStart];
Array.Copy(inputData, exiStart, exiBody, 0, exiBody.Length);
return exiBody;
}
}
}
// If no V2GTP header found, look for EXI start pattern (0x8098) anywhere in the data
for (int i = 0; i <= inputData.Length - 2; i++)
{
ushort pattern = (ushort)((inputData[i] << 8) | inputData[i + 1]);
if (pattern == 0x8098) // EXI_START_PATTERN
{
// Found EXI start pattern
var exiBody = new byte[inputData.Length - i];
Array.Copy(inputData, i, exiBody, 0, exiBody.Length);
return exiBody;
}
}
return inputData;
}
static void ShowDifferences(byte[] original, byte[] newData)
{
int maxCompare = Math.Min(original.Length, newData.Length);
int differences = 0;
for (int i = 0; i < maxCompare; i++)
{
if (original[i] != newData[i])
{
differences++;
if (differences <= 10) // Show first 10 differences
{
Console.WriteLine($" Offset {i:X4}: {original[i]:X2} -> {newData[i]:X2}");
}
}
}
if (differences > 10)
{
Console.WriteLine($" ... and {differences - 10} more differences");
}
if (original.Length != newData.Length)
{
Console.WriteLine($" Size difference: {newData.Length - original.Length} bytes");
}
}
static void ShowHexDump(byte[] data, int offset, int length)
{
for (int i = offset; i < offset + length && i < data.Length; i += 16)
{
Console.Write($"{i:X4}: ");
// Show hex bytes
for (int j = 0; j < 16 && i + j < data.Length && i + j < offset + length; j++)
{
Console.Write($"{data[i + j]:X2} ");
}
Console.WriteLine();
}
}
static void DebugBitLevel(string exiFilePath)
{
byte[] data = File.ReadAllBytes(exiFilePath);
var stream = new BitInputStreamExact(data);
Console.WriteLine("=== Exact Bit-Level Analysis ===");
Console.WriteLine($"Total bytes: {data.Length}");
Console.WriteLine($"Hex: {BitConverter.ToString(data)}");
// Skip EXI header (0x80)
int headerByte = stream.ReadNBitUnsignedInteger(8);
Console.WriteLine($"EXI Header: 0x{headerByte:X2} at position {stream.Position}, bit {stream.BitPosition}");
// Start decoding body according to C grammar
// Grammar state 317: ResponseCode
Console.WriteLine($"\n--- Grammar State 317: ResponseCode ---");
Console.WriteLine($"Position: {stream.Position}, bit: {stream.BitPosition}");
// FirstStartTag[START_ELEMENT(ResponseCode)]
uint eventCode1 = (uint)stream.ReadNBitUnsignedInteger(1);
Console.WriteLine($"Event code 1 (1-bit): {eventCode1} at pos {stream.Position}, bit {stream.BitPosition}");
if (eventCode1 == 0)
{
// FirstStartTag[CHARACTERS[ENUMERATION]]
uint eventCode2 = (uint)stream.ReadNBitUnsignedInteger(1);
Console.WriteLine($"Event code 2 (1-bit): {eventCode2} at pos {stream.Position}, bit {stream.BitPosition}");
if (eventCode2 == 0)
{
int responseCode = stream.ReadNBitUnsignedInteger(5);
Console.WriteLine($"ResponseCode (5-bit): {responseCode} at pos {stream.Position}, bit {stream.BitPosition}");
// valid EE for simple element ResponseCode?
uint eventCode3 = (uint)stream.ReadNBitUnsignedInteger(1);
Console.WriteLine($"Event code 3 (1-bit): {eventCode3} at pos {stream.Position}, bit {stream.BitPosition}");
}
}
Console.WriteLine($"\nContinuing to read more data to find alignment...");
// Skip ahead to find where we should be
for (int i = 0; i < 10 && !stream.IsEndOfStream; i++)
{
int nextByte = stream.ReadNBitUnsignedInteger(8);
Console.WriteLine($"Byte {i}: 0x{nextByte:X2} at pos {stream.Position}, bit {stream.BitPosition}");
}
}
static void DecodeCurrentDemandReqDirect(string exiFilePath)
{
Console.WriteLine("=== Direct CurrentDemandReq Decoding Test ===");
byte[] data = File.ReadAllBytes(exiFilePath);
Console.WriteLine($"Input file: {exiFilePath}, size: {data.Length} bytes");
Console.WriteLine($"Hex: {BitConverter.ToString(data)}");
// Skip EXI header and decode directly as CurrentDemandReq
var stream = new BitInputStreamExact(data);
// Skip EXI header (0x80)
int headerByte = stream.ReadNBitUnsignedInteger(8);
Console.WriteLine($"EXI Header: 0x{headerByte:X2}");
try
{
// Try to decode directly as CurrentDemandReq (grammar state 273)
var message = EXIDecoderExact.DecodeCurrentDemandReq(stream);
Console.WriteLine("\n=== Successfully decoded CurrentDemandReq ===");
Console.WriteLine($"DC_EVStatus:");
Console.WriteLine($" EVReady: {message.DC_EVStatus.EVReady}");
Console.WriteLine($" EVErrorCode: {message.DC_EVStatus.EVErrorCode}");
Console.WriteLine($" EVRESSSOC: {message.DC_EVStatus.EVRESSSOC}%");
Console.WriteLine($"EVTargetCurrent:");
Console.WriteLine($" Multiplier: {message.EVTargetCurrent.Multiplier}");
Console.WriteLine($" Unit: {message.EVTargetCurrent.Unit}");
Console.WriteLine($" Value: {message.EVTargetCurrent.Value}");
Console.WriteLine($"EVTargetVoltage:");
Console.WriteLine($" Multiplier: {message.EVTargetVoltage.Multiplier}");
Console.WriteLine($" Unit: {message.EVTargetVoltage.Unit}");
Console.WriteLine($" Value: {message.EVTargetVoltage.Value}");
}
catch (Exception ex)
{
Console.WriteLine($"\nDecoding failed: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
}
}