- Change target framework from net6.0 to net48 with LangVersion 9.0 - Remove nullable reference types for .NET Framework compatibility - Replace Convert.FromHexString/ToHexString with custom implementations - Fix switch pattern matching to use traditional case statements - Replace range operators with LINQ Take/Skip and Array.Copy - Replace Math.Log2 with Math.Log(p)/Math.Log(2) formula - Fix Contains method calls to use ToLower() instead of StringComparison 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
590 lines
26 KiB
C#
590 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace V2GProtocol
|
|
{
|
|
class Program
|
|
{
|
|
static byte[] FromHexString(string hexString)
|
|
{
|
|
if (hexString.Length % 2 != 0)
|
|
throw new ArgumentException("Invalid hex string length");
|
|
|
|
byte[] bytes = new byte[hexString.Length / 2];
|
|
for (int i = 0; i < bytes.Length; i++)
|
|
{
|
|
bytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
static string ToHexString(byte[] bytes)
|
|
{
|
|
return BitConverter.ToString(bytes).Replace("-", "");
|
|
}
|
|
static void Main(string[] args)
|
|
{
|
|
// Only show header for help or analysis mode (not for encode/decode operations)
|
|
bool showHeader = false;
|
|
|
|
if (args.Length == 0)
|
|
{
|
|
showHeader = true;
|
|
}
|
|
else if (args.Length > 0)
|
|
{
|
|
string firstArg = args[0].ToLower();
|
|
|
|
// Check if it's help command
|
|
if (firstArg == "--help" || firstArg == "-h")
|
|
{
|
|
showHeader = true;
|
|
}
|
|
// Check if it's a file for analysis mode (only .dump/.txt without options)
|
|
else if (File.Exists(args[0]) && args.Length == 1)
|
|
{
|
|
string extension = Path.GetExtension(args[0]).ToLower();
|
|
|
|
// Only show header for analysis mode (.dump/.txt)
|
|
// NOT for auto-conversion (.hex, .xml)
|
|
if (extension == ".dump" || extension == ".txt")
|
|
{
|
|
showHeader = true;
|
|
}
|
|
// .hex and .xml are auto-conversion, no header
|
|
}
|
|
}
|
|
|
|
if (showHeader)
|
|
{
|
|
Console.WriteLine("=== V2G Transfer Protocol Decoder/Encoder");
|
|
Console.WriteLine("=== ISO 15118-2 / DIN SPEC 70121 / SAP Protocol");
|
|
Console.WriteLine("=== made by [tindevil82@gmail.com]");
|
|
Console.WriteLine();
|
|
}
|
|
|
|
try
|
|
{
|
|
if (args.Length == 0)
|
|
{
|
|
ShowUsage();
|
|
return;
|
|
}
|
|
|
|
var command = args[0].ToLower();
|
|
|
|
// 첫 번째 인자가 옵션인 경우 (--decode, --encode, --help)
|
|
if (command.StartsWith("--") || command.StartsWith("-"))
|
|
{
|
|
switch (command)
|
|
{
|
|
case "--decode":
|
|
case "-d":
|
|
HandleDecodeCommand(args);
|
|
break;
|
|
case "--encode":
|
|
case "-e":
|
|
HandleEncodeCommand(args);
|
|
break;
|
|
case "--help":
|
|
case "-h":
|
|
ShowUsage();
|
|
break;
|
|
default:
|
|
Console.WriteLine($"Error: Unknown option: {args[0]}");
|
|
ShowUsage();
|
|
break;
|
|
}
|
|
}
|
|
// 두 번째 인자가 옵션인 경우 (filename --decode, filename --encode)
|
|
else if (args.Length >= 2 && (args[1].ToLower() == "--decode" || args[1].ToLower() == "-d"))
|
|
{
|
|
// filename --decode 형태 - 나머지 인자들도 전달
|
|
var newArgs = new List<string> { args[1], args[0] };
|
|
for (int i = 2; i < args.Length; i++)
|
|
newArgs.Add(args[i]);
|
|
HandleDecodeCommand(newArgs.ToArray());
|
|
}
|
|
else if (args.Length >= 2 && (args[1].ToLower() == "--encode" || args[1].ToLower() == "-e"))
|
|
{
|
|
// filename --encode 형태 - 나머지 인자들도 전달
|
|
var newArgs = new List<string> { args[1], args[0] };
|
|
for (int i = 2; i < args.Length; i++)
|
|
newArgs.Add(args[i]);
|
|
HandleEncodeCommand(newArgs.ToArray());
|
|
}
|
|
// 파일명만 전달된 경우 (분석 모드)
|
|
else if (File.Exists(args[0]))
|
|
{
|
|
// Check file extension to determine default action
|
|
string extension = Path.GetExtension(args[0]).ToLower();
|
|
|
|
if (extension == ".dump" || extension == ".txt")
|
|
{
|
|
// Hex dump files - use full analysis mode
|
|
AnalyzeFile(args[0]);
|
|
}
|
|
else if (extension == ".hex")
|
|
{
|
|
// .hex files - automatically decode to XML
|
|
var autoArgs = new List<string> { "--decode", args[0] };
|
|
// Add remaining args (like -out)
|
|
for (int i = 1; i < args.Length; i++)
|
|
autoArgs.Add(args[i]);
|
|
HandleDecodeCommand(autoArgs.ToArray());
|
|
}
|
|
else if (extension == ".xml")
|
|
{
|
|
// .xml files - automatically encode to EXI
|
|
var autoArgs = new List<string> { "--encode", args[0] };
|
|
// Add remaining args (like -out)
|
|
for (int i = 1; i < args.Length; i++)
|
|
autoArgs.Add(args[i]);
|
|
HandleEncodeCommand(autoArgs.ToArray());
|
|
}
|
|
else
|
|
{
|
|
// Unknown extension - require explicit option
|
|
Console.WriteLine($"Error: Unknown file extension '{extension}'. Please specify operation:");
|
|
Console.WriteLine($" {Path.GetFileName(args[0])} --decode # To decode as EXI to XML");
|
|
Console.WriteLine($" {Path.GetFileName(args[0])} --encode # To encode as XML to EXI");
|
|
Console.WriteLine($" OR use: --decode {args[0]} / --encode {args[0]}");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Error: Unknown option or file not found: {args[0]}");
|
|
//ShowUsage();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
static void ShowUsage()
|
|
{
|
|
Console.WriteLine("Usage:");
|
|
Console.WriteLine(" V2GDecoder.exe [file.dump/.txt] # Analyze hex dump file");
|
|
Console.WriteLine(" V2GDecoder.exe [file.hex] # Auto-decode hex to XML");
|
|
Console.WriteLine(" V2GDecoder.exe [file.xml] # Auto-encode XML to EXI");
|
|
Console.WriteLine(" V2GDecoder.exe --decode <exi_hex_or_file> # Decode EXI to XML");
|
|
Console.WriteLine(" V2GDecoder.exe --encode <xml_file_or_string> # Encode XML to EXI");
|
|
Console.WriteLine(" V2GDecoder.exe <file> --decode [-out file] # Force decode operation");
|
|
Console.WriteLine(" V2GDecoder.exe <file> --encode [-out file] # Force encode operation");
|
|
Console.WriteLine(" V2GDecoder.exe --help # Show this help");
|
|
Console.WriteLine();
|
|
Console.WriteLine("File Extension Auto-Processing:");
|
|
Console.WriteLine(" .dump, .txt → Full analysis mode (network + V2G message)");
|
|
Console.WriteLine(" .hex → Auto-decode to XML");
|
|
Console.WriteLine(" .xml → Auto-encode to EXI hex");
|
|
Console.WriteLine(" Other → Requires --decode or --encode option");
|
|
Console.WriteLine();
|
|
Console.WriteLine("Pipe Usage:");
|
|
Console.WriteLine(" type file.xml | V2GDecoder.exe --encode # Pipe XML file to encode");
|
|
Console.WriteLine(" echo \"hex_string\" | V2GDecoder.exe --decode # Pipe hex string to decode");
|
|
Console.WriteLine("Example:");
|
|
Console.WriteLine("V2GDecoder.exe 01FE80010000001E809802104142423030303831908C0C0C0E0C51E020256968C0C0C0C0C080 --decode");
|
|
Console.WriteLine();
|
|
}
|
|
|
|
static string GetOutputFilename(string[] args)
|
|
{
|
|
for (int i = 0; i < args.Length - 1; i++)
|
|
{
|
|
if (args[i].ToLower() == "-out" || args[i].ToLower() == "-o")
|
|
{
|
|
return args[i + 1];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void HandleDecodeCommand(string[] args)
|
|
{
|
|
string input;
|
|
byte[] exiBytes;
|
|
string outputFile = GetOutputFilename(args);
|
|
|
|
// Check if we have sufficient arguments for file/string input
|
|
if (args.Length >= 2)
|
|
{
|
|
// We have command line arguments, use them
|
|
input = args[1];
|
|
}
|
|
else if (Console.IsInputRedirected)
|
|
{
|
|
// No sufficient arguments, check if input is coming from stdin (pipe)
|
|
input = Console.In.ReadToEnd().Trim();
|
|
if (string.IsNullOrEmpty(input))
|
|
{
|
|
Console.WriteLine("Error: No input data received from stdin");
|
|
return;
|
|
}
|
|
outputFile = null; // Force console output for stdin
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Error: EXI hex string or file required for decode operation");
|
|
Console.WriteLine("Usage: V2GDecoder.exe --decode <exi_hex_string_or_file> [-out output_file]");
|
|
return;
|
|
}
|
|
|
|
// Check if input is a file
|
|
if (File.Exists(input))
|
|
{
|
|
// Handle different file extensions
|
|
string extension = Path.GetExtension(input).ToLower();
|
|
|
|
if (extension == ".dump")
|
|
{
|
|
// Hex dump file format with arrows (→)
|
|
byte[] rawData = V2GDecoder.ParseHexFile(input);
|
|
|
|
// Find V2G message in the hex dump
|
|
var v2gMessage = ExtractV2GMessageFromHexDump(rawData);
|
|
if (v2gMessage != null)
|
|
{
|
|
exiBytes = v2gMessage;
|
|
}
|
|
else
|
|
{
|
|
// Try to decode the raw data directly (might be pure EXI without V2G header)
|
|
exiBytes = rawData;
|
|
}
|
|
}
|
|
else if (extension == ".hex")
|
|
{
|
|
// Binary hex file
|
|
exiBytes = File.ReadAllBytes(input);
|
|
}
|
|
else if (extension == ".txt")
|
|
{
|
|
// Legacy support for .txt files - check content format
|
|
string fileContent = File.ReadAllText(input).Trim();
|
|
if (fileContent.Contains("→"))
|
|
{
|
|
// Hex dump format
|
|
byte[] rawData = V2GDecoder.ParseHexFile(input);
|
|
var v2gMessage = ExtractV2GMessageFromHexDump(rawData);
|
|
if (v2gMessage != null)
|
|
{
|
|
exiBytes = v2gMessage;
|
|
}
|
|
else
|
|
{
|
|
exiBytes = rawData;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Plain hex
|
|
fileContent = fileContent.Replace(" ", "").Replace("-", "").Replace("0x", "").Replace("\n", "").Replace("\r", "");
|
|
exiBytes = FromHexString(fileContent);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Unknown extension, try as plain hex
|
|
string fileContent = File.ReadAllText(input).Trim();
|
|
fileContent = fileContent.Replace(" ", "").Replace("-", "").Replace("0x", "").Replace("\n", "").Replace("\r", "");
|
|
exiBytes = FromHexString(fileContent);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Direct hex string input
|
|
string exiHexString = input.Replace(" ", "").Replace("-", "").Replace("0x", "");
|
|
|
|
if (exiHexString.Length % 2 != 0)
|
|
{
|
|
Console.WriteLine("Error: Invalid hex string (odd number of characters)");
|
|
return;
|
|
}
|
|
|
|
exiBytes = FromHexString(exiHexString);
|
|
}
|
|
|
|
try
|
|
{
|
|
// Decode EXI to XML - output only the XML
|
|
var decodedXml = V2GDecoder.DecodeEXIToXML(exiBytes);
|
|
|
|
if (outputFile != null)
|
|
{
|
|
// Save to file
|
|
File.WriteAllText(outputFile, decodedXml);
|
|
Console.WriteLine($"Decoded XML saved to: {outputFile}");
|
|
}
|
|
else
|
|
{
|
|
// Output to console
|
|
Console.WriteLine(decodedXml);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error decoding EXI: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
static byte[] ExtractV2GMessageFromHexDump(byte[] data)
|
|
{
|
|
// Find V2G Transfer Protocol signature (0x01FE)
|
|
for (int i = 0; i < data.Length - 8; i++)
|
|
{
|
|
if (data[i] == 0x01 && data[i + 1] == 0xFE)
|
|
{
|
|
// Get payload length
|
|
uint payloadLength = (uint)((data[i + 4] << 24) | (data[i + 5] << 16) | (data[i + 6] << 8) | data[i + 7]);
|
|
|
|
// Extract EXI payload
|
|
if (i + 8 + payloadLength <= data.Length)
|
|
{
|
|
byte[] exiPayload = new byte[payloadLength];
|
|
Array.Copy(data, i + 8, exiPayload, 0, (int)payloadLength);
|
|
return exiPayload;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void HandleEncodeCommand(string[] args)
|
|
{
|
|
string xmlInput;
|
|
string xmlContent;
|
|
string outputFile = GetOutputFilename(args);
|
|
|
|
// Check if we have sufficient arguments for file/string input
|
|
if (args.Length >= 2)
|
|
{
|
|
// We have command line arguments, use them
|
|
xmlInput = args[1];
|
|
|
|
// Check if input is a file
|
|
if (File.Exists(xmlInput))
|
|
{
|
|
xmlContent = File.ReadAllText(xmlInput);
|
|
}
|
|
else
|
|
{
|
|
xmlContent = xmlInput;
|
|
}
|
|
}
|
|
else if (Console.IsInputRedirected)
|
|
{
|
|
// No sufficient arguments, check if input is coming from stdin (pipe)
|
|
xmlContent = Console.In.ReadToEnd().Trim();
|
|
if (string.IsNullOrEmpty(xmlContent))
|
|
{
|
|
Console.WriteLine("Error: No input data received from stdin");
|
|
return;
|
|
}
|
|
outputFile = null; // Force console output for stdin
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Error: XML string or file required for encode operation");
|
|
Console.WriteLine("Usage: V2GDecoder.exe --encode <xml_file_or_string> [-out output_file]");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Encode XML to EXI
|
|
var exiBytes = V2GDecoder.EncodeXMLToEXI(xmlContent);
|
|
|
|
if (outputFile != null)
|
|
{
|
|
// Save as binary file
|
|
File.WriteAllBytes(outputFile, exiBytes);
|
|
Console.WriteLine($"Encoded binary saved to: {outputFile} ({exiBytes.Length} bytes)");
|
|
|
|
// Also show hex string on console for reference
|
|
var exiHexString = ToHexString(exiBytes);
|
|
Console.WriteLine($"Hex: {exiHexString}");
|
|
}
|
|
else
|
|
{
|
|
// Output hex string to console
|
|
var exiHexString = ToHexString(exiBytes);
|
|
Console.WriteLine(exiHexString);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error encoding XML: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
static void AnalyzeFile(string dataFilePath)
|
|
{
|
|
if (!File.Exists(dataFilePath))
|
|
{
|
|
Console.WriteLine($"Error: Data file not found: {dataFilePath}");
|
|
return;
|
|
}
|
|
|
|
Console.WriteLine($"Analyzing file: {dataFilePath}");
|
|
|
|
// 헥스 파일에서 바이너리 데이터 파싱
|
|
byte[] rawData = V2GDecoder.ParseHexFile(dataFilePath);
|
|
|
|
Console.WriteLine($"Parsed {rawData.Length} bytes from hex file");
|
|
Console.WriteLine();
|
|
|
|
// 전체 데이터 헥스 덤프
|
|
Console.WriteLine("=== Raw Data Hex Dump ===");
|
|
Console.WriteLine(V2GDecoder.BytesToHex(rawData));
|
|
Console.WriteLine();
|
|
|
|
// V2G 메시지 디코딩 시도
|
|
Console.WriteLine("=== V2G Message Analysis ===");
|
|
|
|
// 전체 데이터에서 V2G 메시지 찾기
|
|
AnalyzeV2GMessages(rawData);
|
|
|
|
// 네트워크 패킷 분석 (이더넷/IPv6/TCP 헤더 포함인 경우)
|
|
AnalyzeNetworkPacket(rawData);
|
|
}
|
|
|
|
static void AnalyzeV2GMessages(byte[] data)
|
|
{
|
|
// V2G Transfer Protocol 시그니처 찾기
|
|
for (int i = 0; i < data.Length - 8; i++)
|
|
{
|
|
if (data[i] == 0x01 && data[i + 1] == 0xFE) // V2G TP Header
|
|
{
|
|
Console.WriteLine($"Potential V2G message found at offset 0x{i:X4}");
|
|
|
|
try
|
|
{
|
|
byte[] messageData = new byte[data.Length - i];
|
|
Array.Copy(data, i, messageData, 0, messageData.Length);
|
|
|
|
var message = V2GDecoder.DecodeMessage(messageData);
|
|
|
|
Console.WriteLine($" Version: 0x{message.Version:X2}");
|
|
Console.WriteLine($" Inverse Version: 0x{message.InverseVersion:X2}");
|
|
Console.WriteLine($" Payload Type: 0x{(ushort)message.PayloadType:X4} ({message.PayloadType})");
|
|
Console.WriteLine($" Payload Length: {message.PayloadLength} bytes");
|
|
Console.WriteLine($" Valid: {message.IsValid}");
|
|
Console.WriteLine();
|
|
|
|
if (message.IsValid && message.Payload != null)
|
|
{
|
|
Console.WriteLine("=== Decoded Message Content ===");
|
|
Console.WriteLine(message.DecodedContent);
|
|
Console.WriteLine();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($" Error decoding message: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void AnalyzeNetworkPacket(byte[] data)
|
|
{
|
|
Console.WriteLine("=== Network Packet Analysis ===");
|
|
|
|
if (data.Length < 54) // Minimum Ethernet + IPv6 + TCP header size
|
|
{
|
|
Console.WriteLine("Data too short for network packet analysis");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// 이더넷 헤더 분석 (14 bytes)
|
|
Console.WriteLine("Ethernet Header:");
|
|
Console.WriteLine($" Destination MAC: {string.Join(":", data.Take(6).Select(b => b.ToString("X2")))}");
|
|
Console.WriteLine($" Source MAC: {string.Join(":", data.Skip(6).Take(6).Select(b => b.ToString("X2")))}");
|
|
Console.WriteLine($" EtherType: 0x{(data[12] << 8 | data[13]):X4}");
|
|
|
|
ushort etherType = (ushort)(data[12] << 8 | data[13]);
|
|
|
|
if (etherType == 0x86DD) // IPv6
|
|
{
|
|
Console.WriteLine("\nIPv6 Header:");
|
|
int ipv6Offset = 14;
|
|
byte versionTrafficClass = data[ipv6Offset];
|
|
Console.WriteLine($" Version: {(versionTrafficClass >> 4) & 0xF}");
|
|
Console.WriteLine($" Traffic Class: 0x{((versionTrafficClass & 0xF) << 4 | (data[ipv6Offset + 1] >> 4)):X2}");
|
|
|
|
ushort payloadLength = (ushort)(data[ipv6Offset + 4] << 8 | data[ipv6Offset + 5]);
|
|
byte nextHeader = data[ipv6Offset + 6];
|
|
byte hopLimit = data[ipv6Offset + 7];
|
|
|
|
Console.WriteLine($" Payload Length: {payloadLength}");
|
|
Console.WriteLine($" Next Header: 0x{nextHeader:X2} ({(nextHeader == 6 ? "TCP" : "Other")})");
|
|
Console.WriteLine($" Hop Limit: {hopLimit}");
|
|
|
|
// IPv6 주소는 16바이트씩
|
|
byte[] srcAddr = new byte[16];
|
|
byte[] dstAddr = new byte[16];
|
|
Array.Copy(data, ipv6Offset + 8, srcAddr, 0, 16);
|
|
Array.Copy(data, ipv6Offset + 24, dstAddr, 0, 16);
|
|
|
|
Console.WriteLine($" Source Address: {string.Join(":", Enumerable.Range(0, 8).Select(i => $"{srcAddr[i*2]:X2}{srcAddr[i*2+1]:X2}"))}");
|
|
Console.WriteLine($" Destination Address: {string.Join(":", Enumerable.Range(0, 8).Select(i => $"{dstAddr[i*2]:X2}{dstAddr[i*2+1]:X2}"))}");
|
|
|
|
if (nextHeader == 6) // TCP
|
|
{
|
|
int tcpOffset = ipv6Offset + 40;
|
|
if (data.Length > tcpOffset + 20)
|
|
{
|
|
Console.WriteLine("\nTCP Header:");
|
|
ushort srcPort = (ushort)(data[tcpOffset] << 8 | data[tcpOffset + 1]);
|
|
ushort dstPort = (ushort)(data[tcpOffset + 2] << 8 | data[tcpOffset + 3]);
|
|
|
|
Console.WriteLine($" Source Port: {srcPort}");
|
|
Console.WriteLine($" Destination Port: {dstPort}");
|
|
|
|
// V2G는 일반적으로 포트 15118을 사용
|
|
if (srcPort == 15118 || dstPort == 15118)
|
|
{
|
|
Console.WriteLine(" -> V2G Communication detected (port 15118)!");
|
|
}
|
|
|
|
// TCP 데이터 시작 위치 계산
|
|
byte dataOffset = (byte)((data[tcpOffset + 12] >> 4) * 4);
|
|
int tcpDataOffset = tcpOffset + dataOffset;
|
|
|
|
if (tcpDataOffset < data.Length)
|
|
{
|
|
Console.WriteLine($"\nTCP Payload (starting at offset 0x{tcpDataOffset:X4}):");
|
|
byte[] tcpPayload = new byte[data.Length - tcpDataOffset];
|
|
Array.Copy(data, tcpDataOffset, tcpPayload, 0, tcpPayload.Length);
|
|
|
|
// TCP 페이로드에서 V2G 메시지 찾기
|
|
if (tcpPayload.Length > 8 && tcpPayload[0] == 0x01 && tcpPayload[1] == 0xFE)
|
|
{
|
|
Console.WriteLine("V2G Message found in TCP payload!");
|
|
var v2gMessage = V2GDecoder.DecodeMessage(tcpPayload);
|
|
Console.WriteLine(v2gMessage.DecodedContent);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("TCP Payload hex dump:");
|
|
Console.WriteLine(V2GDecoder.BytesToHex(tcpPayload));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error analyzing network packet: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
} |