/* * 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 [output.xml]"); Console.WriteLine(" V2GDecoderNet encode-exact [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 $@" {req.DC_EVStatus.EVReady} {req.DC_EVStatus.EVErrorCode} {req.DC_EVStatus.EVRESSSOC} {req.EVTargetCurrent.Multiplier} {req.EVTargetCurrent.Unit} {req.EVTargetCurrent.Value} {req.EVTargetVoltage.Multiplier} {req.EVTargetVoltage.Unit} {req.EVTargetVoltage.Value} "; } else if (v2gMessage.Body.CurrentDemandRes_isUsed) { var res = v2gMessage.Body.CurrentDemandRes; return $@" {res.ResponseCode} {res.DC_EVSEStatus.NotificationMaxDelay} {res.DC_EVSEStatus.EVSENotification} {res.DC_EVSEStatus.EVSEStatusCode} {res.EVSEID} {res.SAScheduleTupleID} "; } else { return @" Message type not recognized"; } } 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}"); } } } }