초기 커밋.

This commit is contained in:
2026-02-03 11:33:58 +09:00
parent 90b3415a1f
commit b6b823b1c4
71 changed files with 4794 additions and 0 deletions

89
.gitignore vendored Normal file
View File

@@ -0,0 +1,89 @@
# Built Visual Studio files
*.exe
*.dll
*.pdb
*.cache
*.mdb
*.obj
*.log
*.user
*.suo
*.userprefs
*.vsp
*.vspx
*.psess
*.pidb
*.tmp
*.svclog
*.sqlite
*.db
*.db-shm
*.db-wal
*.opendb
# Build directories
[Bb]in/
[Oo]bj/
[Ll]og/
[Tt]emp/
[Pp]ublish/
[Rr]elease/
[Dd]ebug/
# Rider / ReSharper
*.DotSettings.user
.idea/
.Rider/
_ReSharper*/
*.[Rr]esult
# Visual Studio Code
.vscode/
# Test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NuGet
*.nupkg
packages/
*.snupkg
**/[Pp]ackages/*
!**/[Pp]ackages/build/
!**/[Pp]ackages/repositories.config
*.nuspec
# Azure
*.azurePubxml
*.portbridge.json
PublishScripts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# Configuration files
appsettings.*.json
secrets.json
*.KeyStore
*.pfx
# Entity Framework
Migrations/obj/
*/migrations/obj/
# Coverage & analysis
*.coverage
*.coveragexml
TestResults/
.coverlet/
# Others
*.bak
*.tlog
*.ipch
*.iuser
*.tlog
*.ipr
.vs/
.DS_Store
Thumbs.db

15
Configs/Entrance.json Normal file
View File

@@ -0,0 +1,15 @@
{
"LaneName": "Entrance",
"LaneId": "#P-001",
"LaneDescription": "Main Entrance",
"CameraIp": "192.168.100.99",
"CameraPort": 80,
"GateIp": "127.0.0.1",
"GatePort": 5000,
"LedIp": "127.0.0.1",
"LedPort": 5001,
"LedTopMessage": "어서오세요",
"LedBottomMessage": "주차관리시스템",
"CmsIp": "127.0.0.1",
"CmsPort": 6000
}

View File

@@ -0,0 +1,6 @@
namespace Inno.Common.Stubs;
public class Class1
{
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,65 @@
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Inno.Communication.TCP
{
public class TcpClientConnector : IDisposable
{
private TcpClient _client;
private NetworkStream _stream;
public bool IsConnected => _client != null && _client.Connected;
public async Task ConnectAsync(string host, int port, int timeout, CancellationToken token)
{
_client = new TcpClient();
var connectTask = _client.ConnectAsync(host, port);
if (await Task.WhenAny(connectTask, Task.Delay(timeout, token)) == connectTask)
{
await connectTask;
try
{
_stream = _client.GetStream();
}
catch
{
_client.Dispose();
throw;
}
}
else
{
_client.Dispose();
throw new TimeoutException();
}
}
public async Task WriteAsync(byte[] data, CancellationToken token)
{
if (_stream == null) throw new InvalidOperationException("Not connected");
await _stream.WriteAsync(data, 0, data.Length, token);
}
public async Task WriteAsync(string message, Encoding encoding, CancellationToken token)
{
if (_stream == null) throw new InvalidOperationException("Not connected");
byte[] data = encoding.GetBytes(message);
await WriteAsync(data, token);
}
public async Task<int> ReadAsync(byte[] buffer, CancellationToken token)
{
if (_stream == null) throw new InvalidOperationException("Not connected");
return await _stream.ReadAsync(buffer, 0, buffer.Length, token);
}
public void Dispose()
{
_stream?.Dispose();
_client?.Dispose();
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Inno.Communication.TCP
{
public class TcpConnectionItem
{
public string Host { get; set; }
public int Port { get; set; }
public TcpConnectionItem(string host, int port) { Host = host; Port = port; }
}
}

View File

@@ -0,0 +1,6 @@
namespace Inno.Rtsp.RawFramesDecoding.DecodedFrames.Video
{
public interface IDecodedVideoFrame
{
}
}

View File

@@ -0,0 +1,23 @@
using System;
using RtspModule;
using Inno.Rtsp.RawFramesDecoding.DecodedFrames.Video;
namespace Inno.Rtsp
{
public class RtspPlayer : IDisposable
{
public event EventHandler<IDecodedVideoFrame> RtspFrameReceived;
public event EventHandler<string> ConnectionChanged;
public void Init(RtspConnectionParameter param) { }
public void StartRtspStreaming() { }
public void StopRtspStreaming() { }
public void ChangeAddress(RtspConnectionParameter param) { }
public void Dispose() { }
}
}
namespace Inno.Rtsp.RawFramesReceiving
{
public class RawFramesSource { }
}

View File

@@ -0,0 +1,7 @@
namespace Inno.Wpf
{
public static class EncodingCodeDefines
{
public const int CODE_EUC_KR = 51949;
}
}

View File

@@ -0,0 +1,8 @@
namespace RtspModule
{
public enum Company { Novitec }
public class RtspConnectionParameter
{
public RtspConnectionParameter(Company company, string ip, string port, string id, string pw) { }
}
}

View File

@@ -0,0 +1,41 @@
namespace DeepLearning_Model_Sharp
{
public enum ENUM_MODEL_TYPE
{
// CPU
_TYPE_C_SSD = 0,
_TYPE_C_YLv4,
_TYPE_C_YLv5,
_TYPE_C_YLvX, // v5, v8
// DARKNET
_TYPE_D_TINY,
_TYPE_D_NORM,
// GPU
_TYPE_G_NORM,
_TYPE_G_CUDA,
_TYPE_G_VINO,
_TYPE_G_TRT,
_TYPE_G_DML,
_TYPE_G_DNNL,
// TRT
_TYPE_T_TRT,
}
public enum ENUM_MODEL_DLL
{
_MODEL_DLL_CPU = 0, // CPU 21.4.2
_MODEL_DLL_CPU2, // CPU 22.3.0 XP
_MODEL_DLL_CPU3, // CPU 23.0.0
_MODEL_DLL_GPU, // DARKNET
_MODEL_DLL_GPU2, // GPU
_MODEL_DLL_GPU3, // TRT
}
public enum ENUM_DLL_RESULT
{
_DLL_RST_OK = 1,
_DLL_RST_ERROR = 0,
_DLL_RST_ERROR_KEY = -1,
_DLL_RST_ERROR_MODEL = -2,
}
}

View File

@@ -0,0 +1,372 @@
using System.Runtime.InteropServices;
using DeepLearningSharp;
using OpenCvSharp;
namespace DeepLearning_Model_Sharp
{
public struct BBOX_T
{
public uint x, y, w, h;
public float prob;
public uint obj_id;
}
public struct LPR_Boxes
{
public uint x, y, w, h;
public float prob;
public uint obj_id;
}
public struct LPR_RESULT_PTR
{
public IntPtr vLPR_Boxes_Ptr;
public int nLPR_Boxes;
public int nRST_LP;
public string mRST_Code;
}
public struct LPR_RESULT_RST
{
public List<LPR_Boxes> vLPR_Boxes;
public LPR_Boxes mRect_LP;
public int nRST_LP;
public string mRST_Code;
}
public class DeepLearning_Wrapper : IDisposable
{
public const string DLL_DETECTION = "DeepLearningLPR26.dll";
public const string DLL_OCR = "DeepLearningLPR.dll";
private IntPtr _pModel = IntPtr.Zero;
private readonly int _nModel_DLL = (int)ENUM_MODEL_DLL._MODEL_DLL_CPU3;
private readonly int _nModel_Type = (int)ENUM_MODEL_TYPE._TYPE_C_YLvX;
private readonly int _nMulti = 1;
private readonly int _nDecryption = 1;
private readonly int _GPUID = 0;
private int _nDLL_RST = (int)ENUM_DLL_RESULT._DLL_RST_ERROR;
private readonly string _cf = "";
private readonly string _wf;
private readonly DeepLearning_Model_Wrapper mModel_Wrapper;
public DeepLearning_Wrapper(int nModel_DLL, string cf, string wf, int nModel_Type, int nMulti, int nDecryption)
{
_nModel_DLL = nModel_DLL;
_nModel_Type = nModel_Type;
_nMulti = nMulti;
_nDecryption = nDecryption;
_cf = cf;
_wf = wf;
mModel_Wrapper = new DeepLearning_Model_Wrapper();
}
public DeepLearning_Wrapper()
{
_wf = DLL_DETECTION;
mModel_Wrapper = new DeepLearning_Model_Wrapper();
}
public DeepLearning_Wrapper(string wf)
{
_wf = wf;
mModel_Wrapper = new DeepLearning_Model_Wrapper();
}
public int Model_Wrapper_Construct()
{
try
{
_nDLL_RST = mModel_Wrapper._Model_Construct(_nModel_DLL, ref _pModel, _cf, _wf, _nModel_Type, _nDecryption, _GPUID);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
return _nDLL_RST;
}
public int Model_Wrapper_Destruct()
{
try
{
_nDLL_RST = mModel_Wrapper._Model_Destruct(_nModel_DLL, ref _pModel);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
return _nDLL_RST;
}
public int Model_Wrapper_Getversion(ref string strVersion)
{
try
{
_nDLL_RST = mModel_Wrapper._Model_Getversion(_nModel_DLL, _pModel, ref strVersion);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
return _nDLL_RST;
}
public int Model_Wrapper_Detect(Mat mat_SRC, ref List<BBOX_T> vBBOX_T)
{
IntPtr matPtr = mat_SRC.CvPtr;
try
{
IntPtr vBBOX_TPtr = IntPtr.Zero;
int nBBOX_T = 0;
_nDLL_RST = mModel_Wrapper._Model_Detect(_nModel_DLL, _pModel, matPtr, ref vBBOX_TPtr, ref nBBOX_T, _nModel_Type);
if (vBBOX_TPtr != IntPtr.Zero)
{
// Calculate the size of each struct element
int structSize = Marshal.SizeOf(typeof(BBOX_T));
// Iterate over the results and convert them to _LPR_Boxes structs
for (int i = 0; i < nBBOX_T; i++)
{
IntPtr structPtr = IntPtr.Add(vBBOX_TPtr, i * structSize);
BBOX_T resultItem = Marshal.PtrToStructure<BBOX_T>(structPtr);
vBBOX_T.Add(resultItem);
}
// Clean up the allocated memory
mModel_Wrapper._Model_FreeHGlobal(vBBOX_TPtr);
}
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
finally
{
;
}
return _nDLL_RST;
}
public int Model_Wrapper_LPR_Plate(Mat mat_LPR, Rect mRect_ROI, ref List<LPR_Boxes> vLPR_Boxes)
{
IntPtr matPtr = mat_LPR.CvPtr;
IntPtr rectPtr = IntPtr.Zero;
try
{
rectPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Rect)));
Marshal.StructureToPtr(mRect_ROI, rectPtr, false);
IntPtr vLPR_BoxesPtr = IntPtr.Zero;
int nLPR_Boxes = 0;
_nDLL_RST = mModel_Wrapper._Model_LPR_Plate(_nModel_DLL, _pModel, matPtr, rectPtr, ref vLPR_BoxesPtr, ref nLPR_Boxes, _nMulti, _nModel_Type);
if (vLPR_BoxesPtr != IntPtr.Zero)
{
// Calculate the size of each struct element
int structSize = Marshal.SizeOf(typeof(LPR_Boxes));
// Iterate over the results and convert them to _LPR_Boxes structs
for (int i = 0; i < nLPR_Boxes; i++)
{
IntPtr structPtr = IntPtr.Add(vLPR_BoxesPtr, i * structSize);
LPR_Boxes resultItem = Marshal.PtrToStructure<LPR_Boxes>(structPtr);
vLPR_Boxes.Add(resultItem);
}
// Clean up the allocated memory
mModel_Wrapper._Model_FreeHGlobal(vLPR_BoxesPtr);
}
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
finally
{
if (rectPtr != IntPtr.Zero) Marshal.FreeHGlobal(rectPtr);
}
return _nDLL_RST;
}
public int Model_Wrapper_LPR_Code(Mat mat_LPR, Rect mRect_LP, ref LPR_RESULT_RST mLPR_RESULT_RST)
{
IntPtr matPtr = mat_LPR.CvPtr;
IntPtr rectPtr = IntPtr.Zero;
try
{
rectPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Rect)));
Marshal.StructureToPtr(mRect_LP, rectPtr, false);
IntPtr LPR_ResultPtr = IntPtr.Zero;
int nLPR_Result = 0;
_nDLL_RST = mModel_Wrapper._Model_LPR_Code(_nModel_DLL, _pModel, matPtr, rectPtr, ref LPR_ResultPtr, ref nLPR_Result, _nModel_Type);
if (LPR_ResultPtr != IntPtr.Zero)
{
// Calculate the size of each struct element
int structSize = Marshal.SizeOf(typeof(LPR_RESULT_PTR));
List<LPR_RESULT_PTR> vLPR_RESULT = [];
// Iterate over the results and convert them to _LPR_Boxes struct
for (int i = 0; i < nLPR_Result; i++)
{
IntPtr structPtr = IntPtr.Add(LPR_ResultPtr, i * structSize);
LPR_RESULT_PTR resultItem = Marshal.PtrToStructure<LPR_RESULT_PTR>(structPtr);
vLPR_RESULT.Add(resultItem);
}
for (int k = 0; k < vLPR_RESULT.Count; k++)
{
IntPtr vLPR_BoxesPtr = vLPR_RESULT[k].vLPR_Boxes_Ptr;
int nLPR_Boxes = vLPR_RESULT[k].nLPR_Boxes;
if (vLPR_BoxesPtr != IntPtr.Zero)
{
// Calculate the size of each struct element
int structSize_Boxes = Marshal.SizeOf(typeof(LPR_Boxes));
mLPR_RESULT_RST.vLPR_Boxes = [];
// Iterate over the results and convert them to _LPR_Boxes struct
for (int i = 0; i < nLPR_Boxes; i++)
{
IntPtr structPtr = IntPtr.Add(vLPR_BoxesPtr, i * structSize);
LPR_Boxes resultItem = Marshal.PtrToStructure<LPR_Boxes>(structPtr);
if (i == 0) mLPR_RESULT_RST.mRect_LP = resultItem;
else mLPR_RESULT_RST.vLPR_Boxes.Add(resultItem);
}
mLPR_RESULT_RST.nRST_LP = vLPR_RESULT[k].nRST_LP;
mLPR_RESULT_RST.mRST_Code = vLPR_RESULT[k].mRST_Code;
// Clean up the allocated memory
mModel_Wrapper._Model_FreeHGlobal(vLPR_BoxesPtr);
}
}
// Clean up the allocated memory
mModel_Wrapper._Model_FreeHGlobal(LPR_ResultPtr);
}
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
finally
{
if (rectPtr != IntPtr.Zero) Marshal.FreeHGlobal(rectPtr);
}
return _nDLL_RST;
}
public int Model_Wrapper_LPR_RST(Mat mat_LPR, Rect mRect_ROI, ref List<LPR_RESULT_RST> vLPR_RESULT_RST)
{
IntPtr matPtr = mat_LPR.CvPtr;
IntPtr rectPtr = IntPtr.Zero;
try
{
rectPtr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Rect)));
Marshal.StructureToPtr(mRect_ROI, rectPtr, false);
IntPtr LPR_ResultPtr = IntPtr.Zero;
int nLPR_Result = 0;
_nDLL_RST = mModel_Wrapper._Model_LPR_RST(_nModel_DLL, _pModel, matPtr, rectPtr, ref LPR_ResultPtr, ref nLPR_Result, _nMulti, _nModel_Type);
if (LPR_ResultPtr != IntPtr.Zero)
{
// Calculate the size of each struct element
int structSize = Marshal.SizeOf(typeof(LPR_RESULT_PTR));
List<LPR_RESULT_PTR> vLPR_RESULT = [];
// Iterate over the results and convert them to _LPR_Boxes struct
for (int i = 0; i < nLPR_Result; i++)
{
IntPtr structPtr = IntPtr.Add(LPR_ResultPtr, i * structSize);
LPR_RESULT_PTR resultItem = Marshal.PtrToStructure<LPR_RESULT_PTR>(structPtr);
vLPR_RESULT.Add(resultItem);
}
LPR_RESULT_RST mLPR_RESULT_RST = new();
for (int k = 0; k < vLPR_RESULT.Count; k++)
{
IntPtr vLPR_BoxesPtr = vLPR_RESULT[k].vLPR_Boxes_Ptr;
int nLPR_Boxes = vLPR_RESULT[k].nLPR_Boxes;
if (vLPR_BoxesPtr != IntPtr.Zero)
{
// Calculate the size of each struct element
int structSize_Boxes = Marshal.SizeOf(typeof(LPR_Boxes));
mLPR_RESULT_RST.vLPR_Boxes = [];
// Iterate over the results and convert them to _LPR_Boxes struct
for (int i = 0; i < nLPR_Boxes; i++)
{
IntPtr structPtr = IntPtr.Add(vLPR_BoxesPtr, i * structSize);
LPR_Boxes resultItem = Marshal.PtrToStructure<LPR_Boxes>(structPtr);
if (i == 0) mLPR_RESULT_RST.mRect_LP = resultItem;
else mLPR_RESULT_RST.vLPR_Boxes.Add(resultItem);
}
mLPR_RESULT_RST.nRST_LP = vLPR_RESULT[k].nRST_LP;
mLPR_RESULT_RST.mRST_Code = vLPR_RESULT[k].mRST_Code;
vLPR_RESULT_RST.Add(mLPR_RESULT_RST);
// Clean up the allocated memory
mModel_Wrapper._Model_FreeHGlobal(vLPR_BoxesPtr);
}
}
// Clean up the allocated memory
mModel_Wrapper._Model_FreeHGlobal(LPR_ResultPtr);
}
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
finally
{
if (rectPtr != IntPtr.Zero) Marshal.FreeHGlobal(rectPtr);
}
return _nDLL_RST;
}
public void Dispose()
{
try
{
_nDLL_RST = Model_Wrapper_Destruct();
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
GC.SuppressFinalize(this);
}
}
}

56
Inno.LPR/Inno.LPR.csproj Normal file
View File

@@ -0,0 +1,56 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenCvSharp4" Version="4.9.0.20240103" />
</ItemGroup>
<ItemGroup>
<Reference Include="DeepLearning_Sharp">
<HintPath>Libraries\DeepLearning_Sharp.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="Libraries\DeepLearningLPR.dll">
<Link>DeepLearningLPR.dll</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Libraries\DeepLearningLPR26.dll">
<Link>DeepLearningLPR26.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Libraries\DeepLearning_CPU3.dll">
<Link>DeepLearning_CPU3.dll</Link>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Libraries\DeepLearning_Model.dll">
<Link>DeepLearning_Model.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Libraries\DeepLearning_Sharp.dll">
<Link>DeepLearning_Sharp.dll</Link>
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</None>
<None Update="Libraries\opencv_world4120.dll">
<Link>opencv_world4120.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Libraries\opencv_world470.dll">
<Link>opencv_world470.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Libraries\tbb12.dll">
<Link>tbb12.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,90 @@
using DeepLearning_Model_Sharp;
using OpenCvSharp;
namespace Inno.LPR
{
public class LicensePlateMotionDetector : IDisposable
{
private readonly DeepLearning_Wrapper _wrapperDetection;
private readonly DeepLearning_Wrapper _wrapperRecognition;
public string ModelVersion { get; private set; }
public LicensePlateMotionDetector()
{
try
{
_wrapperDetection = new DeepLearning_Wrapper(DeepLearning_Wrapper.DLL_DETECTION);
_wrapperRecognition = new DeepLearning_Wrapper(DeepLearning_Wrapper.DLL_OCR);
var constructResult1 = _wrapperDetection.Model_Wrapper_Construct();
if (constructResult1 != 1) { throw new InvalidOperationException("Detection Construct Fail"); }
var constructResult2 = _wrapperRecognition.Model_Wrapper_Construct();
if (constructResult2 != 1) { throw new InvalidOperationException("Recognition Construct Fail"); }
string modelVersion = string.Empty;
var getVersionResult = _wrapperDetection.Model_Wrapper_Getversion(ref modelVersion);
if (getVersionResult != 1) { throw new InvalidOperationException("Getversion Fail"); }
ModelVersion = modelVersion;
}
catch (Exception)
{
throw;
}
}
public List<LPR_Boxes> DetectLicensePlateExistence(Mat image, Rect roi)
{
List<LPR_Boxes> vLPR_Boxes = [];
_ = _wrapperDetection.Model_Wrapper_LPR_Plate(image, roi, ref vLPR_Boxes);
return vLPR_Boxes;
}
public LicensePlateRecognitionMotionData DetectLicensePlateExistence(MotionImageData data)
{
using var originalImage = data.Image;
Rect rectangleROI = data.ROI;
List<LPR_Boxes> vLPR_Boxes = [];
var isLicensePlateExist = _wrapperDetection.Model_Wrapper_LPR_Plate(originalImage, rectangleROI, ref vLPR_Boxes);
bool value = isLicensePlateExist == 1 && vLPR_Boxes.Exists(x => x.w * x.h > data.MinimumArea && x.w * x.h < data.MaximumArea);
var motionData = new LicensePlateRecognitionMotionData(originalImage, data.CreatedTime, vLPR_Boxes, value);
return motionData;
}
public LPR_RESULT_RST DetectLicensePlateCode(Mat image, List<LPR_Boxes> lpList)
{
var mLPR_Boxes = lpList.OrderByDescending(box => box.y).First();
Rect rect_LP = new((int)mLPR_Boxes.x, (int)mLPR_Boxes.y, (int)mLPR_Boxes.w, (int)mLPR_Boxes.h);
LPR_RESULT_RST mLPR_RESULT_RST = new();
_ = _wrapperRecognition.Model_Wrapper_LPR_Code(image, rect_LP, ref mLPR_RESULT_RST);
return mLPR_RESULT_RST;
}
public LPR_RESULT_RST DetectLicensePlateCode(Mat image, LPR_Boxes mLPR_Boxes)
{
Rect rect_LP = new((int)mLPR_Boxes.x, (int)mLPR_Boxes.y, (int)mLPR_Boxes.w, (int)mLPR_Boxes.h);
LPR_RESULT_RST mLPR_RESULT_RST = new();
_ = _wrapperRecognition.Model_Wrapper_LPR_Code(image, rect_LP, ref mLPR_RESULT_RST);
return mLPR_RESULT_RST;
}
public void DetectLicensePlateCode(LicensePlateRecognitionMotionData motionData)
{
if (!motionData.IsLicensePlateExist) { return; }
var mLPR_Boxes = motionData.SelectedTrustedLprBox;
Rect rect_LP = new((int)mLPR_Boxes.x, (int)mLPR_Boxes.y, (int)mLPR_Boxes.w, (int)mLPR_Boxes.h);
LPR_RESULT_RST mLPR_RESULT_RST = new();
var isExistCode = _wrapperRecognition.Model_Wrapper_LPR_Code(motionData.OriginalImage, rect_LP, ref mLPR_RESULT_RST);
if (isExistCode != 1) { return; }
//motionData.DrawLicensePlateCharacterBoxes(mLPR_RESULT_RST.vLPR_Boxes, mLPR_Boxes.x, mLPR_Boxes.y);
motionData.AddLicensePlateCode(mLPR_RESULT_RST.mRST_Code);
}
public void Dispose()
{
_wrapperDetection?.Dispose();
_wrapperRecognition?.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,45 @@
using DeepLearning_Model_Sharp;
using OpenCvSharp;
namespace Inno.LPR
{
public class LicensePlateRecognitionMotionData
{
public Mat OriginalImage { get; private set; }
public DateTime ImageCreatedTime { get; init; }
public List<LPR_Boxes> DetectedLprBoxList { get; init; } = [];
public LPR_Boxes SelectedTrustedLprBox { get; private set; }
public bool IsLicensePlateExist { get; private set; } = false;
public List<string> LicensePlateCodeList { get; set; } = [];
public string LicensePlateCode { get; private set; } = string.Empty;
public int FrameNumber { get; set; } = -1;
public LicensePlateRecognitionMotionData(Mat originalImage, DateTime imageCreatedTime, List<LPR_Boxes> lprBoxes, bool isLicensePlateExist)
{
OriginalImage = originalImage.Clone();
ImageCreatedTime = imageCreatedTime;
DetectedLprBoxList.AddRange(lprBoxes);
if (DetectedLprBoxList.Count > 0)
{
SelectedTrustedLprBox = DetectedLprBoxList.OrderByDescending(box => box.y).First();
}
IsLicensePlateExist = isLicensePlateExist;
}
public void DrawLicensePlateCharacterBoxes(List<LPR_Boxes> boxes, uint coordinateOriginX, uint coordinateOriginY)
{
if (!IsLicensePlateExist) { return; }
//foreach (_LPR_Boxes mLPR_Chars in boxes)
//{
// Rect rect_Char = new((int)mLPR_Chars.x + (int)coordinateOriginX, (int)mLPR_Chars.y + (int)coordinateOriginY,
// (int)mLPR_Chars.w, (int)mLPR_Chars.h);
// Cv2.Rectangle(LicensePlateDisplayImage, rect_Char, Scalar.Blue, 1);
//}
}
public void AddLicensePlateCode(string lpCode)
{
LicensePlateCode = lpCode;
}
}
}

View File

@@ -0,0 +1,13 @@
using OpenCvSharp;
namespace Inno.LPR
{
public class MotionImageData(Mat image, Rect roi, int minimumArea, int maximumArea)
{
public Mat Image { get; private set; } = image;
public DateTime CreatedTime { get; private set; } = DateTime.Now;
public Rect ROI { get; private set; } = roi;
public int MinimumArea { get; private set; } = minimumArea;
public int MaximumArea { get; private set; } = maximumArea;
}
}

12
LPR_Manager.slnx Normal file
View File

@@ -0,0 +1,12 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="x64" />
</Configurations>
<Project Path="Inno.LPR/Inno.LPR.csproj">
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="LPR_Manager/LPR_Manager.csproj">
<Platform Solution="*|x64" Project="x64" />
</Project>
</Solution>

25
LPR_Manager/App.xaml Normal file
View File

@@ -0,0 +1,25 @@
<Application x:Class="LPR_Manager.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="Indigo" SecondaryColor="Lime" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Card.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ListBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.DataGrid.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TextBlock.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ProgressBar.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ComboBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.CheckBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ToggleButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.RadioButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TabControl.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

62
LPR_Manager/App.xaml.cs Normal file
View File

@@ -0,0 +1,62 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace LPR_Manager;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// 0. Initialize Logging
var configService = new Service.ConfigService();
var sysConfig = await configService.LoadSystemConfigAsync();
Service.LogService.ConfigureLogging(sysConfig.LogMode);
// 1. Create Splash Screen
var splash = new View.SplashScreen();
// 2. Show Main Window immediately
var main = new MainWindow();
this.MainWindow = main;
main.Show();
// Center Splash on Main Window
if (main.WindowState == WindowState.Maximized)
{
// If maximized, center on screen is usually correct, but let's ensure
splash.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
else
{
// Calculate center relative to Main
splash.WindowStartupLocation = WindowStartupLocation.Manual;
splash.Left = main.Left + (main.Width - splash.Width) / 2;
splash.Top = main.Top + (main.Height - splash.Height) / 2;
}
splash.Topmost = true; // Re-enforce topmost just in case
splash.Show(); // Show Splash AFTER calculating position
// 3. Wait for MainViewModel Init OR 10s Timeout
if (main.DataContext is ViewModel.MainViewModel vm)
{
var timeoutTask = Task.Delay(10000); // 10 seconds max
var initTask = vm.InitializationTask;
await Task.WhenAny(initTask, timeoutTask);
}
else
{
await Task.Delay(3000);
}
splash.Close();
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,13 @@
namespace LPR_Manager.Code.GlobalObject
{
public static class BaseInfo
{
public static string DeviceCode { get; set; } = "D0001"; // Default mock
}
public static class MainDeviceItem
{
public static string LEDDisplayMessageNoMember { get; set; } = "미등록차량";
public static string LEDDisplayMessageMember { get; set; } = "정기권차량";
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace LPR_Manager.Converters
{
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Collapsed : Visibility.Visible;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

BIN
LPR_Manager/Icon/CI.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
LPR_Manager/Icon/CI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

View File

@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
<ApplicationIcon>Icon\CI.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Resource Include="Icon\CI.png" />
<Resource Include="Icon\CI.ico" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignColors" Version="5.3.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Inno.LPR\Inno.LPR.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Libraries\" />
</ItemGroup>
<ItemGroup>
<None Update="Libraries\NtcClientAPI4Net.dll">
<Link>NtcClientAPI4Net.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,264 @@
<Project>
<PropertyGroup>
<AssemblyName>LPR_Manager</AssemblyName>
<IntermediateOutputPath>obj\Debug\</IntermediateOutputPath>
<BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath>
<MSBuildProjectExtensionsPath>D:\Documents\Projects\Parking\Center\LPR_Manager\LPR_Manager\obj\</MSBuildProjectExtensionsPath>
<_TargetAssemblyProjectName>LPR_Manager</_TargetAssemblyProjectName>
<RootNamespace>LPR_Manager</RootNamespace>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<None Update="..\..\..\NtcSDK\x64\release_static_mt\NtcClientAPI4Net.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="MaterialDesignColors" Version="5.3.0" />
<PackageReference Include="MaterialDesignThemes" Version="5.3.0" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
<PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.11.0.20250507" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Inno.LPR\Inno.LPR.csproj" />
</ItemGroup>
<ItemGroup>
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\Accessibility.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\communitytoolkit.mvvm\8.4.0\lib\net8.0\CommunityToolkit.Mvvm.dll" />
<ReferencePath Include="D:\Documents\Projects\Parking\Center\LPR_Manager\Inno.LPR\bin\Debug\net8.0-windows\Inno.LPR.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\materialdesigncolors\5.3.0\lib\net8.0-windows7.0\MaterialDesignColors.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\materialdesignthemes\5.3.0\lib\net8.0-windows7.0\MaterialDesignThemes.Wpf.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\Microsoft.CSharp.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\Microsoft.VisualBasic.Core.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\Microsoft.VisualBasic.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\Microsoft.Win32.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\Microsoft.Win32.Registry.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\Microsoft.Win32.Registry.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\Microsoft.Win32.SystemEvents.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\microsoft.xaml.behaviors.wpf\1.1.77\lib\net6.0-windows7.0\Microsoft.Xaml.Behaviors.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\mscorlib.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\netstandard.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\opencvsharp4\4.11.0.20250507\lib\net6.0\OpenCvSharp.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\opencvsharp4.wpfextensions\4.11.0.20250507\lib\net6.0\OpenCvSharp.WpfExtensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationCore.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.Aero.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.Aero2.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.AeroLite.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.Classic.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.Luna.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationFramework.Royale.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\PresentationUI.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\ReachFramework.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\serilog\4.3.0\lib\net8.0\Serilog.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\serilog.sinks.file\7.0.0\lib\net8.0\Serilog.Sinks.File.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.AppContext.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Buffers.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.CodeDom.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Collections.Concurrent.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Collections.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Collections.Immutable.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Collections.NonGeneric.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Collections.Specialized.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ComponentModel.Annotations.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ComponentModel.DataAnnotations.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ComponentModel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ComponentModel.EventBasedAsync.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ComponentModel.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ComponentModel.TypeConverter.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Configuration.ConfigurationManager.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Configuration.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Console.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Core.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Data.Common.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Data.DataSetExtensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Data.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.Contracts.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.Debug.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.DiagnosticSource.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.EventLog.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.FileVersionInfo.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.PerformanceCounter.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.Process.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.StackTrace.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.TextWriterTraceListener.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.Tools.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.TraceSource.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Diagnostics.Tracing.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.DirectoryServices.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.dll" />
<ReferencePath Include="C:\Users\jabez\.nuget\packages\system.drawing.common\8.0.11\lib\net8.0\System.Drawing.Common.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Drawing.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Drawing.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Dynamic.Runtime.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Formats.Asn1.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Formats.Tar.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Globalization.Calendars.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Globalization.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Globalization.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.Compression.Brotli.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.Compression.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.Compression.FileSystem.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.Compression.ZipFile.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.FileSystem.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.FileSystem.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.FileSystem.DriveInfo.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.FileSystem.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.FileSystem.Watcher.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.IsolatedStorage.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.MemoryMappedFiles.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.IO.Packaging.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.Pipes.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.Pipes.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.IO.UnmanagedMemoryStream.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Linq.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Linq.Expressions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Linq.Parallel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Linq.Queryable.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Memory.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Http.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Http.Json.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.HttpListener.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Mail.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.NameResolution.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.NetworkInformation.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Ping.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Quic.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Requests.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Security.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.ServicePoint.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.Sockets.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.WebClient.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.WebHeaderCollection.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.WebProxy.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.WebSockets.Client.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Net.WebSockets.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Numerics.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Numerics.Vectors.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ObjectModel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Printing.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.DispatchProxy.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.Emit.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.Emit.ILGeneration.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.Emit.Lightweight.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.Metadata.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Reflection.TypeExtensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Resources.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Resources.Reader.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Resources.ResourceManager.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Resources.Writer.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.CompilerServices.Unsafe.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.CompilerServices.VisualC.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Handles.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.InteropServices.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.InteropServices.JavaScript.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.InteropServices.RuntimeInformation.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Intrinsics.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Loader.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Numerics.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Serialization.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Serialization.Formatters.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Serialization.Json.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Serialization.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Runtime.Serialization.Xml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Claims.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Algorithms.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Cng.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Csp.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Encoding.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.OpenSsl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Pkcs.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.ProtectedData.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.X509Certificates.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Security.Cryptography.Xml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Security.Permissions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Principal.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.Principal.Windows.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Security.SecureString.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ServiceModel.Web.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ServiceProcess.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Text.Encoding.CodePages.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Text.Encoding.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Text.Encoding.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Text.Encodings.Web.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Text.Json.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Text.RegularExpressions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Threading.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Channels.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Overlapped.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Tasks.Dataflow.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Tasks.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Tasks.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Tasks.Parallel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Thread.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.ThreadPool.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Threading.Timer.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Transactions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Transactions.Local.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.ValueTuple.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Web.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Web.HttpUtility.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Windows.Controls.Ribbon.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Windows.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Windows.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Windows.Input.Manipulations.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Windows.Presentation.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\System.Xaml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.Linq.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.ReaderWriter.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.Serialization.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.XDocument.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.XmlDocument.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.XmlSerializer.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.XPath.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\ref\net8.0\System.Xml.XPath.XDocument.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\UIAutomationClient.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\UIAutomationClientSideProviders.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\UIAutomationProvider.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\UIAutomationTypes.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.23\ref\net8.0\WindowsBase.dll" />
</ItemGroup>
<ItemGroup>
<Compile Include="D:\Documents\Projects\Parking\Center\LPR_Manager\LPR_Manager\obj\Debug\net8.0-windows\MainWindow.g.cs" />
<Compile Include="D:\Documents\Projects\Parking\Center\LPR_Manager\LPR_Manager\obj\Debug\net8.0-windows\App.g.cs" />
<Compile Include="D:\Documents\Projects\Parking\Center\LPR_Manager\LPR_Manager\obj\Debug\net8.0-windows\LPR_Manager_Content.g.cs" />
<Compile Include="D:\Documents\Projects\Parking\Center\LPR_Manager\LPR_Manager\obj\Debug\net8.0-windows\GeneratedInternalTypeHelper.g.cs" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.102\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll" />
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.102\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll" />
<Analyzer Include="C:\Users\jabez\.nuget\packages\communitytoolkit.mvvm\8.4.0\analyzers\dotnet\roslyn4.12\cs\CommunityToolkit.Mvvm.CodeFixers.dll" />
<Analyzer Include="C:\Users\jabez\.nuget\packages\communitytoolkit.mvvm\8.4.0\analyzers\dotnet\roslyn4.12\cs\CommunityToolkit.Mvvm.SourceGenerators.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.23\analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>

312
LPR_Manager/MainWindow.xaml Normal file
View File

@@ -0,0 +1,312 @@
<Window x:Class="LPR_Manager.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:vm="clr-namespace:LPR_Manager.ViewModel"
xmlns:view="clr-namespace:LPR_Manager.View"
xmlns:converters="clr-namespace:LPR_Manager.Converters"
mc:Ignorable="d"
TextElement.Foreground="{DynamicResource MaterialDesignBody}"
TextElement.FontWeight="Regular"
TextElement.FontSize="13"
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="Auto"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}"
Title="LPR Manager" Height="823" Width="1505" Icon="pack://application:,,,/Icon/CI.ico">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<converters:InverseBooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter"/>
</Window.Resources>
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid Background="#F8F9FB">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Sidebar -->
<ColumnDefinition Width="*"/>
<!-- Main Content Area -->
</Grid.ColumnDefinitions>
<!-- 1. Sidebar -->
<Border Grid.Column="0" Background="White" BorderBrush="#EEEEEE" BorderThickness="0,0,1,0">
<DockPanel LastChildFill="True" Width="70">
<!-- Logo -->
<StackPanel DockPanel.Dock="Top" Margin="0,30,0,50" HorizontalAlignment="Center">
<Border Width="42" Height="42" CornerRadius="10" Background="{DynamicResource PrimaryHueMidBrush}">
<TextBlock Text="P" Foreground="White" FontSize="26" FontWeight="ExtraBold" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,-2,0,0"/>
</Border>
</StackPanel>
<!-- Settings at Bottom -->
<Button DockPanel.Dock="Bottom" Style="{DynamicResource MaterialDesignFlatButton}"
Foreground="#B0BEC5" Margin="0,20" Padding="0" Height="50"
Command="{Binding GoToSettingsCommand}">
<materialDesign:PackIcon Kind="SettingsOutline" Width="26" Height="26"/>
</Button>
<!-- Navigation List -->
<ListBox SelectedIndex="{Binding SelectedTabIndex}"
Style="{DynamicResource MaterialDesignNavigationListBox}"
ItemContainerStyle="{DynamicResource MaterialDesignNavigationListBoxItem}"
BorderThickness="0" Background="Transparent"
SelectionMode="Single">
<ListBox.Resources>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource MaterialDesignNavigationListBoxItem}">
<Setter Property="Padding" Value="0,18"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
</ListBox.Resources>
<ListBoxItem ToolTip="Dashboard">
<materialDesign:PackIcon Kind="ViewDashboard" Width="26" Height="26" HorizontalAlignment="Center"/>
</ListBoxItem>
<ListBoxItem ToolTip="Analytics">
<materialDesign:PackIcon Kind="MonitorDashboard" Width="26" Height="26" HorizontalAlignment="Center" Cursor=""/>
</ListBoxItem>
<ListBoxItem ToolTip="History">
<materialDesign:PackIcon Kind="History" Width="26" Height="26" HorizontalAlignment="Center"/>
</ListBoxItem>
<ListBoxItem ToolTip="Notifications">
<Grid>
<materialDesign:PackIcon Kind="BellOutline" Width="26" Height="26" HorizontalAlignment="Center"
Visibility="{Binding DataContext.HasCriticalErrors, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource InverseBooleanToVisibilityConverter}}"/>
<materialDesign:PackIcon Kind="BellAlert" Width="26" Height="26" HorizontalAlignment="Center" Foreground="#FF5252"
Visibility="{Binding DataContext.HasCriticalErrors, RelativeSource={RelativeSource AncestorType=Window}, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Grid>
</ListBoxItem>
<ListBoxItem ToolTip="Settings">
<materialDesign:PackIcon Kind="Cog" Width="26" Height="26" HorizontalAlignment="Center"/>
</ListBoxItem>
</ListBox>
</DockPanel>
</Border>
<!-- 2. Main Content Container -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<!-- Top Header -->
<RowDefinition Height="*"/>
<!-- Tab Content -->
</Grid.RowDefinitions>
<!-- Top Header -->
<Border Grid.Row="0" Background="White" Padding="24,14" BorderBrush="#EEEEEE" BorderThickness="0,0,0,1">
<DockPanel LastChildFill="True">
<!-- Right side User Profile -->
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right" VerticalAlignment="Center">
<!-- Session Timer -->
<TextBlock Text="{Binding SessionTimeRemaining}"
Foreground="#FF8A65" FontWeight="Bold" FontSize="14"
VerticalAlignment="Center" Margin="0,0,16,0"
Visibility="{Binding SessionTimeRemaining, Converter={StaticResource BooleanToVisibilityConverter}, FallbackValue=Collapsed}"/>
<StackPanel VerticalAlignment="Center" Margin="0,0,14,0" HorizontalAlignment="Right">
<TextBlock Text="{Binding UserName}" FontSize="13" FontWeight="Bold" Foreground="#263238" HorizontalAlignment="Right"/>
<TextBlock Text="{Binding UserRole}" FontSize="10" FontWeight="Bold" Foreground="#9FA8DA" HorizontalAlignment="Right"/>
</StackPanel>
<Button Style="{DynamicResource MaterialDesignFlatButton}" Padding="0" Margin="0" Width="40" Height="40"
Command="{Binding LoginCommand}" ToolTip="Click to Toggle Login/Logout">
<Border Width="40" Height="40" CornerRadius="20" Background="#F1F3F4">
<materialDesign:PackIcon Kind="Account" Width="28" Height="28" Foreground="#B0BEC5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Button>
</StackPanel>
<!-- Left side Title & Status -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Multi-Site Command" FontSize="18" FontWeight="Bold" Foreground="#263238" VerticalAlignment="Center"/>
<Border Width="1" Height="24" Background="#EEEEEE" Margin="24,0"/>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse Width="8" Height="8" Fill="#4CAF50" Margin="0,1,8,0"/>
<TextBlock Text="IIS STATUS: " Foreground="#90A4AE" FontSize="12"/>
<TextBlock Text="{Binding HealthStatus}" Foreground="#4CAF50" FontSize="12" FontWeight="Bold"/>
<TextBlock Text="{Binding CurrentTime}" Foreground="#90A4AE" FontSize="12" Margin="20,0,0,0" FontFamily="Segoe UI Semibold"/>
</StackPanel>
</StackPanel>
</DockPanel>
</Border>
<!-- Main Tab Control -->
<Grid Grid.Row="1">
<!-- Navigation Arrows (Mockup UI) -->
<Button HorizontalAlignment="Left" VerticalAlignment="Center" Margin="12,0"
Style="{DynamicResource MaterialDesignFloatingActionMiniButton}"
Background="White" Foreground="{DynamicResource PrimaryHueMidBrush}"
Width="36" Height="36" BorderThickness="0"
Click="OnScrollLeft">
<materialDesign:PackIcon Kind="ChevronLeft" Width="20" Height="20"/>
<Button.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.08"/>
</Button.Effect>
</Button>
<Button HorizontalAlignment="Right" VerticalAlignment="Center" Margin="12,0"
Style="{DynamicResource MaterialDesignFloatingActionMiniButton}"
Background="White" Foreground="{DynamicResource PrimaryHueMidBrush}"
Width="36" Height="36" BorderThickness="0"
Click="OnScrollRight">
<materialDesign:PackIcon Kind="ChevronRight" Width="20" Height="20"/>
<Button.Effect>
<DropShadowEffect BlurRadius="10" ShadowDepth="2" Opacity="0.08"/>
</Button.Effect>
</Button>
<materialDesign:TransitioningContent Margin="60,20">
<TabControl SelectedIndex="{Binding SelectedTabIndex}"
Style="{DynamicResource MaterialDesignTabControl}"
BorderThickness="0">
<TabControl.ItemContainerStyle>
<Style TargetType="TabItem">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</TabControl.ItemContainerStyle>
<!-- 0: DASHBOARD -->
<TabItem>
<ScrollViewer x:Name="DashboardScrollViewer" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Lanes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<view:LprLaneControl/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</TabItem>
<!-- 1: ANALYTICS -->
<TabItem>
<Grid Margin="20">
<DockPanel>
<Border DockPanel.Dock="Top" Margin="0,0,0,20">
<TextBlock Text="Recognition Performance Analytics" FontSize="20" FontWeight="Bold" Foreground="#37474F"/>
</Border>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Lanes}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<materialDesign:Card Width="300" Margin="10" Padding="16" UniformCornerRadius="8">
<StackPanel>
<DockPanel LastChildFill="True" Margin="0,0,0,10">
<TextBlock Text="{Binding LaneName}" FontWeight="Bold" Foreground="#455A64" FontSize="16"/>
<TextBlock Text="{Binding SuccessRate, StringFormat={}{0:F1}%}" HorizontalAlignment="Right" FontWeight="Bold" Foreground="{DynamicResource PrimaryHueMidBrush}"/>
</DockPanel>
<Grid Height="10" Margin="0,0,0,15">
<ProgressBar Value="{Binding SuccessRate}" Maximum="100" Height="10"
Background="#ECEFF1" BorderThickness="0"
Foreground="{DynamicResource PrimaryHueMidBrush}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Total" FontSize="11" Foreground="#90A4AE"/>
<TextBlock Text="{Binding TotalRecognitions}" FontSize="16" FontWeight="SemiBold"/>
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Success" FontSize="11" Foreground="#90A4AE"/>
<TextBlock Text="{Binding SuccessCount}" FontSize="16" FontWeight="SemiBold" Foreground="#4CAF50"/>
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="Fail" FontSize="11" Foreground="#90A4AE"/>
<TextBlock Text="{Binding FailCount}" FontSize="16" FontWeight="SemiBold" Foreground="#EF5350"/>
</StackPanel>
</Grid>
</StackPanel>
</materialDesign:Card>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Grid>
</TabItem>
<!-- 2: HISTORY -->
<TabItem>
<Grid Margin="20">
<materialDesign:Card Padding="0" UniformCornerRadius="12">
<DockPanel>
<Border DockPanel.Dock="Top" Background="#F1F3F4" Padding="20,12">
<TextBlock Text="System Activity Logs" FontWeight="Bold" Foreground="#37474F"/>
</Border>
<ListBox ItemsSource="{Binding Logs}" Padding="10" BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</materialDesign:Card>
</Grid>
</TabItem>
<!-- 3: ALERTS / NOTIFICATIONS -->
<TabItem>
<Grid Margin="20">
<DockPanel>
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,20">
<Button DockPanel.Dock="Right" Content="Clear All" Command="{Binding ClearErrorsCommand}" Style="{DynamicResource MaterialDesignOutlinedButton}"/>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Critical Errors" FontSize="20" FontWeight="Bold" Foreground="#D32F2F"/>
<TextBlock Text="Review and resolve critical system issues" FontSize="12" Foreground="#90A4AE"/>
</StackPanel>
</DockPanel>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding ErrorLogs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<materialDesign:Card Margin="0,0,0,10" Padding="16" UniformCornerRadius="6" BorderBrush="#FFEBEE" BorderThickness="1">
<DockPanel>
<materialDesign:PackIcon Kind="AlertCircleOutline" Width="24" Height="24" Foreground="#D32F2F" Margin="0,0,16,0" VerticalAlignment="Top"/>
<StackPanel>
<DockPanel Margin="0,0,0,4">
<TextBlock Text="{Binding Timestamp, StringFormat={}{0:yyyy-MM-dd HH:mm:ss}}" Foreground="#90A4AE" FontSize="12" DockPanel.Dock="Right"/>
<TextBlock Text="System Error" FontWeight="Bold" Foreground="#37474F"/>
</DockPanel>
<TextBlock Text="{Binding Message}" TextWrapping="Wrap" Foreground="#546E7A"/>
</StackPanel>
</DockPanel>
</materialDesign:Card>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Grid>
</TabItem>
<!-- 4: SETTINGS -->
<TabItem>
<view:SettingsControl/>
</TabItem>
</TabControl>
</materialDesign:TransitioningContent>
</Grid>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,45 @@
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace LPR_Manager;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (DataContext is ViewModel.MainViewModel vm) vm.ResetIdleTimer();
}
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
if (DataContext is ViewModel.MainViewModel vm) vm.ResetIdleTimer();
}
private void OnScrollLeft(object sender, RoutedEventArgs e)
{
DashboardScrollViewer.ScrollToHorizontalOffset(DashboardScrollViewer.HorizontalOffset - 380);
}
private void OnScrollRight(object sender, RoutedEventArgs e)
{
DashboardScrollViewer.ScrollToHorizontalOffset(DashboardScrollViewer.HorizontalOffset + 380);
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace LPR_Manager.Model
{
public enum LogSeverity
{
Info,
Warning,
Error
}
public class AppLogEntry
{
public DateTime Timestamp { get; set; } = DateTime.Now;
public string Message { get; set; } = string.Empty;
public LogSeverity Severity { get; set; } = LogSeverity.Info;
public string Source { get; set; } = "System";
public string DisplayTimestamp => Timestamp.ToString("HH:mm:ss");
public override string ToString()
{
return $"[{DisplayTimestamp}] [{Severity}] {Message}";
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
//using Inno.Communication.TCP;
using LPR_Manager.Model.Device.Interfaces;
using LPR_Manager.Protocol.Gate;
namespace LPR_Manager.Model.Device
{
public class GateController : IGateController
{
// private readonly TcpClientConnector _connector = new();
private readonly string _ip;
private readonly int _port;
private CancellationTokenSource _cts;
public GateStatus Status { get; private set; } = GateStatus.CLOSED;
// public bool IsConnected => _connector.IsConnected;
public GateController(string ip, int port)
{
_ip = ip;
_port = port;
}
public async Task<bool> ConnectAsync()
{
try
{
_cts = new CancellationTokenSource();
// await _connector.ConnectAsync(_ip, _port, 2000, _cts.Token);
return true;
}
catch
{
return false;
}
}
public void Disconnect()
{
_cts?.Cancel();
// _connector.Dispose();
}
public void Open()
{
SendCommand(GateSendCMD.OPEN);
}
public void Close()
{
SendCommand(GateSendCMD.CLOSE);
}
private void SendCommand(GateSendCMD cmd)
{
var cmdStr = FPTV1.GetCommand(cmd);
//_connector.WriteAsync(cmdStr, Encoding.ASCII, CancellationToken.None).Wait();
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
using LPR_Manager.Model.Device.SubClass;
namespace LPR_Manager.Model.Device.Interfaces
{
public interface ICmsClient
{
void SendResult(LPRDataSet data);
bool IsConnected { get; }
Task<bool> ConnectAsync();
void Disconnect();
event EventHandler<CMSDataSet> OnCommandReceived;
}
}

View File

@@ -0,0 +1,13 @@
using LPR_Manager.Model;
namespace LPR_Manager.Model.Device.Interfaces
{
public interface IGateController
{
void Open();
void Close();
GateStatus Status { get; }
Task<bool> ConnectAsync();
void Disconnect();
}
}

View File

@@ -0,0 +1,12 @@
using LPR_Manager.Model;
namespace LPR_Manager.Model.Device.Interfaces
{
public interface ILedController
{
void SetText(string top, string bottom, LEDColor topColor = LEDColor.GREEN, LEDColor bottomColor = LEDColor.YELLOW);
void Send();
Task<bool> ConnectAsync();
void Disconnect();
}
}

View File

@@ -0,0 +1,80 @@
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
//using Inno.Communication.TCP;
using LPR_Manager.Model.Device.Interfaces;
using LPR_Manager.Model.Device.SubClass;
using LPR_Manager.Protocol.LED;
using LPR_Manager.Model;
namespace LPR_Manager.Model.Device
{
public class LedController : ILedController
{
// private readonly TcpClientConnector _connector = new();
private readonly string _ip;
private readonly int _port;
private readonly LEDProtocolType _protocolType;
private CancellationTokenSource? _cts;
private LEDTextSet _top = new();
private LEDTextSet _bottom = new();
public LedController(string ip, int port, LEDProtocolType protocolType = LEDProtocolType.YJMICRO)
{
_ip = ip;
_port = port;
_protocolType = protocolType;
}
// public bool IsConnected => _connector.IsConnected;
public async Task<bool> ConnectAsync()
{
try
{
_cts = new CancellationTokenSource();
// await _connector.ConnectAsync(_ip, _port, 2000, _cts.Token);
return true;
}
catch
{
return false;
}
}
public void Disconnect()
{
_cts?.Cancel();
// _connector.Dispose();
}
public void SetText(string top, string bottom, LEDColor topColor = LEDColor.GREEN, LEDColor bottomColor = LEDColor.YELLOW)
{
_top.Message = top;
_top.Color = topColor;
_bottom.Message = bottom;
_bottom.Color = bottomColor;
}
public void Send()
{
byte[] sendData;
if (_protocolType == LEDProtocolType.UJIN)
{
sendData = UJIN.GetMatrixMessage(_top, _bottom);
}
else
{
var cmd = YJMICRO.GetCommand(_top, _bottom);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
sendData = Encoding.GetEncoding(949).GetBytes(cmd);
}
if (sendData != null && sendData.Length > 0)
{
//_connector.WriteAsync(sendData, CancellationToken.None).Wait();
}
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace LPR_Manager.Model.Device.SubClass
{
public class CMSDataSet
{
public CMSCMD MainCmd { get; set; }
public string CenterID { get; set; }
public string Data1 { get; set; }
public string Data2 { get; set; }
public string Data3 { get; set; }
public Enum EnumData { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace LPR_Manager.Model.Device.SubClass
{
public class LEDSendTextSet
{
public LEDSendCMD Cmd { get; set; }
public LEDTextSet TopTextSet { get; set; }
public LEDTextSet BottomTextSet { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System;
namespace LPR_Manager.Model.Device.SubClass
{
public class LEDTextSet : ICloneable
{
public string Message { get; set; } = "";
public LEDColor Color { get; set; } = LEDColor.GREEN;
public LEDColor Color2 { get; set; } = LEDColor.GREEN; // Used in YJMICRO
public TextLocation Location { get; set; } = TextLocation.TOP;
public object Clone()
{
return this.MemberwiseClone();
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
namespace LPR_Manager.Model.Device.SubClass
{
public class LPRDataSet : ICloneable
{
public LPRCMDType LPRCMDType { get; set; } = LPRCMDType.OUTOFCMD;
public RFIDCMDType RFIDCMDType { get; set; } = RFIDCMDType.OUTOFCMD;
public string RFIDRcvdata { get; set; }
public string CMDString { get; set; }
public string DeviceID { get; set; }
public string TimeRaWData { get; set; }
public string ImagePath { get; set; }
public DateTime Time { get; set; }
public string RCarNumber { get; set; }
public bool IsMember { get; set; }
public bool IsInsertedCar { get; set; } = false;
public bool IsSended { get; set; } = false;
public bool IsDone { get; set; } = false;
public bool IsDBInserted { get; set; } = false;
public bool IsBlackList { get; set; } = false;
public bool IsVisitor { get; set; } = false;
public bool IsEmergencyVehicle { get; set; } = false;
public string GetProtocolString()
{
// P0001,20110719-124841-000002-T1,24누3545,/2011/07/19/12484102S_XXXX.jpg,1234567890123456
return $"{DeviceID},{TimeRaWData},{RCarNumber},{ImagePath},{Time:yyyyMMddHHmmss},{RFIDRcvdata}";
}
public string GetRawProtocolString()
{
// STXF0001 | TMP | LPRS | 20100330 - 003241 - 000002 - T1 | 07저9682 | ETX
return $"\u0002{DeviceID}|TMP|LPRS|{TimeRaWData}|{RCarNumber}|\u0003";
}
public object Clone()
{
return this.MemberwiseClone();
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace LPR_Manager.Model
{
public enum GateStatus { OPENDED = 0, OPENING = 1, CLOSED = 2, CLOSING = 3, ERROR = 4, UPLOCK = 5, INITED = 6, CHECKINGSTATUSE = 7 }
public enum GateSendCMD { OPEN, CLOSE, STATUS, UPLOCK, UNLOCK, RESET }
public enum CMSCMD { OUTOFCMD, CONFIRM, GATECTRL, LEDDISPLAY, LEDDISPLAYC, REGREFRESH }
public enum RunningStatus { NORMAL, ERROR }
public enum LEDColor { RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE }
public enum LEDProtocolType { YJMICRO, UJIN }
public enum TextLocation { TOP, BOTTOM }
public enum LEDSendCMD { NORMALTEXT, REGISTERTEXT, NOREGISTERTEXT, NOREGAFTER, NOREGAFTER2, DAYVALIDATION, REMAINDAYOFREG, TWOCAMPVIOLATION, FIVECAMPVIOLATION, BLACKLIST, APTNER, Extra }
public enum LEDMessage { NORMALTOP, NORMALBOTTOM, REGISTERTOP, REGISTERBOTTOM, NOREGISTERTOP, NOREGISTERBOTTOM, NOREGAFTERTOP, NOREGAFTERBOTTOM, NOREGAFTER2TOP, NOREGAFTER2BOTTOM, DAYVALIDATIONTOP, DAYVALIDATIONBOTTOM, REMAINDAYOFREGTOP, REMAINDAYOFREGBOTTOM, TWOCAMPVIOLATIONTOP, TWOCAMPVIOLATIONBOTTOM, FIVECAMPVIOLATIONTOP, FIVECAMPVIOLATIONBOTTOM, BLACKLISTTOP, BLACKLISTBOTTOM, APTNERTOP, APTNERBOTTOM }
public enum LPRCMDType { OUTOFCMD, NORMAL }
public enum RFIDCMDType { OUTOFCMD, NORMAL }
public enum RecognitionOrder
{
BottomUp,
TopDown,
LeftRight,
RightLeft
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace LPR_Manager.Model
{
public class LaneConfig
{
public string LaneName { get; set; } = string.Empty;
public string LaneId { get; set; } = string.Empty;
public string LaneDescription { get; set; } = string.Empty;
public string CameraIp { get; set; } = string.Empty;
public int CameraPort { get; set; } = 80;
public string CameraId { get; set; } = "admin";
public string CameraPassword { get; set; } = "Inno!@#$5678";
public RecognitionOrder RecognitionOrder { get; set; } = RecognitionOrder.BottomUp;
public string RecognitionArea { get; set; } = "0,0,0,0"; // x,y,w,h
public string NoPlateCode { get; set; } = "XXXXXXXX"; // "????????", "XXXXXXXX"
public bool UseSingleShot { get; set; } = false;
public string CmsIp { get; set; } = "127.0.0.1";
public int CmsPort { get; set; } = 6000;
}
}

View File

@@ -0,0 +1,9 @@
namespace LPR_Manager.Model
{
public enum LogMode
{
Seq,
File,
Both
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace LPR_Manager.Model
{
public class LogModel
{
public string Time { get; set; }
public string Plate { get; set; }
public string Result { get; set; }
public LogModel(string plate, string result)
{
Time = DateTime.Now.ToString("HH:mm:ss");
Plate = plate;
Result = result;
}
}
}

View File

@@ -0,0 +1,22 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace LPR_Manager.Model
{
public partial class SystemConfig : ObservableObject
{
[ObservableProperty]
private int _imageServerPort = 5000;
[ObservableProperty]
private string _imageServerId = "image";
[ObservableProperty]
private string _imageServerPassword = "1234";
[ObservableProperty]
private string _imageRootPath = "Images";
[ObservableProperty]
private LogMode _logMode = LogMode.Both;
}
}

View File

@@ -0,0 +1,115 @@
using System;
using LPR_Manager.Code.GlobalObject;
using LPR_Manager.Model;
using LPR_Manager.Model.Device.SubClass;
namespace LPR_Manager.Protocol.CMS
{
class InnoV1
{
internal static string GetStatusChkDetailStr(string status, string devicecode)
{
// #1|CMSDTST|1-1,0-0,1-0|||^
return $"#{devicecode}|CMSDTST|{status}|||^";
}
internal static string GetStatusChkStr(string devicecode)
{
// #1|LPRST|OK|||^
return $"#{devicecode}|CMSST|OK|||^";
}
internal static string GetResultStr(LPRDataSet data, string fpath)
{
if (!string.IsNullOrEmpty(data.ImagePath) && data.ImagePath.Length >= fpath.Length && data.ImagePath.Substring(0, fpath.Length) != fpath)
{
data.ImagePath = $"{fpath}{data.ImagePath}";
}
return $"#{BaseInfo.DeviceCode}|RESULT|{data.GetProtocolString()}|||^";
}
internal static string GetResultUpdateStr(LPRDataSet data, string fpath)
{
if (data.ImagePath.Length >= fpath.Length && data.ImagePath.Substring(0, fpath.Length) != fpath)
{
data.ImagePath = $"{fpath}{data.ImagePath}";
}
return $"#{BaseInfo.DeviceCode}|RESULTUP|{data.GetProtocolString()}|||^";
}
internal static string GetRawResultStr(LPRDataSet data)
{
return data.GetRawProtocolString();
}
internal static CMSDataSet Parse(string strdata)
{
CMSDataSet tmpResultDataset = new CMSDataSet();
// #C1|CMSCMD|REGREFRESH|||^
strdata = strdata.Substring(1);
string[] spdata = strdata.Split('|');
if (spdata[1] == "CMSCMD" && spdata.Length > 4)
{
tmpResultDataset.CenterID = spdata[0];
if (Enum.TryParse(spdata[2], out CMSCMD tmpParseredcmd))
{
tmpResultDataset.MainCmd = tmpParseredcmd;
switch (tmpResultDataset.MainCmd)
{
case CMSCMD.CONFIRM:
tmpResultDataset.Data1 = spdata[3];
break;
case CMSCMD.GATECTRL:
tmpResultDataset.EnumData = (GateSendCMD)Enum.Parse(typeof(GateSendCMD), spdata[3]);
break;
case CMSCMD.LEDDISPLAY:
tmpResultDataset.Data1 = spdata[3];
if (spdata[4].Contains("일반"))
{
tmpResultDataset.Data2 = MainDeviceItem.LEDDisplayMessageNoMember;
}
else if (spdata[4].Contains("등록"))
{
tmpResultDataset.Data2 = MainDeviceItem.LEDDisplayMessageMember;
}
break;
case CMSCMD.LEDDISPLAYC:
tmpResultDataset.MainCmd = CMSCMD.LEDDISPLAY;
tmpResultDataset.Data1 = spdata[3];
tmpResultDataset.Data2 = spdata[4];
tmpResultDataset.Data3 = spdata[5];
break;
}
}
else
{
tmpResultDataset.MainCmd = CMSCMD.OUTOFCMD;
}
}
else
{
tmpResultDataset.MainCmd = CMSCMD.OUTOFCMD;
}
return tmpResultDataset;
}
internal static string GetSendToServerCMD(CMSCMD cmd, Enum subcmd, string data1 = "", string data2 = "")
{
string result = "";
switch (cmd)
{
case CMSCMD.CONFIRM:
result = $"#{BaseInfo.DeviceCode}|CMSCMD|CONFIRM|{data1}||^";
break;
case CMSCMD.GATECTRL:
result = $"#{BaseInfo.DeviceCode}|CMSCMD|GATECTRL|{((GateSendCMD)subcmd)}||^";
break;
case CMSCMD.LEDDISPLAY:
result = $"#{BaseInfo.DeviceCode}|CMSCMD|LEDDISPLAY|{data1}|{data2}|^";
break;
}
return result;
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using LPR_Manager.Model;
namespace LPR_Manager.Protocol.Gate
{
class FPTV1
{
internal static string NewLine = "\u0003";
internal static GateStatus GetGateStatus(string strdata)
{
GateStatus result = GateStatus.CLOSED;
strdata = strdata.Replace($"{(char)0x02}", "").Replace($"{(char)0x03}", "");
if (strdata.Contains(","))
{
string[] spdata = strdata.Split(',');
strdata = spdata[0].Replace("GATE=", "");
}
if (strdata.Contains("GATE UP ACTION")) result = GateStatus.OPENING;
else if (strdata.Contains("GATE DOWN ACTION")) result = GateStatus.CLOSING;
else if (strdata.Contains("GATE DOWN OK")) result = GateStatus.CLOSED;
else if (strdata.Contains("GATE UP OK")) result = GateStatus.OPENDED;
else if (strdata.Contains("ERROR")) result = GateStatus.ERROR;
else if (strdata.Contains("UPLOCK")) result = GateStatus.UPLOCK;
else if (strdata.Contains("INIT")) result = GateStatus.INITED;
return result;
}
internal static GateStatus ParseGateStatusIndirectMode(string strdata)
{
GateStatus result = GateStatus.ERROR;
string[] spstr = strdata.Split('|');
if (spstr.Length >= 5)
{
if (spstr[1] == "CMSDTST")
{
string[] tmpspstrgate = spstr[2].Split(',');
if (tmpspstrgate.Length >= 1)
{
string[] spstrgatestatus = tmpspstrgate[0].Split('-');
Enum.TryParse(spstrgatestatus[1], out result);
}
}
}
return result;
}
internal static string GetCommand(GateSendCMD cmd)
{
string result = string.Empty;
switch (cmd)
{
case GateSendCMD.OPEN: result = $"{(char)0x02}GATE UP{(char)0x03}"; break;
case GateSendCMD.CLOSE: result = $"{(char)0x02}GATE DOWN{(char)0x03}"; break;
case GateSendCMD.STATUS: result = $"{(char)0x02}STATUS{(char)0x03}"; break;
case GateSendCMD.UPLOCK: result = $"{(char)0x02}GATE UPLOCK{(char)0x03}"; break;
case GateSendCMD.UNLOCK: result = $"{(char)0x02}GATE UNLOCK{(char)0x03}"; break;
case GateSendCMD.RESET: result = $"{(char)0x02}SYSTEM RESET{(char)0x03}"; break;
}
return result;
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LPR_Manager.Model;
using LPR_Manager.Model.Device.SubClass;
namespace LPR_Manager.Protocol.LED
{
public static class UJIN
{
public static byte[] GetMatrixMessage(LEDTextSet topMessage, LEDTextSet bottomMessage)
{
// Windows-949 encoding (Korean)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var encoding = Encoding.GetEncoding(949);
var color1 = CreateColorBytes(topMessage.Message, topMessage.Color, encoding);
var color2 = CreateColorBytes(bottomMessage.Message, bottomMessage.Color, encoding);
var alignmentString1 = GetAlignmentString(encoding, topMessage.Message);
var alignmentString2 = GetAlignmentString(encoding, bottomMessage.Message);
var char1 = encoding.GetBytes(alignmentString1);
var char2 = encoding.GetBytes(alignmentString2);
var dataLength = color1.Length + color2.Length + char1.Length + char2.Length + 16 + 1;
var lengthPacket = ConvertToBigEndian((short)dataLength);
var packets = new List<byte>();
packets.AddRange(new byte[] { 0x10, 0x02, 0x01 });
packets.AddRange(lengthPacket);
packets.AddRange(new byte[]
{
0x94, 0x00, 0x00, 0x63, 0x01, 0x00,
0x03, 0x01, 0x01, 0x00, 0x00, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00
});
packets.AddRange(color1);
packets.AddRange(color2);
packets.AddRange(char1);
packets.AddRange(char2);
packets.AddRange(new byte[] { 0x10, 0x03 });
return packets.ToArray();
}
private static byte[] CreateColorBytes(string text, LEDColor color, Encoding encoding)
{
var bytes = new List<byte>();
var alignmentString = GetAlignmentString(encoding, text);
var colorByte = ParseColorToColorMessage(color);
foreach (var ch in alignmentString)
{
bytes.Add(colorByte);
if (encoding.GetByteCount(ch.ToString()) != 1)
bytes.Add(0x00);
}
return bytes.ToArray();
}
private static string GetAlignmentString(Encoding encoding, string message, int maxlength = 12)
{
var checkBytes = encoding.GetBytes(message);
if (checkBytes.Length > maxlength)
{
Array.Resize(ref checkBytes, maxlength);
}
byte[] resultstr = Enumerable.Repeat((byte)0x20, maxlength).ToArray();
Array.Copy(checkBytes, 0, resultstr, 0, checkBytes.Length);
return encoding.GetString(resultstr).TrimEnd((char)0x00);
}
private static byte[] ConvertToBigEndian(short value)
{
var bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return bytes;
}
private static byte ParseColorToColorMessage(LEDColor color)
{
return color switch
{
LEDColor.RED => 0x01,
LEDColor.GREEN => 0x02,
LEDColor.YELLOW => 0x03,
LEDColor.BLUE => 0x04,
LEDColor.MAGENTA => 0x05,
LEDColor.CYAN => 0x06,
LEDColor.WHITE => 0x07,
_ => 0x00
};
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using LPR_Manager.Model;
using LPR_Manager.Model.Device.SubClass;
namespace LPR_Manager.Protocol.LED
{
class YJMICRO
{
internal static string NewLine = "[COMOFF]";
internal static string Space = " ";
internal static string GetCommand(LEDTextSet textsettop, LEDTextSet textsetbottom)
{
return $"[TMODE]\r[COMON][PX0 PY0][RTX0 RTY0][CR{(int)textsettop.Color}]{textsettop.Message}[ACT0][PX0 PY0][RTX0 RTY16][CR{(int)textsetbottom.Color}]{textsetbottom.Message}[ACT0][COMOFF]";
}
}
}

View File

@@ -0,0 +1,78 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace LPR_Manager.Service
{
public class ConfigService
{
private const string ConfigDir = "Configs";
public ConfigService()
{
if (!Directory.Exists(ConfigDir))
{
Directory.CreateDirectory(ConfigDir);
}
}
public async Task SaveLaneAsync(Model.LaneConfig lane)
{
var fileName = Path.Combine(ConfigDir, $"{lane.LaneId}.json");
var json = JsonConvert.SerializeObject(lane, Formatting.Indented);
await File.WriteAllTextAsync(fileName, json);
}
public void DeleteLaneConfig(string laneId)
{
var fileName = Path.Combine(ConfigDir, $"{laneId}.json");
if (File.Exists(fileName))
{
File.Delete(fileName);
}
}
public async Task<List<Model.LaneConfig>> LoadAllLanesAsync()
{
var lanes = new List<Model.LaneConfig>();
if (!Directory.Exists(ConfigDir)) return lanes;
foreach (var file in Directory.GetFiles(ConfigDir, "*.json"))
{
if (Path.GetFileName(file).Equals("system_config.json", System.StringComparison.OrdinalIgnoreCase)) continue;
var json = await File.ReadAllTextAsync(file);
var lane = JsonConvert.DeserializeObject<Model.LaneConfig>(json);
if (lane != null) lanes.Add(lane);
}
return lanes;
}
public async Task<Model.SystemConfig> LoadSystemConfigAsync()
{
var path = Path.Combine(ConfigDir, "system_config.json");
if (!File.Exists(path))
{
return new Model.SystemConfig();
}
try
{
var json = await File.ReadAllTextAsync(path);
return JsonConvert.DeserializeObject<Model.SystemConfig>(json) ?? new Model.SystemConfig();
}
catch
{
return new Model.SystemConfig();
}
}
public async Task SaveSystemConfigAsync(Model.SystemConfig config)
{
var path = Path.Combine(ConfigDir, "system_config.json");
var json = JsonConvert.SerializeObject(config, Formatting.Indented);
await File.WriteAllTextAsync(path, json);
}
}
}

View File

@@ -0,0 +1,141 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace LPR_Manager.Service
{
public class ImageServer
{
private WebApplication? _app;
private readonly string _webRootPath;
private readonly int _port;
private readonly string _username;
private readonly string _password;
public bool IsRunning => _app != null;
public ImageServer(Model.SystemConfig config)
{
if (Path.IsPathRooted(config.ImageRootPath))
{
_webRootPath = config.ImageRootPath;
}
else
{
_webRootPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, config.ImageRootPath);
}
_port = config.ImageServerPort;
_username = config.ImageServerId;
_password = config.ImageServerPassword;
Directory.CreateDirectory(_webRootPath);
}
public async Task StartAsync(CancellationToken cancellationToken = default)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(_port);
});
builder.Logging.ClearProviders();
builder.Logging.AddSerilog();
_app = builder.Build();
// Basic Auth Middleware
_app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/images"))
{
string? authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic "))
{
var encodedUsernamePassword = authHeader.Substring(6).Trim();
var encoding = Encoding.UTF8;
try
{
string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));
int separatorIndex = usernamePassword.IndexOf(':');
if (separatorIndex >= 0)
{
var username = usernamePassword.Substring(0, separatorIndex);
var password = usernamePassword.Substring(separatorIndex + 1);
if (username == _username && password == _password)
{
await next();
return;
}
}
}
catch
{
// Ignore parsing errors
}
}
context.Response.StatusCode = 401;
context.Response.Headers["WWW-Authenticate"] = "Basic realm=\"ParkingImages\"";
await context.Response.WriteAsync("Unauthorized");
return;
}
await next();
});
_app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(_webRootPath),
RequestPath = "/images"
});
_app.MapGet("/", () => Results.Content(@"
<html>
<head>
<style>
body { font-family: 'Segoe UI', sans-serif; text-align: center; padding: 50px; background-color: #f0f2f5; }
.container { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); display: inline-block; }
h1 { color: #0078d4; margin-bottom: 10px; }
p { color: #333; font-size: 1.1em; }
.status { color: green; font-weight: bold; }
.footer { margin-top: 20px; font-size: 0.8em; color: #666; }
</style>
</head>
<body>
<div class='container'>
<h1>Parking Interface Image Server</h1>
<p>Status: <span class='status'>Active</span></p>
<p>The image server is running and ready to serve files.</p>
<hr/>
<p style='font-size: 0.9em'>Authentication Required for /images path.</p>
<div class='footer'>ParkingInterface System v1.0</div>
</div>
</body>
</html>", "text/html"));
await _app.StartAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken = default)
{
if (_app != null)
{
await _app.StopAsync(cancellationToken);
await _app.DisposeAsync();
_app = null;
}
}
}
}

View File

@@ -0,0 +1,30 @@
using Serilog;
using System.IO;
using LPR_Manager.Model;
namespace LPR_Manager.Service
{
public static class LogService
{
public static void ConfigureLogging(LogMode mode)
{
var loggerConfig = new LoggerConfiguration()
.MinimumLevel.Debug();
var logPath = Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Logs", "log.txt");
if (mode == LogMode.Seq || mode == LogMode.Both)
{
loggerConfig.WriteTo.Seq("http://localhost:5341");
}
if (mode == LogMode.File || mode == LogMode.Both)
{
loggerConfig.WriteTo.File(logPath, rollingInterval: RollingInterval.Day, retainedFileCountLimit: 30);
}
Log.Logger = loggerConfig.CreateLogger();
Log.Information($"Logging configured. Mode: {mode}");
}
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Runtime.InteropServices;
using System.IO;
namespace LPR_Manager.Service
{
public class WushiCamera : IDisposable
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void NtcConnectCallback([MarshalAs(UnmanagedType.LPUTF8Str)] string id, bool connected);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void NtcTriggerCallback([MarshalAs(UnmanagedType.LPUTF8Str)] string id, [MarshalAs(UnmanagedType.LPUTF8Str)] string plate, IntPtr pImage, int imageSize);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void NtcAuthCallback(int code, [MarshalAs(UnmanagedType.LPUTF8Str)] string msg);
[DllImport("NtcClientAPI4Net.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void NtcApi_Init([MarshalAs(UnmanagedType.LPStr)] string logPath);
[DllImport("NtcClientAPI4Net.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr NtcApi_CreateClient(
[MarshalAs(UnmanagedType.LPStr)] string host,
[MarshalAs(UnmanagedType.LPUTF8Str)] string id,
[MarshalAs(UnmanagedType.LPStr)] string adminId,
[MarshalAs(UnmanagedType.LPStr)] string adminPw,
NtcConnectCallback ccb, NtcTriggerCallback tcb, NtcAuthCallback acb);
[DllImport("NtcClientAPI4Net.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void NtcApi_DeleteClient(IntPtr pClient);
[DllImport("NtcClientAPI4Net.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int NtcApi_ClientStart(IntPtr pClient);
[DllImport("NtcClientAPI4Net.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int NtcApi_ClientStop(IntPtr pClient);
private IntPtr _pClient = IntPtr.Zero;
private NtcConnectCallback _connectCallback;
private NtcTriggerCallback _triggerCallback;
private NtcAuthCallback _authCallback;
public bool IsConnected { get; private set; }
public event EventHandler<byte[]> OnImageReceived;
public event EventHandler<string> OnMessage;
public WushiCamera()
{
string logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs", "NtcSDK");
if (!Directory.Exists(logPath)) Directory.CreateDirectory(logPath);
NtcApi_Init(logPath);
_connectCallback = (id, connected) =>
{
IsConnected = connected;
OnMessage?.Invoke(this, $"Camera {id} Connected: {connected}");
};
_authCallback = (code, msg) => OnMessage?.Invoke(this, $"Camera Auth: {code}, {msg}");
_triggerCallback = (id, plate, pImage, imageSize) =>
{
byte[] jpgImage = new byte[imageSize];
Marshal.Copy(pImage, jpgImage, 0, imageSize);
OnImageReceived?.Invoke(this, jpgImage);
};
}
public void Start(string ip, string id, string user, string pw)
{
_pClient = NtcApi_CreateClient(ip, id, user, pw, _connectCallback, _triggerCallback, _authCallback);
if (_pClient != IntPtr.Zero)
{
NtcApi_ClientStart(_pClient);
}
}
public void Stop()
{
if (_pClient != IntPtr.Zero)
{
NtcApi_ClientStop(_pClient);
NtcApi_DeleteClient(_pClient);
_pClient = IntPtr.Zero;
}
}
public void Dispose()
{
Stop();
}
}
}

View File

@@ -0,0 +1,109 @@
<Window x:Class="LPR_Manager.View.LoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Title="Sign In" Height="650" Width="400"
WindowStartupLocation="CenterOwner" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
ResizeMode="NoResize" FontFamily="Segoe UI">
<Grid Margin="20">
<!-- Main Card -->
<Border CornerRadius="25" Background="White">
<Border.Effect>
<DropShadowEffect BlurRadius="40" ShadowDepth="0" Opacity="0.2" Color="#9E9E9E"/>
</Border.Effect>
<Grid Margin="30">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- Top Bar -->
<RowDefinition Height="Auto"/> <!-- Avatar -->
<RowDefinition Height="Auto"/> <!-- Header -->
<RowDefinition Height="*"/> <!-- Inputs -->
<RowDefinition Height="Auto"/> <!-- Button -->
</Grid.RowDefinitions>
<!-- 1. Top Bar -->
<Grid Grid.Row="0">
<!-- 'P' Logo -->
<Border HorizontalAlignment="Left" Width="32" Height="32" CornerRadius="16" Background="#E3F2FD">
<TextBlock Text="P" Foreground="#1565C0" FontWeight="Bold" FontSize="18"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- Close Button -->
<Button HorizontalAlignment="Right" Width="30" Height="30" Padding="0"
Background="Transparent" BorderBrush="Transparent"
Click="OnCloseClick" Cursor="Hand">
<materialDesign:PackIcon Kind="Close" Width="20" Height="20" Foreground="#757575"/>
</Button>
</Grid>
<!-- 2. Avatar -->
<Border Grid.Row="1" Width="80" Height="80" CornerRadius="40" Background="#F5F6FA" Margin="0,30,0,20">
<materialDesign:PackIcon Kind="AccountCircle" Width="50" Height="50" Foreground="#1565C0"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<!-- 3. Header -->
<StackPanel Grid.Row="2" HorizontalAlignment="Center" Margin="0,0,0,30">
<TextBlock Text="Sign In" FontSize="26" FontWeight="Bold" Foreground="Black" HorizontalAlignment="Center" Margin="0,0,0,10"/>
<TextBlock Text="Enter your credentials to access the parking" Foreground="#78909C" FontSize="13" HorizontalAlignment="Center"/>
<TextBlock Text="management portal" Foreground="#78909C" FontSize="13" HorizontalAlignment="Center"/>
</StackPanel>
<!-- 4. Inputs -->
<StackPanel Grid.Row="3" VerticalAlignment="Top">
<!-- Username -->
<TextBlock Text="Username" FontWeight="SemiBold" Foreground="#37474F" Margin="5,0,0,5"/>
<Border BorderThickness="1" BorderBrush="#E0E0E0" CornerRadius="15" Padding="5" Margin="0,0,0,20">
<TextBox x:Name="UserBox"
materialDesign:HintAssist.Hint="admin_parking"
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
BorderThickness="0" Background="Transparent"
VerticalContentAlignment="Center" Height="35" Padding="10,0" FontSize="14"/>
</Border>
<!-- Password -->
<TextBlock Text="Password" FontWeight="SemiBold" Foreground="#37474F" Margin="5,0,0,5"/>
<Border BorderThickness="1" BorderBrush="#E0E0E0" CornerRadius="15" Padding="5">
<Grid>
<PasswordBox x:Name="PassBox"
materialDesign:HintAssist.Hint="••••••••"
materialDesign:TextFieldAssist.DecorationVisibility="Hidden"
BorderThickness="0" Background="Transparent"
VerticalContentAlignment="Center" Height="35" Padding="10,0" FontSize="14"
FontFamily="Courier New"/>
<materialDesign:PackIcon Kind="Eye" HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,10,0" Foreground="#90A4AE" Cursor="Hand"/>
</Grid>
</Border>
<!-- Error Message -->
<TextBlock x:Name="ErrorText" Text="Invalid credentials" Foreground="#D32F2F" FontSize="12"
HorizontalAlignment="Center" Margin="0,15,0,0" Visibility="Collapsed"/>
</StackPanel>
<!-- 5. Login Button -->
<Button Grid.Row="4" IsDefault="True" Click="OnLogin"
Background="#1565C0" BorderBrush="#1565C0" Foreground="White"
Height="50" materialDesign:ButtonAssist.CornerRadius="25"
Margin="0,20,0,0" FontSize="16" FontWeight="Bold">
<StackPanel Orientation="Horizontal">
<TextBlock Text="LOGIN" Margin="0,0,10,0"/>
<materialDesign:PackIcon Kind="ArrowRight"/>
</StackPanel>
</Button>
<!-- Close Button (Hidden/Cancel functionality via 'Esc' or top right logic) -->
<!-- Since windowstyle is none, we should probably have a way to close if user cancels,
but the request removed the cancel button. We'll map ESC key in code behind or rely on user knowing standard behaviors.
Ideally, let's add a small 'Close' or 'Cancel' text logic safely below or use the X top right if needed.
Mockup shows three dots. I'll make the three dots clickable to close for now or just trust IsCancel hidden button. -->
<Button IsCancel="True" Click="OnCancel" Visibility="Collapsed"/>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,46 @@
using System.Windows;
using System.Windows.Controls;
namespace LPR_Manager.View
{
public partial class LoginWindow : Window
{
public bool IsAuthenticated { get; private set; } = false;
public LoginWindow()
{
InitializeComponent();
UserBox.Focus();
}
private void OnLogin(object sender, RoutedEventArgs e)
{
// Fixed credentials
var user = UserBox.Text;
var pass = PassBox.Password;
if (user == "admin" && pass == "090706")
{
IsAuthenticated = true;
DialogResult = true;
Close();
}
else
{
ErrorText.Visibility = Visibility.Visible;
PassBox.Password = "";
PassBox.Focus();
}
}
private void OnCancel(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void OnCloseClick(object sender, RoutedEventArgs e)
{
Close();
}
}
}

View File

@@ -0,0 +1,224 @@
<UserControl x:Class="LPR_Manager.View.LprLaneControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:LPR_Manager.View"
mc:Ignorable="d"
Width="360" Height="680" Margin="10">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<!-- Storyboard for vehicle detection glow animation -->
<Storyboard x:Key="VehicleDetectedGlow" RepeatBehavior="5x" Duration="0:0:5">
<ColorAnimation Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
From="#3F51B5" To="Transparent" Duration="0:0:1"/>
</Storyboard>
</UserControl.Resources>
<!-- Main Card -->
<md:Card UniformCornerRadius="12" md:ElevationAssist.Elevation="Dp2" Background="White">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<!-- Header -->
<RowDefinition Height="200"/>
<!-- Main Image with Overlay -->
<RowDefinition Height="110"/>
<!-- Recognition Info -->
<RowDefinition Height="Auto"/>
<!-- Status Pills -->
<RowDefinition Height="*"/>
<!-- Logs -->
</Grid.RowDefinitions>
<!-- 1. Header Area with Lane ID -->
<Border Grid.Row="0" Padding="16,12" Background="White" BorderBrush="#EEEEEE" BorderThickness="0,0,0,0">
<DockPanel>
<TextBlock Text="{Binding LaneId, TargetNullValue='#P-001'}" DockPanel.Dock="Right"
FontSize="11" Foreground="#BDBDBD" FontWeight="Bold" VerticalAlignment="Top" Margin="0,2,0,0" FontFamily="Courier New"/>
<StackPanel>
<DockPanel>
<TextBlock Text="{Binding LaneName}" FontWeight="Bold" Foreground="{DynamicResource PrimaryHueMidBrush}" FontSize="11"/>
</DockPanel>
<TextBlock Text="{Binding LaneDescription}" FontSize="16" FontWeight="Bold" Foreground="#263238" Margin="0,2,0,0"/>
</StackPanel>
</DockPanel>
</Border>
<!-- 2. Main Image Area -->
<Border Grid.Row="1" BorderThickness="2" BorderBrush="Transparent" CornerRadius="4">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsVehicleDetected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource VehicleDetectedGlow}"/>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<!-- Vehicle Image -->
<Image Source="{Binding CameraImage}" Stretch="UniformToFill" VerticalAlignment="Center"/>
<!-- Placeholder if no image -->
<Grid Visibility="{Binding ShowPlaceholder, Converter={StaticResource BooleanToVisibilityConverter}}" Background="#F5F5F5">
<md:PackIcon Kind="Car" Width="50" Height="50" Foreground="#E0E0E0" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<!-- Live Badge -->
</Grid>
</Border>
<!-- 3. Recognition Info -->
<Grid Grid.Row="2" Margin="16,16,16,8">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<DockPanel LastChildFill="False">
<TextBlock Text="RECOGNITION RESULT" Foreground="#90A4AE" FontSize="10" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding LastDetectionTime, StringFormat=HH:mm:ss, TargetNullValue='--:--:--'}" Foreground="#90A4AE" FontSize="10" DockPanel.Dock="Right" VerticalAlignment="Center"/>
</DockPanel>
<TextBlock Text="{Binding LastCarNumber}" FontSize="32" FontWeight="Black" Foreground="#263238" Margin="0,4,0,2"/>
<TextBlock Text="AUTHORIZED MATCH" Foreground="#388E3C" FontSize="11" FontWeight="Bold"
Visibility="{Binding IsAuthorized, Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock.Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource {x:Type TextBlock}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsAuthorized}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Plate Overlay (Picture-in-Picture) -->
<Border Margin="2" CornerRadius="4" BorderBrush="White" BorderThickness="1" Background="White">
<Grid>
<Image Source="{Binding PlateImage}" Stretch="Uniform"/>
<TextBlock Text="No Plate" Foreground="Gray" FontSize="10" HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="{Binding ShowNoPlate, Converter={StaticResource BooleanToVisibilityConverter}}"/>
</Grid>
<Border.Effect>
<DropShadowEffect BlurRadius="8" ShadowDepth="2" Opacity="0.4"/>
</Border.Effect>
</Border>
</Grid>
</Grid>
<!-- 4. Device Status Pills -->
<StackPanel Grid.Row="3" Orientation="Horizontal" Margin="16,12,16,8">
<StackPanel.Resources>
<Style TargetType="Border" x:Key="StatusPill">
<Setter Property="Background" Value="White"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="Margin" Value="0,0,8,0"/>
<Setter Property="BorderBrush" Value="#EEEEEE"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="4" ShadowDepth="1" Direction="270" Opacity="0.1" Color="#000000"/>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Ellipse" x:Key="StatusDot">
<Setter Property="Width" Value="8"/>
<Setter Property="Height" Value="8"/>
<Setter Property="Margin" Value="0,0,6,0"/>
</Style>
</StackPanel.Resources>
<!-- CAM -->
<Border Style="{StaticResource StatusPill}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse>
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCameraConnected}" Value="True">
<Setter Property="Fill" Value="#2E7D32"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsCameraConnected}" Value="False">
<Setter Property="Fill" Value="#C62828"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="CAM" FontSize="11" FontWeight="Bold" Foreground="#37474F"/>
</StackPanel>
</Border>
<!-- CMS -->
<Border Style="{StaticResource StatusPill}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Ellipse>
<Ellipse.Style>
<Style TargetType="Ellipse" BasedOn="{StaticResource StatusDot}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCmsConnected}" Value="True">
<Setter Property="Fill" Value="#2E7D32"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsCmsConnected}" Value="False">
<Setter Property="Fill" Value="#C62828"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<TextBlock Text="CMS" FontSize="11" FontWeight="Bold" Foreground="#37474F"/>
</StackPanel>
</Border>
</StackPanel>
<!-- 5. Logs Grid -->
<DataGrid Grid.Row="4" Background="White" BorderThickness="0,1,0,0" BorderBrush="#EEEEEE"
ItemsSource="{Binding Logs}"
AutoGenerateColumns="False" HeadersVisibility="None" IsReadOnly="True"
GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#F5F5F5"
Style="{DynamicResource MaterialDesignDataGrid}" Margin="16,8,16,16"
RowHeight="28">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding .}" Width="*" Foreground="#546E7A">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="TextWrapping" Value="NoWrap"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</md:Card>
</UserControl>

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace LPR_Manager.View
{
public partial class LprLaneControl : UserControl
{
public LprLaneControl()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,257 @@
<UserControl x:Class="LPR_Manager.View.SettingsControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:model="clr-namespace:LPR_Manager.Model"
mc:Ignorable="d"
Background="{DynamicResource MaterialDesignPaper}"
d:DesignHeight="600" d:DesignWidth="1000">
<UserControl.Resources>
<ObjectDataProvider x:Key="RecognitionOrderEnum" MethodName="GetValues" ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="model:RecognitionOrder"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<ObjectDataProvider x:Key="LogModeEnum" MethodName="GetValues" ObjectType="{x:Type sys:Enum}">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="model:LogMode"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</UserControl.Resources>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="SETTINGS" Style="{DynamicResource MaterialDesignHeadline5TextBlock}" Margin="0,0,0,16"/>
<TabControl Grid.Row="1" Style="{DynamicResource MaterialDesignNaviRailTabControl}" TabStripPlacement="Top">
<!-- System Setting Tab -->
<TabItem Header="System Setting" Style="{DynamicResource MaterialDesignNavigationRailTabItem}">
<Grid Margin="20">
<StackPanel MaxWidth="500" HorizontalAlignment="Left" VerticalAlignment="Top">
<TextBlock Text="Image Server Configuration" Style="{StaticResource MaterialDesignHeadline6TextBlock}" Margin="0,0,0,20"/>
<TextBox materialDesign:HintAssist.Hint="Server Port (Default: 5000)"
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
Text="{Binding SystemConfig.ImageServerPort}"
Margin="0,0,0,20"/>
<TextBox materialDesign:HintAssist.Hint="Server ID"
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
Text="{Binding SystemConfig.ImageServerId}"
Margin="0,0,0,20"/>
<TextBox materialDesign:HintAssist.Hint="Server Password"
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
Text="{Binding SystemConfig.ImageServerPassword}"
Margin="0,0,0,20"/>
<DockPanel Margin="0,0,0,30">
<Button Command="{Binding BrowseImageFolderCommand}"
Content="..."
ToolTip="Browse Folder"
Style="{DynamicResource MaterialDesignOutlinedButton}"
Width="40" Padding="0" Margin="10,0,0,0" DockPanel.Dock="Right"/>
<TextBox materialDesign:HintAssist.Hint="Image Root Path (Or Absolute Path)"
Style="{StaticResource MaterialDesignFloatingHintTextBox}"
Text="{Binding SystemConfig.ImageRootPath}"/>
</DockPanel>
<TextBlock Text="Log Configuration" Style="{StaticResource MaterialDesignHeadline6TextBlock}" Margin="0,0,0,20"/>
<ComboBox ItemsSource="{Binding Source={StaticResource LogModeEnum}}"
SelectedItem="{Binding SystemConfig.LogMode}"
materialDesign:HintAssist.Hint="Log Storage Mode"
Style="{StaticResource MaterialDesignFloatingHintComboBox}"
Margin="0,0,0,30"/>
<Button Command="{Binding SaveSystemConfigCommand}"
Content="Save System Settings"
Style="{StaticResource MaterialDesignRaisedButton}"
HorizontalAlignment="Left" Width="200"/>
<TextBlock Text="* Restart required to apply changes"
Foreground="Gray" Margin="0,10,0,0" FontStyle="Italic"/>
</StackPanel>
</Grid>
</TabItem>
<!-- SITE SETTING TAB -->
<TabItem Header="SITE SETTING">
<Grid Margin="0,16,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- LEFT: SITE LIST -->
<materialDesign:Card Grid.Column="0" VerticalAlignment="Stretch" Margin="0,0,16,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel Margin="8">
<TextBlock Text="Site List" FontWeight="Bold" VerticalAlignment="Center" Margin="8,0"/>
<Button Command="{Binding AddLaneCommand}" Style="{DynamicResource MaterialDesignFlatButton}"
HorizontalAlignment="Right" ToolTip="Add New Site">
<materialDesign:PackIcon Kind="Plus"/>
</Button>
</DockPanel>
<DataGrid Grid.Row="1" ItemsSource="{Binding Lanes}" SelectedItem="{Binding SelectedLane}"
AutoGenerateColumns="False" CanUserAddRows="False" IsReadOnly="True"
HeadersVisibility="Column" SelectionMode="Single"
Style="{DynamicResource MaterialDesignDataGrid}">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding LaneId}" Width="60"/>
<DataGridTextColumn Header="NAME" Binding="{Binding LaneName}" Width="*"/>
<DataGridTemplateColumn Width="50">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.RemoveLaneCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Style="{DynamicResource MaterialDesignFlatButton}"
Foreground="#EF5350" Padding="0" Width="24" Height="24">
<materialDesign:PackIcon Kind="Delete" Width="16" Height="16"/>
</Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</materialDesign:Card>
<!-- RIGHT: DETAILS -->
<materialDesign:Card Grid.Column="1" VerticalAlignment="Stretch" Padding="24">
<Grid>
<Grid>
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedLane}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Header -->
<DockPanel Margin="0,0,0,24">
<StackPanel>
<TextBlock Text="{Binding SelectedLane.LaneName}" Style="{DynamicResource MaterialDesignHeadline6TextBlock}"/>
<TextBlock Text="{Binding SelectedLane.LaneId}" FontSize="12" Foreground="Gray"/>
</StackPanel>
</DockPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Basic Info -->
<TextBlock Text="Basic Information" FontWeight="Bold" Margin="0,0,0,8" Foreground="{DynamicResource PrimaryHueMidBrush}"/>
<TextBox Text="{Binding SelectedLane.LaneName}" materialDesign:HintAssist.Hint="Site Name" Style="{DynamicResource MaterialDesignFloatingHintTextBox}" Margin="0,0,0,16"/>
<TextBox Text="{Binding SelectedLane.LaneDescription}" materialDesign:HintAssist.Hint="Description" Style="{DynamicResource MaterialDesignFloatingHintTextBox}" Margin="0,0,0,24"/>
<!-- Camera Settings -->
<TextBlock Text="Camera Configuration" FontWeight="Bold" Margin="0,0,0,8" Foreground="{DynamicResource PrimaryHueMidBrush}"/>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding SelectedLane.CameraIpString}" materialDesign:HintAssist.Hint="IP Address" Style="{DynamicResource MaterialDesignFloatingHintTextBox}" Margin="0,0,8,0"/>
<TextBox Grid.Column="1" Text="{Binding SelectedLane.CameraPort}" materialDesign:HintAssist.Hint="Port" Style="{DynamicResource MaterialDesignFloatingHintTextBox}" Margin="8,0,0,0"/>
</Grid>
<Grid Margin="0,0,0,24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding SelectedLane.CameraId}" materialDesign:HintAssist.Hint="User ID" Style="{DynamicResource MaterialDesignFloatingHintTextBox}" Margin="0,0,8,0"/>
<TextBox Grid.Column="1" Text="{Binding SelectedLane.CameraPassword}" materialDesign:HintAssist.Hint="Password" Style="{DynamicResource MaterialDesignFloatingHintTextBox}" Margin="8,0,0,0"/>
</Grid>
<!-- Recognition Settings -->
<TextBlock Text="Recognition Settings" FontWeight="Bold" Margin="0,0,0,8" Foreground="{DynamicResource PrimaryHueMidBrush}"/>
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ComboBox Grid.Column="0"
ItemsSource="{Binding Source={StaticResource RecognitionOrderEnum}}"
SelectedItem="{Binding SelectedLane.RecognitionOrder}"
materialDesign:HintAssist.Hint="Recognition Order"
Style="{DynamicResource MaterialDesignFloatingHintComboBox}" Margin="0,0,8,0"/>
<!-- No Plate Code -->
<StackPanel Grid.Column="1" Margin="8,0,0,0" VerticalAlignment="Bottom">
<TextBlock Text="No Plate Result" FontSize="10" Foreground="Gray" Margin="0,0,0,4"/>
<ComboBox Text="{Binding SelectedLane.NoPlateCode}" IsEditable="True" materialDesign:HintAssist.Hint="Select or Type Code" Style="{DynamicResource MaterialDesignFloatingHintComboBox}">
<ComboBoxItem Content="XXXXXXXX"/>
<ComboBoxItem Content="????????"/>
</ComboBox>
</StackPanel>
</Grid>
<!-- ROI / Single Shot -->
<TextBlock Text="Recognition Area (ROI)" FontWeight="Bold" Margin="0,0,0,8" Foreground="{DynamicResource PrimaryHueMidBrush}"/>
<DockPanel LastChildFill="False">
<Button Content="SINGLE SHOT" Command="{Binding SelectedLane.SingleShotCommand}" Style="{DynamicResource MaterialDesignOutlinedButton}" Margin="0,0,16,0"/>
<TextBlock Text="{Binding SelectedLane.RecognitionArea}" VerticalAlignment="Center" Foreground="Gray"/>
</DockPanel>
</StackPanel>
</ScrollViewer>
<!-- Actions -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,16,0,0">
<Button Content="SAVE SETTINGS" Command="{Binding SaveLaneCommand}" Style="{DynamicResource MaterialDesignRaisedButton}"/>
</StackPanel>
</Grid>
<!-- Empty State -->
<Grid>
<Grid.Visibility>
<Binding Path="SelectedLane" Converter="{StaticResource BooleanToVisibilityConverter}" ConverterParameter="Inverse" FallbackValue="Visible"/>
</Grid.Visibility>
<!-- Note: BooleanToVisibilityConverter standard usually True=Visible. Need Inverse for empty state if SelectedLane is null (False) -> Visible.
Standard WPF doesn't have Inverse param. Using Trigger instead. -->
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedLane}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<TextBlock Text="Select a site to configure" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="#BDBDBD" FontSize="16"/>
</Grid>
</Grid>
</materialDesign:Card>
</Grid>
</TabItem>
</TabControl>
</Grid>
</UserControl>

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace LPR_Manager.View
{
public partial class SettingsControl : UserControl
{
public SettingsControl()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,50 @@
<Window x:Class="LPR_Manager.View.SplashScreen"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
Title="SplashScreen" Height="400" Width="400"
Topmost="True"
WindowStartupLocation="CenterScreen" WindowStyle="None" AllowsTransparency="True" Background="Transparent"
ResizeMode="NoResize" FontFamily="Segoe UI">
<Border CornerRadius="20" Background="#111111" BorderBrush="#333333" BorderThickness="1">
<Grid Margin="20">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<!-- Logo Animation Container -->
<Grid Width="120" Height="120" Margin="0,0,0,40">
<!-- Rotating Arc -->
<materialDesign:Card UniformCornerRadius="60" Background="Transparent" Padding="0">
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}"
Value="0" IsIndeterminate="True"
Width="100" Height="100"
Foreground="#2962FF"
BorderThickness="0"/>
</materialDesign:Card>
<!-- Center 'P' -->
<TextBlock Text="P" FontSize="48" FontWeight="Bold" Foreground="#2962FF"
HorizontalAlignment="Center" VerticalAlignment="Center"
Effect="{DynamicResource MaterialDesignShadowDepth2}"/>
</Grid>
<!-- Text Branding -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,0,0,20">
<TextBlock Text="INNO" FontSize="20" FontWeight="Bold" Foreground="White" Margin="0,0,0,0"/>
<TextBlock Text="MATRICS" FontSize="20" FontWeight="Bold" Foreground="#2962FF"/>
</StackPanel>
<!-- Loading Text -->
<TextBlock Text="LOADING..." FontSize="12" FontWeight="SemiBold" Foreground="Gray"
HorizontalAlignment="Center"/>
<!-- Small Progress Line -->
<ProgressBar IsIndeterminate="True" Height="2" Width="100" Margin="0,10,0,0"
Foreground="#2962FF" Background="#333333" BorderThickness="0"/>
</StackPanel>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,12 @@
using System.Windows;
namespace LPR_Manager.View
{
public partial class SplashScreen : Window
{
public SplashScreen()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,531 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.IO;
using System.Collections.ObjectModel;
using LPR_Manager.Model.Device.SubClass;
using LPR_Manager.Service;
using Inno.LPR;
using LPR_Manager.Model;
using LPR_Manager.Model.Device;
using Serilog;
using OpenCvSharp;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System.Globalization;
using LPR_Manager.Model;
namespace LPR_Manager.ViewModel
{
public partial class LprLaneViewModel : ObservableObject
{
private WushiCamera _camera;
private LicensePlateMotionDetector? _lprDetector;
//private CmsClient _cms;
private System.Threading.Timer? _statusTimer;
[ObservableProperty]
private string _laneName;
[ObservableProperty]
private string _laneId = "F0001"; // Placeholder or ctor param
[ObservableProperty]
private string _laneDescription = "Main Entrance"; // Default or passed in ctor
[ObservableProperty]
private string _cameraId = "admin";
[ObservableProperty]
private string _cameraPassword = "Inno!@#$5678";
[ObservableProperty]
private int _cameraPort = 80;
[ObservableProperty]
private string _cameraIpString = "127.0.0.1"; // Renamed to avoid confusion with existing logic if any, but clearer to match config
[ObservableProperty]
private RecognitionOrder _recognitionOrder = RecognitionOrder.BottomUp;
[ObservableProperty]
private string _recognitionArea = "0,0,0,0";
[ObservableProperty]
private string _noPlateCode = "XXXXXXXX";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowPlaceholder))]
private BitmapSource? _cameraImage;
public bool ShowPlaceholder => CameraImage == null;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowNoPlate))]
private BitmapSource? _plateImage;
public bool ShowNoPlate => PlateImage == null;
[ObservableProperty]
private string _lastCarNumber = "-";
[ObservableProperty]
private bool _isVehicleDetected;
[ObservableProperty]
private bool _isAuthorized;
[ObservableProperty]
private string _connectionStatus = "Init";
[ObservableProperty]
private bool _isCameraConnected;
[ObservableProperty]
private bool _isCmsConnected;
[ObservableProperty]
private string _cameraStatusText = "Disconnected";
[ObservableProperty]
private string _cmsStatusText = "Disconnected";
[ObservableProperty]
private int _totalRecognitions;
[ObservableProperty]
private int _successCount;
[ObservableProperty]
private int _failCount;
public double SuccessRate => TotalRecognitions == 0 ? 0 : (double)SuccessCount / TotalRecognitions * 100;
public event Action<string, LogSeverity>? OnLog;
public event Action<LPRDataSet>? OnCarDetected;
public event Action<LprLaneViewModel>? OnRemove;
// Config properties
private LaneConfig _config;
private readonly Serilog.ILogger _logger;
public LprLaneViewModel(LaneConfig config, LogMode logMode)
{
_config = config;
LaneName = config.LaneName;
LaneId = config.LaneId;
LaneDescription = config.LaneDescription;
CameraId = config.CameraId;
CameraPassword = config.CameraPassword;
CameraPort = config.CameraPort;
CameraIpString = config.CameraIp;
RecognitionOrder = config.RecognitionOrder;
RecognitionArea = config.RecognitionArea;
NoPlateCode = config.NoPlateCode;
// Configure Serilog
var loggerConfig = new LoggerConfiguration()
.Enrich.WithProperty("LaneId", LaneId);
if (logMode == LogMode.File || logMode == LogMode.Both)
{
loggerConfig.WriteTo.File($"Logs/{LaneId}/log-.txt", rollingInterval: RollingInterval.Day);
}
if (logMode == LogMode.Seq || logMode == LogMode.Both)
{
loggerConfig.WriteTo.Seq("http://localhost:5341");
}
_logger = loggerConfig.CreateLogger();
try
{
_lprDetector = new LicensePlateMotionDetector();
Log("Engine", "Loaded");
}
catch (Exception ex)
{
Log("Init Error", ex.Message, LogSeverity.Error);
}
_camera = new WushiCamera();
_camera.OnImageReceived += Camera_OnImageReceived;
_camera.OnMessage += (s, msg) => Log("Camera", msg);
// _cms = new CmsClient(_config.CmsIp, _config.CmsPort);
StartStatusPolling();
}
public async Task InitializeAsync()
{
try
{
ConnectionStatus = "Connecting...";
//var cmsTask = _cms.ConnectAsync();
// await Task.WhenAll(cmsTask);
// Assuming fixed credentials for now, or pass them in if needed
_camera.Start(_config.CameraIp, LaneName, "admin", "Inno!@#$5678");
ConnectionStatus = "Running";
Log("System", "Started");
}
catch (Exception ex)
{
ConnectionStatus = "Error";
Log("Start Error", ex.Message, LogSeverity.Error);
}
}
private void StartStatusPolling()
{
_statusTimer = new System.Threading.Timer((state) =>
{
UpdateStatus();
}, null, 1000, 1000);
}
private void UpdateStatus()
{
if (Application.Current == null) return;
Application.Current.Dispatcher.Invoke(() =>
{
IsCameraConnected = _camera.IsConnected;
// IsCmsConnected = _cms.IsConnected;
CameraStatusText = IsCameraConnected ? "Connected" : "Disconnected";
CmsStatusText = IsCmsConnected ? "Connected" : "Disconnected";
});
}
public LaneConfig GetConfig() => _config;
// Method to update config object before saving
public void UpdateConfig()
{
_config.LaneName = LaneName;
_config.LaneDescription = LaneDescription;
_config.CameraId = CameraId;
_config.CameraPassword = CameraPassword;
_config.CameraPort = CameraPort;
_config.CameraIp = CameraIpString;
_config.RecognitionOrder = RecognitionOrder;
_config.RecognitionArea = RecognitionArea;
_config.NoPlateCode = NoPlateCode;
}
private void Camera_OnImageReceived(object? sender, byte[] buffer)
{
if (buffer == null || buffer.Length == 0) return;
try
{
using var mat = Cv2.ImDecode(buffer, ImreadModes.Color);
//mat.SaveImage("test.jpg");
var bitmapSource = OpenCvSharp.WpfExtensions.WriteableBitmapConverter.ToWriteableBitmap(mat);
bitmapSource.Freeze();
Application.Current.Dispatcher.Invoke(() =>
{
CameraImage = bitmapSource;
});
if (mat.Empty()) return;
// LPR Processing
var sw = System.Diagnostics.Stopwatch.StartNew();
if (_lprDetector == null)
{
Log("LPR Error", "Detector not initialized", LogSeverity.Error);
return;
}
var boxes = _lprDetector.DetectLicensePlateExistence(mat, new OpenCvSharp.Rect(0, 0, mat.Width, mat.Height));
sw.Stop();
long detectionTime = sw.ElapsedMilliseconds;
if (boxes.Count > 0)
{
// 1. Sort boxes based on user configuration
var sortedBoxes = _config.RecognitionOrder switch
{
RecognitionOrder.TopDown => boxes.OrderBy(b => b.y).ToList(),
RecognitionOrder.BottomUp => boxes.OrderByDescending(b => b.y).ToList(),
RecognitionOrder.LeftRight => boxes.OrderBy(b => b.x).ToList(),
RecognitionOrder.RightLeft => boxes.OrderByDescending(b => b.x).ToList(),
_ => boxes
};
bool validFound = false;
foreach (var box in sortedBoxes)
{
// Crop Plate Image logic
var plateRect = new OpenCvSharp.Rect((int)box.x, (int)box.y, (int)box.w, (int)box.h);
plateRect = plateRect.Intersect(new OpenCvSharp.Rect(0, 0, mat.Width, mat.Height));
if (plateRect.Width <= 0 || plateRect.Height <= 0) continue;
using var plateMat = new Mat(mat, plateRect);
var singleBoxList = new List<DeepLearning_Model_Sharp.LPR_Boxes> { box };
sw.Restart();
var result = _lprDetector.DetectLicensePlateCode(mat, singleBoxList);
sw.Stop();
long recognitionTime = sw.ElapsedMilliseconds;
var carNum = result.mRST_Code;
if (string.IsNullOrEmpty(carNum)) continue;
bool isValid = Regex.IsMatch(carNum, @"\d[가-힣]\d{4}");
if (isValid)
{
// Update UI with this Plate Image
var plateBitmap = OpenCvSharp.WpfExtensions.WriteableBitmapConverter.ToWriteableBitmap(plateMat);
plateBitmap.Freeze();
Application.Current.Dispatcher.Invoke(() => PlateImage = plateBitmap);
Application.Current.Dispatcher.Invoke(() =>
{
LastCarNumber = carNum;
IsAuthorized = true;
IsVehicleDetected = true;
TotalRecognitions++;
SuccessCount++;
OnPropertyChanged(nameof(SuccessRate));
Log($"[LP: {carNum}] (Valid) Order:{_config.RecognitionOrder} 검지: {detectionTime}ms, 인식: {recognitionTime}ms");
Task.Delay(3000).ContinueWith(_ =>
{
if (Application.Current != null)
{
Application.Current.Dispatcher.Invoke(() => IsVehicleDetected = false);
}
});
});
try
{
ProcessDetection(carNum, "ImagePath_Placeholder.jpg");
// Clone the mat for background processing to avoid race conditions with disposal
var matClone = mat.Clone();
Task.Run(() =>
{
try
{
SaveRecognitionImage(matClone, carNum);
}
finally
{
matClone.Dispose();
}
});
}
catch (Exception ex)
{
Log("Process Error", ex.Message, LogSeverity.Error);
}
// STOP processing detection for this frame as we found a valid one
validFound = true;
break;
}
}
// 4. Fallback: No Valid Plate Found but Boxes Existed
if (!validFound && boxes.Count > 0)
{
var failCode = _config.NoPlateCode; // "XXXXXXXX" or "????????"
Application.Current.Dispatcher.Invoke(() =>
{
LastCarNumber = failCode;
IsAuthorized = false;
IsVehicleDetected = true;
TotalRecognitions++;
FailCount++;
OnPropertyChanged(nameof(SuccessRate));
Log($"[LP: {failCode}] (No Valid Plate) Count:{boxes.Count}", LogSeverity.Warning);
Task.Delay(3000).ContinueWith(_ =>
{
if (Application.Current != null)
{
Application.Current.Dispatcher.Invoke(() => IsVehicleDetected = false);
}
});
});
try
{
ProcessDetection(failCode, "ImagePath_Placeholder_Fail.jpg");
// Clone for background save
var matClone = mat.Clone();
Task.Run(() =>
{
try
{
SaveRecognitionImage(matClone, failCode);
}
finally
{
matClone.Dispose();
}
});
}
catch (Exception ex)
{
Log("Process Fail Error", ex.Message, LogSeverity.Error);
}
}
}
}
catch (Exception ex)
{
_logger?.Error(ex, "Image Processing Error");
}
}
[RelayCommand]
private void SingleShot()
{
Log("Action", "Single Shot Triggered - Future: Open ROI Editor");
// Placeholder for ROI capture logic
}
[ObservableProperty]
private string? _imageRootPath;
private void SaveRecognitionImage(Mat originalMat, string resultText)
{
if (string.IsNullOrWhiteSpace(ImageRootPath)) return;
try
{
// 1. Create Directory Structure
var now = DateTime.Now;
var datePath = Path.Combine(ImageRootPath, LaneId, now.ToString("yyyy"), now.ToString("MM"), now.ToString("dd"));
if (!Directory.Exists(datePath)) Directory.CreateDirectory(datePath);
// 2. Generate Filename
var last4 = resultText.Length > 4 ? resultText.Substring(resultText.Length - 4) : resultText;
var fileName = $"{now:HHmmssfff}_{last4}.jpg";
var fullPath = Path.Combine(datePath, fileName);
// 3. Render and Encode on UI Thread (WPF requirement)
byte[]? imageBytes = null;
Application.Current.Dispatcher.Invoke(() =>
{
// Convert Mat to BitmapSource
var bitmapSource = OpenCvSharp.WpfExtensions.WriteableBitmapConverter.ToWriteableBitmap(originalMat);
var visual = new DrawingVisual();
using (var dc = visual.RenderOpen())
{
// Draw Image
dc.DrawImage(bitmapSource, new System.Windows.Rect(0, 0, bitmapSource.Width, bitmapSource.Height));
// Draw Text
var overlayText = $"{now:yyyy-MM-dd HH:mm:ss} {resultText}";
var typeface = new Typeface(new FontFamily("Malgun Gothic"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal);
var fontSize = 24.0;
var culture = CultureInfo.CurrentCulture;
var textPos = new System.Windows.Point(10, 10);
// Shadow (Black)
var shadowText = new FormattedText(overlayText, culture, FlowDirection.LeftToRight, typeface, fontSize, Brushes.Black, VisualTreeHelper.GetDpi(visual).PixelsPerDip);
dc.DrawText(shadowText, new System.Windows.Point(textPos.X + 2, textPos.Y + 2));
// Main (Yellow)
var mainText = new FormattedText(overlayText, culture, FlowDirection.LeftToRight, typeface, fontSize, Brushes.Yellow, VisualTreeHelper.GetDpi(visual).PixelsPerDip);
dc.DrawText(mainText, textPos);
}
// Render to Bitmap
var rtb = new RenderTargetBitmap(bitmapSource.PixelWidth, bitmapSource.PixelHeight, 96, 96, PixelFormats.Pbgra32);
rtb.Render(visual);
// Encode to Jpeg
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
using var ms = new MemoryStream();
encoder.Save(ms);
imageBytes = ms.ToArray();
});
// 4. Save to File (Background Thread)
if (imageBytes != null)
{
File.WriteAllBytes(fullPath, imageBytes);
}
}
catch (Exception ex)
{
Log("Image Save Error", ex.Message, LogSeverity.Error);
}
}
private void ProcessDetection(string carNum, string imagePath)
{
// Notify MainViewModel to send CMS
var data = new LPRDataSet
{
DeviceID = LaneName, // Use LaneName as DeviceID for now
RCarNumber = carNum,
Time = DateTime.Now,
ImagePath = imagePath,
TimeRaWData = DateTime.Now.ToString("yyyyMMdd-HHmmss-ffffff")
};
//if (_cms.IsConnected)
{
// _cms.SendResult(data);
Log(carNum, "Sent CMS");
}
OnCarDetected?.Invoke(data);
}
public ObservableCollection<string> Logs { get; } = new ObservableCollection<string>();
private void Log(string plate, string result, LogSeverity severity = LogSeverity.Info)
{
Log($"{plate}: {result}", severity);
}
private void Log(string msg, LogSeverity severity = LogSeverity.Info)
{
_logger?.Information(msg);
if (Application.Current == null) return;
Application.Current.Dispatcher.Invoke(() =>
{
Logs.Insert(0, $"[{DateTime.Now:HH:mm:ss}] {msg}");
if (Logs.Count > 50) Logs.RemoveAt(Logs.Count - 1);
});
OnLog?.Invoke($"[{LaneName}] {msg}", severity);
}
public void Cleanup()
{
_statusTimer?.Dispose();
// Dispose other resources if implemented in their classes
}
}
}

View File

@@ -0,0 +1,377 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Inno.LPR;
using LPR_Manager.Model.Device;
using LPR_Manager.Model.Device.SubClass;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Win32;
using Model = LPR_Manager.Model;
namespace LPR_Manager.ViewModel
{
public partial class MainViewModel : ObservableObject
{
public ObservableCollection<LprLaneViewModel> Lanes { get; } = new ObservableCollection<LprLaneViewModel>();
public ObservableCollection<LPR_Manager.Model.AppLogEntry> Logs { get; } = new ObservableCollection<LPR_Manager.Model.AppLogEntry>();
public ObservableCollection<LPR_Manager.Model.AppLogEntry> ErrorLogs { get; } = new ObservableCollection<LPR_Manager.Model.AppLogEntry>();
[ObservableProperty]
private bool _hasCriticalErrors;
[RelayCommand]
private void ClearErrors()
{
ErrorLogs.Clear();
HasCriticalErrors = false;
}
[ObservableProperty]
private LprLaneViewModel? _selectedLane;
[ObservableProperty]
private int _selectedTabIndex;
[ObservableProperty]
private string _connectionStatus = "Init";
[ObservableProperty]
private string _currentTime = DateTime.Now.ToString("HH:mm:ss");
[ObservableProperty]
private string _healthStatus = "Optimal";
[ObservableProperty]
private string _userName = "Guest";
[ObservableProperty]
private string _userRole = "Log in to Access";
[ObservableProperty]
private string _sessionTimeRemaining = "";
private bool _isLoggedIn = false;
private DateTime _lastActivity;
private System.Threading.Timer? _statusTimer;
// Expose init task for App startup waiting
public Task InitializationTask { get; private set; }
[ObservableProperty]
private Model.SystemConfig _systemConfig = new();
private Service.ImageServer? _imageServer;
private readonly Service.ConfigService _configService = new();
public MainViewModel()
{
InitializationTask = InitializeSystemAsync();
StartStatusPolling();
}
public void ResetIdleTimer()
{
_lastActivity = DateTime.Now;
}
private void StartStatusPolling()
{
_statusTimer = new System.Threading.Timer((state) =>
{
UpdateStatus();
}, null, 1000, 1000);
}
private void UpdateStatus()
{
CurrentTime = DateTime.Now.ToString("HH:mm:ss");
if (_isLoggedIn)
{
var remaining = TimeSpan.FromMinutes(10) - (DateTime.Now - _lastActivity);
if (remaining.TotalSeconds <= 0)
{
Logout();
}
else
{
SessionTimeRemaining = remaining.ToString(@"mm\:ss");
}
}
else
{
SessionTimeRemaining = "";
}
// Update Image Server Status
if (_imageServer != null && _imageServer.IsRunning)
{
HealthStatus = "Running";
}
else
{
HealthStatus = "Stopped";
}
}
private async Task InitializeSystemAsync()
{
await Task.Run(async () =>
{
try
{
ConnectionStatus = "Connecting...";
// Load System Config
SystemConfig = await _configService.LoadSystemConfigAsync();
// Start Image Server
try
{
_imageServer = new Service.ImageServer(SystemConfig);
await _imageServer.StartAsync();
AddLog($"Image Server started on port {SystemConfig.ImageServerPort}");
}
catch (Exception ex)
{
AddLog($"Image Server Start Failed: {ex.Message}", LPR_Manager.Model.LogSeverity.Error);
}
var configs = await _configService.LoadAllLanesAsync();
if (configs.Count == 0)
{
var defaultLane = new Model.LaneConfig
{
LaneName = "Entrance",
CameraIp = "192.168.100.99",
CameraPort = 80,
LaneId = "F0001",
LaneDescription = "Main Entrance",
CmsIp = "127.0.0.1",
CmsPort = 6000
};
configs.Add(defaultLane);
await _configService.SaveLaneAsync(defaultLane);
}
// UI Update must be on Dispatcher
await Application.Current.Dispatcher.InvokeAsync(() =>
{
foreach (var cfg in configs)
{
AddLaneInternal(cfg);
}
});
// Update ImageRootPath for all lanes
foreach (var lane in Lanes)
{
lane.ImageRootPath = SystemConfig.ImageRootPath;
}
// Connect All Lanes
var laneTasks = new System.Collections.Generic.List<Task>();
foreach (var lane in Lanes)
{
laneTasks.Add(lane.InitializeAsync());
}
await Task.WhenAll(laneTasks);
ConnectionStatus = "Running";
AddLog("System Started (Multi-Lane Mode).");
}
catch (Exception ex)
{
ConnectionStatus = "Error";
AddLog($"Start Error: {ex.Message}", LPR_Manager.Model.LogSeverity.Error);
}
});
}
[RelayCommand]
private async Task SaveSystemConfig()
{
await _configService.SaveSystemConfigAsync(SystemConfig);
Service.LogService.ConfigureLogging(SystemConfig.LogMode); // Apply changes immediately
// Apply Image settings immediately
foreach (var lane in Lanes)
{
lane.ImageRootPath = SystemConfig.ImageRootPath;
}
AddLog("System settings saved. Restart required to apply changes.");
MessageBox.Show("System settings saved. Please restart the application to apply changes.", "Settings Saved", MessageBoxButton.OK, MessageBoxImage.Information);
}
[RelayCommand]
private void BrowseImageFolder()
{
var dialog = new OpenFolderDialog
{
Title = "Select Image Server Root Folder",
Multiselect = false
};
// Initial Directory
if (!string.IsNullOrEmpty(SystemConfig.ImageRootPath) && System.IO.Directory.Exists(SystemConfig.ImageRootPath))
{
dialog.InitialDirectory = SystemConfig.ImageRootPath;
}
if (dialog.ShowDialog() == true)
{
SystemConfig.ImageRootPath = dialog.FolderName;
}
}
[RelayCommand]
private async Task AddLane()
{
// Fix: Calculate ID based on existing max ID, not Count
int maxId = 0;
foreach (var lane in Lanes)
{
if (lane.LaneId.StartsWith("F") && int.TryParse(lane.LaneId.Substring(1), out int id))
{
if (id > maxId) maxId = id;
}
}
var newIdx = maxId + 1;
var cfg = new Model.LaneConfig
{
LaneName = $"Lane {newIdx}",
CameraIp = "127.0.0.1",
CameraPort = 80,
LaneId = $"F{newIdx:D4}",
LaneDescription = "New Site",
CmsIp = "127.0.0.1",
CmsPort = 6000
};
var laneVm = AddLaneInternal(cfg);
_ = laneVm.InitializeAsync();
await _configService.SaveLaneAsync(cfg);
SelectedLane = laneVm; // Auto-select new lane
}
[RelayCommand]
private async Task SaveLane()
{
if (SelectedLane == null) return;
SelectedLane.UpdateConfig();
await _configService.SaveLaneAsync(SelectedLane.GetConfig());
AddLog($"Saved settings for {SelectedLane.LaneName}");
}
private LprLaneViewModel AddLaneInternal(Model.LaneConfig cfg)
{
var lane = new LprLaneViewModel(cfg, SystemConfig.LogMode);
lane.ImageRootPath = SystemConfig.ImageRootPath; // Set path
lane.OnLog += AddLog;
lane.OnRemove += async (vm) => await RemoveLane(vm);
Lanes.Add(lane);
return lane;
}
[RelayCommand]
private async Task RemoveLane(LprLaneViewModel vm)
{
if (vm == null) return;
var result = MessageBox.Show(Application.Current.MainWindow, $"Are you sure you want to delete {vm.LaneName}?", "Confirm Delete", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result != MessageBoxResult.Yes) return;
vm.Cleanup();
Lanes.Remove(vm);
_configService.DeleteLaneConfig(vm.LaneId);
if (SelectedLane == vm) SelectedLane = null;
}
[RelayCommand]
private void Login()
{
if (_isLoggedIn)
{
Logout();
return;
}
var loginWin = new View.LoginWindow();
if (Application.Current.MainWindow != null && Application.Current.MainWindow != loginWin)
{
loginWin.Owner = Application.Current.MainWindow;
}
if (loginWin.ShowDialog() == true)
{
_isLoggedIn = true;
UserName = "Admin User";
UserRole = "ADMIN";
ResetIdleTimer();
SelectedTabIndex = 4;
}
}
[RelayCommand]
private void Logout()
{
_isLoggedIn = false;
UserName = "Guest";
UserRole = "Log in to Access";
// Redirect to Dashboard if currently on Settings
if (Application.Current != null)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (SelectedTabIndex == 4) SelectedTabIndex = 0;
});
}
}
[RelayCommand]
private void GoToSettings()
{
if (_isLoggedIn)
{
SelectedTabIndex = 4;
}
else
{
// If not logged in, trigger login flow
Login();
}
}
private void AddLog(string msg, LPR_Manager.Model.LogSeverity severity = LPR_Manager.Model.LogSeverity.Info)
{
if (Application.Current == null) return;
Application.Current.Dispatcher.Invoke(() =>
{
var entry = new LPR_Manager.Model.AppLogEntry { Message = msg, Severity = severity, Timestamp = DateTime.Now };
Logs.Insert(0, entry);
if (Logs.Count > 1000) Logs.RemoveAt(Logs.Count - 1);
if (severity == LPR_Manager.Model.LogSeverity.Error)
{
ErrorLogs.Insert(0, entry);
HasCriticalErrors = true;
}
});
}
}
}

View File

@@ -0,0 +1,16 @@
2026-01-30 12:47:10.575 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:47:12.661 +09:00 [INF] System: Started
2026-01-30 12:47:12.680 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:47:12.781 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 12:48:38.420 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:48:40.498 +09:00 [INF] System: Started
2026-01-30 12:48:40.518 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:48:40.623 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 12:50:10.281 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:50:12.346 +09:00 [INF] System: Started
2026-01-30 12:50:12.363 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:50:12.467 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 12:54:21.792 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:54:23.882 +09:00 [INF] System: Started
2026-01-30 12:54:23.890 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:54:23.995 +09:00 [INF] Camera: Camera Auth: 0, OK

View File

@@ -0,0 +1,11 @@
2026-01-30 12:54:21.863 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:54:23.906 +09:00 [INF] System: Started
2026-01-30 12:54:23.910 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:54:24.058 +09:00 [INF] Camera: Camera Auth: -1, ERROR
2026-01-30 12:54:41.173 +09:00 [INF] Camera: Camera Entrance Connected: False
2026-01-30 12:54:41.176 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:54:41.244 +09:00 [INF] Camera: Camera Auth: -1, ERROR
2026-01-30 12:54:46.047 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:54:48.126 +09:00 [INF] System: Started
2026-01-30 12:54:48.137 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:54:48.240 +09:00 [INF] Camera: Camera Auth: 0, OK

View File

@@ -0,0 +1,153 @@
2026-01-30 12:54:46.108 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:54:48.149 +09:00 [INF] System: Started
2026-01-30 12:54:48.153 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:54:48.302 +09:00 [INF] Camera: Camera Auth: -1, ERROR
2026-01-30 12:55:11.209 +09:00 [INF] Camera: Camera Entrance Connected: False
2026-01-30 12:55:11.212 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:55:11.278 +09:00 [INF] Camera: Camera Auth: -1, ERROR
2026-01-30 13:10:32.619 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:10:34.700 +09:00 [INF] System: Started
2026-01-30 13:10:34.719 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:10:34.825 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:12:51.133 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:12:53.211 +09:00 [INF] System: Started
2026-01-30 13:12:53.215 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:12:53.327 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:13:57.728 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:13:59.803 +09:00 [INF] System: Started
2026-01-30 13:13:59.819 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:13:59.927 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:17:43.362 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:17:45.424 +09:00 [INF] System: Started
2026-01-30 13:17:45.428 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:17:45.543 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:20:08.779 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:20:10.843 +09:00 [INF] System: Started
2026-01-30 13:20:10.846 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:20:10.957 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:21:49.918 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:21:51.992 +09:00 [INF] System: Started
2026-01-30 13:21:52.011 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:21:52.117 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:22:50.512 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:22:52.571 +09:00 [INF] System: Started
2026-01-30 13:22:52.595 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:22:52.696 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:26:54.547 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:26:56.618 +09:00 [INF] System: Started
2026-01-30 13:26:56.636 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:26:56.743 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:27:07.603 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:27:09.660 +09:00 [INF] System: Started
2026-01-30 13:27:09.664 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:27:09.779 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:28:53.224 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:28:55.283 +09:00 [INF] System: Started
2026-01-30 13:28:55.286 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:28:55.397 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 13:30:41.204 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 13:30:43.276 +09:00 [INF] System: Started
2026-01-30 13:30:43.293 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 13:30:43.396 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:00:12.290 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:00:14.384 +09:00 [INF] System: Started
2026-01-30 14:00:14.400 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:00:14.509 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:22:59.841 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:23:01.932 +09:00 [INF] System: Started
2026-01-30 14:23:01.951 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:23:02.059 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:24:54.813 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:24:57.095 +09:00 [INF] System: Started
2026-01-30 14:24:57.114 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:24:57.225 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:26:36.015 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:26:38.072 +09:00 [INF] System: Started
2026-01-30 14:26:38.075 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:26:38.186 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:27:39.066 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:27:41.134 +09:00 [INF] System: Started
2026-01-30 14:27:41.137 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:27:41.247 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:28:27.130 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:28:29.191 +09:00 [INF] System: Started
2026-01-30 14:28:29.195 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:28:29.308 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:31:20.028 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:31:22.104 +09:00 [INF] System: Started
2026-01-30 14:31:22.107 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:31:22.223 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:40:18.823 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:41:10.320 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:41:12.382 +09:00 [INF] System: Started
2026-01-30 14:41:12.386 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:41:12.494 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:44:46.031 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:44:48.088 +09:00 [INF] System: Started
2026-01-30 14:44:48.092 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:44:48.205 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 14:55:07.333 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 14:55:09.393 +09:00 [INF] System: Started
2026-01-30 14:55:09.413 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 14:55:09.513 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 15:01:55.884 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 15:01:57.949 +09:00 [INF] System: Started
2026-01-30 15:01:57.959 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 15:01:58.074 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 15:14:14.379 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 15:14:16.462 +09:00 [INF] System: Started
2026-01-30 15:14:16.480 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 15:14:16.584 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 15:20:47.653 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 15:20:49.716 +09:00 [INF] System: Started
2026-01-30 15:20:49.737 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 15:20:49.844 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 15:58:16.570 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 15:58:18.687 +09:00 [INF] System: Started
2026-01-30 15:58:18.710 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 15:58:18.822 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:02:43.224 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:02:45.285 +09:00 [INF] System: Started
2026-01-30 16:02:45.321 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:02:45.424 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:03:04.640 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:03:18.423 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:03:20.472 +09:00 [INF] System: Started
2026-01-30 16:03:20.492 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:03:20.600 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:05:42.811 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:05:44.897 +09:00 [INF] System: Started
2026-01-30 16:05:44.920 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:05:45.028 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:08:50.505 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:08:52.562 +09:00 [INF] System: Started
2026-01-30 16:08:52.582 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:08:52.692 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:13:06.500 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:13:08.566 +09:00 [INF] System: Started
2026-01-30 16:13:08.570 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:13:08.680 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:14:45.711 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:14:47.774 +09:00 [INF] System: Started
2026-01-30 16:14:47.803 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:14:47.913 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:29:28.994 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:29:31.094 +09:00 [INF] System: Started
2026-01-30 16:29:31.115 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:29:31.220 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:33:25.655 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:33:27.765 +09:00 [INF] System: Started
2026-01-30 16:33:27.778 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:33:27.905 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:37:45.848 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:37:47.938 +09:00 [INF] System: Started
2026-01-30 16:37:47.956 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:37:48.057 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:40:07.541 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:40:09.640 +09:00 [INF] System: Started
2026-01-30 16:40:09.663 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:40:09.774 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 16:41:01.389 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 16:41:03.462 +09:00 [INF] System: Started
2026-01-30 16:41:03.480 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 16:41:03.585 +09:00 [INF] Camera: Camera Auth: 0, OK

View File

@@ -0,0 +1,44 @@
2026-02-02 11:05:31.977 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:05:32.184 +09:00 [INF] System: Started
2026-02-02 11:05:32.202 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:05:32.338 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:31:04.489 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:31:04.633 +09:00 [INF] System: Started
2026-02-02 11:31:04.653 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:31:04.846 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:35:57.381 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:35:57.425 +09:00 [INF] System: Started
2026-02-02 11:35:57.443 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:35:57.590 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:36:23.530 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:36:23.569 +09:00 [INF] System: Started
2026-02-02 11:36:23.591 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:36:23.724 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:40:46.010 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:40:46.066 +09:00 [INF] System: Started
2026-02-02 11:40:46.085 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:40:46.228 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:42:27.318 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:42:27.358 +09:00 [INF] System: Started
2026-02-02 11:42:27.363 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:42:27.470 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:47:36.740 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:47:36.771 +09:00 [INF] System: Started
2026-02-02 11:47:36.791 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:47:36.953 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 11:59:05.924 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 11:59:05.990 +09:00 [INF] System: Started
2026-02-02 11:59:06.007 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 11:59:06.106 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 12:00:08.907 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 12:00:08.943 +09:00 [INF] System: Started
2026-02-02 12:00:08.963 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 12:00:09.105 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 12:01:01.317 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 12:01:01.361 +09:00 [INF] System: Started
2026-02-02 12:01:01.375 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 12:01:01.474 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-02-02 14:45:54.423 +09:00 [INF] Init Error: Detection Construct Fail
2026-02-02 14:45:54.574 +09:00 [INF] System: Started
2026-02-02 14:45:54.579 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-02-02 14:45:54.739 +09:00 [INF] Camera: Camera Auth: 0, OK

View File

@@ -0,0 +1,8 @@
2026-01-30 10:29:15.351 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:29:17.427 +09:00 [INF] System: Started
2026-01-30 10:29:17.430 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 10:29:17.544 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 12:41:27.784 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 12:41:30.049 +09:00 [INF] System: Started
2026-01-30 12:41:30.086 +09:00 [INF] Camera: Camera Entrance Connected: True
2026-01-30 12:41:30.196 +09:00 [INF] Camera: Camera Auth: 0, OK

View File

@@ -0,0 +1,60 @@
2026-01-30 09:13:18.574 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:13:20.676 +09:00 [INF] System: Started
2026-01-30 09:13:20.695 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:13:20.804 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:18:38.521 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:18:40.646 +09:00 [INF] System: Started
2026-01-30 09:18:40.650 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:18:40.772 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:22:23.876 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:22:25.982 +09:00 [INF] System: Started
2026-01-30 09:22:25.986 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:22:26.128 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:26:31.302 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:26:33.494 +09:00 [INF] System: Started
2026-01-30 09:26:33.516 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:26:33.628 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:50:20.434 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:50:22.543 +09:00 [INF] System: Started
2026-01-30 09:50:22.559 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:50:22.665 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:56:16.842 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:56:18.934 +09:00 [INF] System: Started
2026-01-30 09:56:18.939 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:56:19.055 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:57:47.899 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:57:49.989 +09:00 [INF] System: Started
2026-01-30 09:57:49.992 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:57:50.112 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 09:58:53.338 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:58:55.437 +09:00 [INF] System: Started
2026-01-30 09:58:55.442 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 09:58:55.562 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:00:23.003 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:00:25.091 +09:00 [INF] System: Started
2026-01-30 10:00:25.094 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:00:25.213 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:03:30.282 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:03:32.388 +09:00 [INF] System: Started
2026-01-30 10:03:32.408 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:03:32.519 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:14:03.816 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:14:05.909 +09:00 [INF] System: Started
2026-01-30 10:14:05.927 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:14:06.026 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:14:43.046 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:14:45.142 +09:00 [INF] System: Started
2026-01-30 10:14:45.152 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:14:45.286 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:15:14.922 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:15:17.017 +09:00 [INF] System: Started
2026-01-30 10:15:17.022 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:15:17.156 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:20:44.132 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:20:46.225 +09:00 [INF] System: Started
2026-01-30 10:20:46.242 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:20:46.348 +09:00 [INF] Camera: Camera Auth: 0, OK
2026-01-30 10:23:25.697 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:23:27.783 +09:00 [INF] System: Started
2026-01-30 10:23:27.787 +09:00 [INF] Camera: Camera Lane 1 Connected: True
2026-01-30 10:23:27.910 +09:00 [INF] Camera: Camera Auth: 0, OK

View File

@@ -0,0 +1,30 @@
2026-01-30 09:13:18.619 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:13:20.673 +09:00 [INF] System: Started
2026-01-30 09:18:38.589 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:18:40.650 +09:00 [INF] System: Started
2026-01-30 09:22:23.925 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:22:25.978 +09:00 [INF] System: Started
2026-01-30 09:26:31.457 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:26:33.498 +09:00 [INF] System: Started
2026-01-30 09:50:20.494 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:50:22.540 +09:00 [INF] System: Started
2026-01-30 09:56:16.889 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:56:18.936 +09:00 [INF] System: Started
2026-01-30 09:57:47.948 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:57:49.992 +09:00 [INF] System: Started
2026-01-30 09:58:53.386 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 09:58:55.435 +09:00 [INF] System: Started
2026-01-30 10:00:23.047 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:00:25.089 +09:00 [INF] System: Started
2026-01-30 10:03:30.331 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:03:32.385 +09:00 [INF] System: Started
2026-01-30 10:14:03.862 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:14:05.903 +09:00 [INF] System: Started
2026-01-30 10:14:43.096 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:14:45.148 +09:00 [INF] System: Started
2026-01-30 10:15:14.970 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:15:17.022 +09:00 [INF] System: Started
2026-01-30 10:20:44.177 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:20:46.229 +09:00 [INF] System: Started
2026-01-30 10:23:25.747 +09:00 [INF] Init Error: Detection Construct Fail
2026-01-30 10:23:27.786 +09:00 [INF] System: Started

91
ProjectAnalysis.md Normal file
View File

@@ -0,0 +1,91 @@
# LPR_Manager 프로젝트 분석 보고서
## 1. 개요
- **목적**: 다차선 주차장 번호판 인식 결과를 실시간으로 모니터링하고, 카메라/LED/차단기를 통합 제어하는 WPF 클라이언트.
- **플랫폼**: .NET 8 WPF (`net8.0-windows`), MaterialDesign XAML 테마, CommunityToolkit.Mvvm 기반 MVVM 구조.
- **주요 의존성**: `OpenCvSharp4`/`WpfExtensions`(영상 처리), `Serilog`(로그), `Microsoft.AspNetCore.App`(내장 이미지 서버), 외부 네이티브 SDK `NtcClientAPI4Net.dll`, 사내 라이브러리 `Inno.LPR`.
- **구조 요약**: `View`/`ViewModel`/`Model`/`Service`/`Protocol` 계층으로 분리, Lane 단위를 중심으로 한 ObservableCollection 관리.
## 2. 계층별 구성
### UI (XAML)
- `MainWindow.xaml`: 좌측 네비게이션 + 상단 상태바 + 탭 기반 콘텐츠. 대시보드 탭은 `LprLaneControl` 반복, 나머지는 Placeholder UI.
- `View/LprLaneControl.xaml`: 단일 차선 카드. 카메라 스트림, 인식 번호판, 상태 Pill, 최근 로그 DataGrid 등으로 구성.
- `View/SettingsControl.xaml`: 시스템/차선 설정 편집 UI. Lane 목록과 상세 편집 폼을 하나의 뷰로 처리.
- `View/LoginWindow.xaml`, `View/SplashScreen.xaml`: 로그인 및 초기 스플래시 UX.
### ViewModel & 상태 관리
- `ViewModel/MainViewModel.cs`: 전역 상태, Lane 목록, 설정 및 인증 흐름을 담당.
- 앱 시작 시 `InitializationTask`에서 설정 로드 → 이미지 서버 기동 (`Service.ImageServer`) → Lane ViewModel 생성/초기화.
- `System.Threading.Timer`로 UI 타임스탬프, 세션 타이머, 이미지 서버 상태를 1초 주기로 갱신.
- Lane 추가/저장/삭제, 시스템 설정 저장, 로그인/로그아웃 Command 제공.
- `ViewModel/LprLaneViewModel.cs`: 카메라/인식/LED/CMS 제어에 대한 핵심 로직.
- `WushiCamera` SDK 이벤트를 받아 OpenCvSharp로 영상 디코딩/번호판 탐지/인식 수행.
- Serilog Logger를 Lane별로 구성하여 파일 혹은 Seq 출력.
- 인식 결과를 UI 속성 및 `OnCarDetected` 이벤트로 전달하고, 로컬 이미지 저장/추후 CMS 전송 준비.
### Services
- `Service.ConfigService`: `Configs/` 폴더에 Lane 및 시스템 설정을 JSON 으로 직렬화/역직렬화.
- `Service.ImageServer`: Minimal API (`WebApplication`)로 구동되는 정적 파일 서버. `/images/*` 경로를 Basic Auth 로 보호.
- `Service.LogService`: 전역 Serilog 싱크 구성(파일/Seq).
- `Service.WushiCamera`: `NtcClientAPI4Net.dll` P/Invoke 래퍼. 카메라 연결 및 트리거 콜백 처리.
### Device & Protocol Layer
- `Model/Device/*`: Gate/LED/CMS 컨트롤러 인터페이스 + 기본 구현. 현재 TCP 커넥터 부분은 주석 처리되어 있으며, 프로토콜 패킷 조합만 남아 있음.
- `Protocol/*`: LED(UJIN, YJMICRO), Gate(FPTV1), CMS(InnoV1) 규격 문자열/바이트 생성 및 파싱 함수.
- `Model/Device/SubClass/*`: CMS/LPR 데이터 전송 DTO, LED 텍스트 세트 등 부가 모델 정의.
## 3. 실행 흐름 요약
1. `App.OnStartup` (`App.xaml.cs`)
- `ConfigService`로 시스템 설정 로드 → `LogService` 설정.
- 메인 윈도우를 표시한 뒤 스플래시를 메인 중앙에 띄우고 `MainViewModel.InitializationTask` 또는 10초 타임아웃을 기다림.
2. `MainViewModel.InitializeSystemAsync`
- 이미지 서버 (`Service.ImageServer`)를 `SystemConfig` 기반으로 기동.
- Lane 설정을 로드하고 없으면 기본 Lane 생성/저장.
- 각 Lane마다 `LprLaneViewModel` 생성 → `InitializeAsync`로 카메라 연결 및 엔진 준비.
3. `LprLaneViewModel`
- `WushiCamera.OnImageReceived` 이벤트에서 JPEG 버퍼를 OpenCV `Mat`으로 디코드 → 번호판 박스 탐지 → 사용자가 지정한 정렬 기준으로 우선순위 결정.
- 정상 판독 시 UI 상태, 로그, `ProcessDetection` 호출, 이미지 파일 저장.
- CMS/LED/Gate 제어 부분은 주석/스텁으로 남아 있어 확장 여지 존재.
## 4. 데이터 & 설정 파일
- `Configs/system_config.json`: 이미지 서버 포트/자격증명, 이미지 저장 경로, 로그 모드.
- `Configs/*.json`: Lane 별 카메라/CMS 설정 (동적으로 생성).
- `Logs/`: Serilog 및 `WushiCamera` 로그, Lane별 로그.
- 이미지 저장 경로: `SystemConfig.ImageRootPath` (기본 `Images/`). 날짜/차선 단위 폴더 구조로 저장.
## 5. 개선이 필요한 부분 (우선순위 순)
1. **잘못된 바인딩 / 누락된 속성**
- `MainWindow.xaml:92`에서 `SessionTimeRemaining`(문자열)을 `BooleanToVisibilityConverter`에 그대로 바인딩하여 런타임 예외가 발생. 표시 여부를 위한 bool 속성을 별도로 두거나 `StringIsNullOrEmpty` 변환기를 구현해야 함.
- `View/LprLaneControl.xaml:91``LastDetectionTime`에 바인딩하지만 `LprLaneViewModel`에 해당 속성이 전혀 없음. 인식 시점 추적용 속성을 추가하고 `Camera_OnImageReceived`에서 갱신해야 함.
2. **리소스 해제 부재**
- `MainViewModel``LprLaneViewModel``System.Threading.Timer`와 네이티브 카메라 핸들을 생성하지만 `IDisposable`을 구현하지 않아 윈도우를 닫아도 백그라운드 스레드와 네이티브 리소스가 남음 (`MainWindow.xaml.cs`는 종료 훅도 없음).
- `Service.ImageServer``StartAsync`만 호출하고 애플리케이션 종료 시 `StopAsync`를 호출하지 않음. Graceful shutdown 경로를 추가해야 포트가 해제됨.
3. **동기식/무제한 영상 처리** (`ViewModel/LprLaneViewModel.cs:210` 이후)
- 카메라 SDK 콜백에서 OpenCV 처리와 WPF `Dispatcher.Invoke`가 연쇄적으로 실행되어 콜백 쓰레드가 블로킹되고 GC 압박이 큼. CPU 집약 작업을 전용 `Task.Run` 파이프라인으로 분리하고, UI 접근은 `Dispatcher.BeginInvoke`로 최소화할 필요가 있음.
- 인식 실패 시에도 모든 박스를 순회하므로 고주파 입력에서 backlog가 쉽게 발생. 프레임 드랍/율 제한이 필요.
4. **하드코딩된 인증 정보 및 보안 취약점**
- `View/LoginWindow.xaml.cs`에 관리자 ID/비밀번호가 평문으로 고정되어 있으며 시도 횟수 제한도 없음. 구성 파일 또는 외부 인증과 연동하고 암호는 해시로 비교하도록 변경 필요.
- `Service.ImageServer`는 HTTP + Basic Auth를 사용하지만 TLS가 없고 비밀번호가 `Configs/system_config.json`에 평문 저장됨. 최소한 HTTPS(개발 인증서)와 암호 해싱/암호화, 혹은 내부망 한정이라는 주석/문서가 필요.
5. **설정값 미사용**
- `LprLaneViewModel.InitializeAsync`에서 카메라 로그인 정보를 `_config` 대신 하드코딩(`admin` / `Inno!@#$5678`)으로 넘김. 설정 UI에서 값을 바꿔도 실제 카메라에는 적용되지 않음.
- `RecognitionArea` 문자열, `UseSingleShot`, CMS/LED 관련 설정이 실제 로직에 연결되어 있지 않아 UI/Config와 동작 간 괴리가 큼.
6. **저장 이미지 경로 처리 오류**
- `ProcessDetection`에서 CMS로 넘기는 `ImagePath`가 항상 플레이스홀더 문자열이라 실제 저장 경로를 외부 시스템이 참조할 수 없음 (`ImageRootPath`를 기준으로 상대/절대 경로를 반환해야 함).
- `SaveRecognitionImage`는 WPF UI 스레드에서 `RenderTargetBitmap`를 생성하므로 고해상도 프레임 처리 시 UI 프리즈가 발생. 비 UI 스레드 인코딩 또는 WriteableBitmap → JPEG 변환 파이프라인으로 교체 필요.
7. **예외 및 복구 전략 부족**
- `MainViewModel.InitializeSystemAsync``Task.Run` 내부에서 예외를 삼키고 상태만 "Error"로 바꾸지만 사용자에게 구체적 원인을 표시하지 않으며 재시작 버튼도 없음.
- `WushiCamera.Start` 호출 실패 케이스(핸들 null, 반환 코드) 미검증. 연결 재시도 로직 필요.
8. **테스트/운영 편의성**
- Lane 구성이 파일로만 존재하여 UI 없이도 배포 환경에서 초기 Config를 넣기 어렵고, 버전 관리도 힘듦. 샘플 config와 Schema 설명 문서가 필요.
- `Inno.LPR` 프로젝트 및 네이티브 DLL 에 대한 빌드/배포 가이드가 없음 (README 미제공).
## 6. 권장 후속 작업
1. **ViewModel 정비**: `LprLaneViewModel``LastDetectionTime` 및 bool 노출 추가, `RecognitionArea` 파싱/적용, 카메라 인증 정보/이미지 경로 반영.
2. **리소스 & 수명 주기 관리**: `MainWindow` 종료 시점에 Lane VM/이미지 서버/Timer/카메라를 명시적으로 Dispose. `App.xaml.cs` 혹은 `MainViewModel``IDisposable` 구현 권장.
3. **영상 처리 파이프라인 개선**: 카메라 콜백에서 Frame Buffer를 채널로 넘기고, 전용 Task에서 OpenCV 및 저장 처리, UI 업데이트는 최소화. FPS 제한·중복 판독 Debounce 도입.
4. **보안 강화**: 로그인 절차를 Config 기반 혹은 사내 인증 연동으로 교체, 이미지 서버에 HTTPS/TLS 적용 및 비밀번호 암호화, Config 파일 접근권한 가이드 문서화.
5. **운영 도구화**: Config 폴더 구조/필드 설명, 샘플 JSON, 배포 스크립트, 로그 경로 정리 등을 README 또는 위키에 문서화.
6. **CMS/LED/Gate 통합 마무리**: 현재 주석으로 남아있는 TCP 커넥터 부분(`Model.Device`)과 `Inno.LPR` 결과 연동을 구현하여 실제 하드웨어 제어까지 닫기.
---
이 문서는 `D:/Documents/Projects/Parking/Center/LPR_Manager/LPR_Manager` 기준 소스(2026-02-02) 상태를 바탕으로 작성되었습니다.

40
README.md Normal file
View File

@@ -0,0 +1,40 @@
# LPR_Manager
Center-Based Recognition & Control System.
## Project Overview
- **Framework**: .NET 8.0 (WPF)
- **Architecture**: MVVM
- **Dependencies**:
- `Inno.LPR` (LPR Engine integration)
- `Inno.Novitec` (Camera SDK integration)
- `MaterialDesignThemes` (UI)
- `CommunityToolkit.Mvvm`
- `Serilog`
## Structure
- `LPR_Manager`: Main Application (WPF).
- `Model`: Data models and Enums.
- `Protocol`: Protocol implementations (CMS, Gate, LED).
- `ViewModel`: UI Logic (`MainViewModel`).
- `Code`: Global objects.
- `Inno.LPR`: Legacy LPR Engine wrapper (Migrated to .NET 8).
- `Inno.Novitec`: Camera SDK wrapper (Migrated to .NET 8).
- `Inno.Common.Stubs`: Re-implementation of missing dependencies (`Inno.Communication`, `Inno.Rtsp`) to ensure compilation without full legacy codebase.
## Prerequisites
- The solution relies on `NtcSDK` and `Inno.LPR` native DLLs.
- Ensure `Libraries` folder in `Inno.LPR` contains the necessary DLLs (`DeepLearning*.dll`, `opencv*.dll`). These are copied to output directory on build.
## How to Run
1. Open `LPR_Manager.slnx` or `LPR_Manager.sln` in Visual Studio 2022.
2. Build Solution.
3. Run `LPR_Manager`.
## Notes
- **Protocols**:
- CMS: `InnoV1`
- Gate: `FPTV1`
- LED: `YJMICRO`
- **Networking**: TCP communication is implemented using a lightweight `TcpClientConnector` in `Inno.Common.Stubs`.
- **UI**: Material Design based Dashboard.

12
lanes_config.json Normal file
View File

@@ -0,0 +1,12 @@
[
{
"LaneName": "Entrance",
"CameraIp": "192.168.100.99",
"GateIp": "127.0.0.1",
"GatePort": 5000,
"LedIp": "127.0.0.1",
"LedPort": 5001,
"LaneId": "#P-001",
"LaneDescription": "Main Entrance"
}
]