From 86eeb40101ecc3917329f002204ae213730bcab6 Mon Sep 17 00:00:00 2001 From: logonkhi Date: Mon, 2 Jun 2025 11:41:39 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EC=BB=A4=EB=B0=8B.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- XRServer.sln | 28 ++++ XRServer/App.xaml | 9 + XRServer/App.xaml.cs | 14 ++ XRServer/AssemblyInfo.cs | 10 ++ XRServer/BaseInfoHandler.cs | 151 +++++++++++++++++ XRServer/DateTimeUtil.cs | 117 +++++++++++++ XRServer/INIFile.cs | 243 +++++++++++++++++++++++++++ XRServer/Log4net.config | 42 +++++ XRServer/Logger.cs | 153 +++++++++++++++++ XRServer/MQTTService.cs | 214 ++++++++++++++++++++++++ XRServer/MainWindow.xaml | 55 ++++++ XRServer/MainWindow.xaml.cs | 322 ++++++++++++++++++++++++++++++++++++ XRServer/SQLiteService.cs | 191 +++++++++++++++++++++ XRServer/XRServer.csproj | 32 ++++ XRServer/XRServer.ini | 2 + XRServer/icon.ico | Bin 0 -> 12185 bytes 16 files changed, 1583 insertions(+) create mode 100644 XRServer.sln create mode 100644 XRServer/App.xaml create mode 100644 XRServer/App.xaml.cs create mode 100644 XRServer/AssemblyInfo.cs create mode 100644 XRServer/BaseInfoHandler.cs create mode 100644 XRServer/DateTimeUtil.cs create mode 100644 XRServer/INIFile.cs create mode 100644 XRServer/Log4net.config create mode 100644 XRServer/Logger.cs create mode 100644 XRServer/MQTTService.cs create mode 100644 XRServer/MainWindow.xaml create mode 100644 XRServer/MainWindow.xaml.cs create mode 100644 XRServer/SQLiteService.cs create mode 100644 XRServer/XRServer.csproj create mode 100644 XRServer/XRServer.ini create mode 100644 XRServer/icon.ico diff --git a/XRServer.sln b/XRServer.sln new file mode 100644 index 0000000..a702e06 --- /dev/null +++ b/XRServer.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XRServer", "XRServer\XRServer.csproj", "{A07DFA95-49EF-4586-A27B-2FB2EA2CD340}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Debug|x86.ActiveCfg = Debug|x86 + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Debug|x86.Build.0 = Debug|x86 + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Release|Any CPU.Build.0 = Release|Any CPU + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Release|x86.ActiveCfg = Release|x86 + {A07DFA95-49EF-4586-A27B-2FB2EA2CD340}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/XRServer/App.xaml b/XRServer/App.xaml new file mode 100644 index 0000000..01df825 --- /dev/null +++ b/XRServer/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/XRServer/App.xaml.cs b/XRServer/App.xaml.cs new file mode 100644 index 0000000..5424718 --- /dev/null +++ b/XRServer/App.xaml.cs @@ -0,0 +1,14 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace XRServer +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } + +} diff --git a/XRServer/AssemblyInfo.cs b/XRServer/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/XRServer/AssemblyInfo.cs @@ -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) +)] diff --git a/XRServer/BaseInfoHandler.cs b/XRServer/BaseInfoHandler.cs new file mode 100644 index 0000000..315cc0f --- /dev/null +++ b/XRServer/BaseInfoHandler.cs @@ -0,0 +1,151 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.RegularExpressions; +using uhttpsharp; +using uhttpsharp.Headers; +using utils; + +namespace XRServer +{ + /// + /// /baseinfo 요청을 처리하는 핸들러 + /// + public class BaseInfoHandler : IHttpRequestHandler + { + + private SQLiteService sqliteService; + public BaseInfoHandler(SQLiteService sqliteService) + { + this.sqliteService = sqliteService; + } + + /// + /// /baseinfo 요청을 처리하는 메서드 + /// + /// + /// + /// + /// + public async Task Handle(IHttpContext context, Func next) + { + // /baseinfo/분:초(mm:ss format) 문자열을 받아 해당 시간에 대한 SelectBySecondBaseInfo 데이터 반환 + string url = context.Request.Uri.OriginalString; + Logger.Debug($"BaseInfoHandler: {url} 요청 처리 중..."); + + //url에 mm:ss 형식이 포함되어 있는지 확인 + Match match = Regex.Match(url, @"([0-5][0-9]):([0-5][0-9])$"); + if (match.Success) + { + // mm:ss 형식이 맞으면 URL에서 mm:ss 부분 추출 + string timePart = url.Substring(match.Index, 5); // URL에서 mm:ss 부분 추출 + + // 데이터 조회 + List data = await sqliteService.SelectBySecondBaseInfo(timePart); + // 데이터가 null이 아니고, 데이터가 있는지 확인 + if (data != null && data.Count > 0) + { + string jsonData = data[0].data; + Logger.Debug($"BaseInfoHandler: {data.Count}개의 데이터 조회됨. 요청 시간: {timePart}"); + + if (url.StartsWith("/baseinfo/", StringComparison.OrdinalIgnoreCase)) + { + var json = data.Count > 0 ? jsonData : "{}"; + + //Logger.Debug($"BaseInfoHandler: 조회된 데이터: {json}"); + // gzip 압축 + byte[] compressedBytes = GzipCompress(json); + + var headers = new List> + { + new KeyValuePair("Content-Type", "application/json; charset=utf-8"), + new KeyValuePair("Content-Encoding", "gzip"), + new KeyValuePair("Content-Length", compressedBytes.Length.ToString()) + }; + + context.Response = new HttpResponse( + HttpResponseCode.Ok, + "application/json; charset=utf-8", + new MemoryStream(compressedBytes), + context.Request.Headers.KeepAliveConnection(), + headers + ); + + } + else + { + JObject? jsonObject = JsonConvert.DeserializeObject(jsonData); + if (jsonObject != null) + { + foreach (var property in jsonObject.Properties()) + { + if (url.StartsWith($"/{property.Name}", StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug($"BaseInfoHandler: {property.Name}:"); + // 속성 값을 문자열로 직렬화 + string propertyValue = property.Value.ToString(Formatting.None); + // gzip 압축 + byte[] compressedBytes = GzipCompress(propertyValue); + + var headers = new List> + { + new KeyValuePair("Content-Type", "application/json; charset=utf-8"), + new KeyValuePair("Content-Encoding", "gzip"), + new KeyValuePair("Content-Length", compressedBytes.Length.ToString()) + }; + context.Response = new HttpResponse( + HttpResponseCode.Ok, + "application/json; charset=utf-8", + new MemoryStream(compressedBytes), + context.Request.Headers.KeepAliveConnection(), + headers + ); + break; + } + } + } + else + { + // JSON 데이터가 비어있는 경우 + context.Response = new HttpResponse(HttpResponseCode.NoContent, "No Content", false); + } + } + } + else + { // 데이터가 없는 경우 + context.Response = new HttpResponse(HttpResponseCode.NotFound, "No data found for the specified time.", false); + } + } + else + { + context.Response = new HttpResponse(HttpResponseCode.BadRequest, "Invalid time format. Use mm:ss.", false); + } + } + + private byte[] GzipCompress(string content) + { + byte[] inputBytes = Encoding.UTF8.GetBytes(content); + using (var outputStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(outputStream, CompressionLevel.Optimal, true)) + { + gzipStream.Write(inputBytes, 0, inputBytes.Length); + } + return outputStream.ToArray(); + } + } + + private MemoryStream StringToStream(string content) + { + MemoryStream memoryStream = new MemoryStream(); + StreamWriter streamWriter = new StreamWriter(memoryStream); + streamWriter.Write(content); + streamWriter.Flush(); + return memoryStream; + } + + + } +} diff --git a/XRServer/DateTimeUtil.cs b/XRServer/DateTimeUtil.cs new file mode 100644 index 0000000..7d9d5f0 --- /dev/null +++ b/XRServer/DateTimeUtil.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XRServer +{ + public class DateTimeUtil + { + //헝가리 서버에서 NQTT 시간이 UTC 시간으로 전송되서 Greenwich Standard Time 로 변경 + private static TimeZoneInfo hungaryInfo = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time"); + private static TimeZoneInfo koreadInfo = TimeZoneInfo.FindSystemTimeZoneById("Korea Standard Time");//korea + + /// + /// 헝가리 시간 + /// + public static DateTime HungaryNow { get => TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.Local, hungaryInfo); } + + public static string HungaryNowString { get => FormatTime(HungaryNow); } + + public static DateTime UtcNow { get => DateTime.UtcNow; } + public static string UtcNowString { get => FormatTime(UtcNow); } + + + private static TimeSpan utcHungaryGap = TimeSpan.Zero; + + /// + /// hungary - utc 시간 차이 + /// + public static TimeSpan UtcHungaryGap { get => (utcHungaryGap == TimeSpan.Zero) ? ToHungaryDateTime(DateTime.UtcNow) - DateTime.UtcNow : utcHungaryGap; } + + /// + /// UTC string을 UTC DateTime으로 변환. yyyy-MM-ddTHH:mm:ss.fffZ 포맷이라 제대로 변환 됨 + /// + /// + /// + public static DateTime UtcParse(string s) { return DateTime.Parse(s).ToUniversalTime(); } + + /// + /// string을 DateTime으로 변환 + /// + /// + /// + /// + public static DateTime Parse(string s, string format) { return DateTime.ParseExact(s, format, CultureInfo.InvariantCulture); } + + + /// + /// UTC DateTime을 헝가리 DateTime으로 변환 + /// + /// + /// + public static DateTime ToHungaryDateTime(DateTime utcDateTime) + { + return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, hungaryInfo); + } + + /// + /// UTC string을 헝가리 DateTime으로 변환 + /// + /// + /// + public static DateTime UtcStringToHungaryDateTime(string s) + { + return ToHungaryDateTime(UtcParse(s)); + } + + /// + /// UTC string을 헝가리 Time String으로 변환 + /// + /// + /// + public static string UtcStringToHungaryTimeString(string s) + { + return FormatTime(UtcStringToHungaryDateTime(s)); + } + + + /// + /// UTC DateTime을 한국 DateTime으로 변환 + /// + /// + /// + public static DateTime ToKoreaDateTime(DateTime utcDateTime) + { + return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, koreadInfo); + } + + /// + /// UTC string을 한국 DateTime으로 변환 + /// + /// + /// + public static DateTime UtcStringToKoreaDateTime(string s) + { + return ToKoreaDateTime(UtcParse(s)); + } + + + /// + /// UTC string을 한국 Time String으로 변환 + /// + /// + /// + public static string UtcStringToKoreaTimeString(string s) + { + return FormatTime(UtcStringToKoreaDateTime(s)); + } + + public static string FormatTime(DateTime dateTime) + { + return dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + } + } +} diff --git a/XRServer/INIFile.cs b/XRServer/INIFile.cs new file mode 100644 index 0000000..5aec69c --- /dev/null +++ b/XRServer/INIFile.cs @@ -0,0 +1,243 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace XRServer +{ + /// + /// INIFile + /// Window INI 파일을 다루기 위한 클레스 + /// + /// // 사용법 예 + /// String path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, System.Diagnostics.Process.GetCurrentProcess().ProcessName + @".ini"); + /// FileInfo f = new FileInfo(path); + /// if (!f.Exists) + /// { + /// path = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Application.Current.MainWindow.GetType().Assembly.GetName().Name + @".ini"); + /// } + /// INIFile ini = new INIFile(path); + /// Config.UPDATE_FILE = ini.GetString("CONFIG", "UPDATE_FILE"); + /// Config.IS_TEST_TOP = ini.GetBoolean("DEBUG", "IS_TEST_TOP", false); + /// + /// // ini 내부 + /// [CONFIG] + /// UPDATE_URL = http://app.stickint.com/samsung/check_update.do + /// LOG_URL = http://app.stickint.com/samsung/upload_log.do + /// + /// [DEBUG] + /// IS_TEST_TOP = false + /// + /// + public class INIFile + { + + //********************************************************************************************************************* + // + // properties + // + //********************************************************************************************************************* + + + /// + /// ini 파일명을 저장 + /// + private string INIFileName; + + + + + /// + /// ini 파일을 지정하거나 가져올때 쓰는 속성 + /// + public string FileName + { + get { return INIFileName; } + set { INIFileName = value; } + } + + //********************************************************************************************************************* + // + // construct + // + //********************************************************************************************************************* + /// + /// 생성자 : 사용할 ini 파일을 지정 + /// + /// 사용할 파일명 + public INIFile(string FileName) + { + INIFileName = FileName; + } + + //********************************************************************************************************************* + // + // methods + // + //********************************************************************************************************************* + + + /// + /// ini 파일에서 정보를 가져오기 위한 API 기초 함수 + /// + [DllImport("kernel32.dll")] + private static extern int GetPrivateProfileString( + string section, + string key, + string def, + StringBuilder retVal, + int size, + string filePath); + /// + /// ini 파일에서 정보를 쓰기위한 위한 API 기초 함수 + /// + [DllImport("kernel32.dll")] + private static extern long WritePrivateProfileString( + string section, + string key, + string val, + string filePath); + /// + /// ini 파일에 정보를 기록하기 위한 함수 + /// + /// 섹션명 + /// 키 명 + /// 기록할 값 + private void _IniWriteValue(string Section, string Key, string Value) + { + WritePrivateProfileString(Section, Key, Value, INIFileName); + } + /// + /// ini 파일에 정보를 가져오기 위한 함수 + /// + /// 섹션명 + /// 키 명 + /// 가져온 값 + private string _IniReadValue(string Section, string Key) + { + StringBuilder temp = new StringBuilder(2000); + int i = GetPrivateProfileString(Section, Key, "", temp, 2000, INIFileName); + return temp.ToString().Trim(); + } + + + //********************************************************************************************************************* + // + // public method + // + //********************************************************************************************************************* + + /// + /// 문자열 타입으로 값을 기록한다 + /// + /// 섹션명 + /// 키 명 + /// 기록 할 문자열 + public void SetString(string Section, string Key, string Value) + { + _IniWriteValue(Section, Key, Value.Trim()); + } + /// + /// 정수 타입으로 값을 기록한다 + /// + /// 섹션명 + /// 키 명 + /// 기록 할 정수값 + /// + public void SetInteger(string Section, string Key, int Value) + { + _IniWriteValue(Section, Key, Value.ToString().Trim()); + } + /// + /// 소수 타입으로 값을 기록한다 + /// + /// 섹션명 + /// 키 명 + /// 기록 할 소수값 + /// + public void SetNumber(string Section, string Key, float Value) + { + _IniWriteValue(Section, Key, Value.ToString().Trim()); + } + + /// + /// 논리 타입으로 값을 기록 한다. + /// + /// 섹션명 + /// 키 명 + /// 기록 할 논리 값 + public void SetBoolean(string Section, string Key, bool Value) + { + _IniWriteValue(Section, Key, Value ? "1" : "0"); + } + /// + /// 논리 타입으로 값을 가져온다 + /// + /// 섹션명 + /// 키 값 + /// 기본값 + /// 가져온 논리값 + public bool GetBoolean(string Section, string Key, bool def) + { + bool temp = def; + string stTemp = _IniReadValue(Section, Key); + if (stTemp == "") return def; + if (stTemp.Trim() == "true" || stTemp.Trim() == "True" || stTemp.Trim() == "1") return true; + else return false; + } + /// + /// 문자열로 값을 가져 온다 + /// + /// 섹션명 + /// 키 명 + /// 가져온 문자열 + public string GetString(string Section, string Key) + { + return _IniReadValue(Section, Key).Trim(); + } + /// + /// 정수 타입으로 값을 가져 온다 + /// + /// 섹션명 + /// 키 명 + /// 기본값 + /// 가져온 정수값 + public int GetInteger(string Section, string Key, int def) + { + int temp = def; + string stTemp = _IniReadValue(Section, Key); + if (stTemp == "") return def; + try + { + temp = int.Parse(stTemp.Trim()); + } + catch (Exception) + { + return def; + } + return temp; + } + + /// + /// 소수 타입으로 값을 가져 온다 + /// + /// 섹션명 + /// 키 명 + /// 기본값 + /// 가져온 소수값 + public float GetNumber(string Section, string Key, float def) + { + float temp = def; + string stTemp = _IniReadValue(Section, Key); + if (stTemp == "") return def; + try + { + temp = float.Parse(stTemp.Trim()); + } + catch (Exception) + { + return def; + } + return temp; + } + + } +} diff --git a/XRServer/Log4net.config b/XRServer/Log4net.config new file mode 100644 index 0000000..4bc5b8c --- /dev/null +++ b/XRServer/Log4net.config @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/XRServer/Logger.cs b/XRServer/Logger.cs new file mode 100644 index 0000000..56e51db --- /dev/null +++ b/XRServer/Logger.cs @@ -0,0 +1,153 @@ +using log4net; +using log4net.Config; +using System.Diagnostics; +using System.IO; +using System.Reflection; + +namespace utils +{ + + public class Logger + { + + //private static readonly log4net.ILog logger = log4net.LogManager.GetLogger("Logger"); + private static readonly ILog logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + + static Logger() + { + try + { + //log4net.Config.XmlConfigurator.Configure(new System.IO.FileInfo("log4netSqlite.config")); + string configFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log4net.config"); + FileInfo configFile = new FileInfo(configFilePath); + if (configFile.Exists) + { + XmlConfigurator.ConfigureAndWatch(configFile); + } + else + { + throw new FileNotFoundException("log4net configuration file not found", configFilePath); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error configuring log4net: {ex.Message}"); + throw; + } + } + + + public static void Info(string msg) + { + + StackFrame frame = (new StackTrace(true)).GetFrame(1); + var method = frame.GetMethod(); + var type = method.DeclaringType; + var name = method.Name; + + int lineNumber = frame.GetFileLineNumber(); + + if (logger.IsInfoEnabled) Log(logType.Info, type.Name + "." + name + " [line " + lineNumber + "] " + msg); + } + + public static void Debug(string msg) + { + StackFrame frame = (new StackTrace(true)).GetFrame(1); + var method = frame.GetMethod(); + var type = method.DeclaringType; + var name = method.Name; + int lineNumber = frame.GetFileLineNumber(); + + if (logger.IsDebugEnabled) Log(logType.Debug, $"{(type.FullName == null ? type.Name : type.FullName)}.{name} [line {lineNumber}] {msg}"); + } + + + public static void Warning(string msg, Exception ex) + { + StackFrame frame = (new StackTrace(true)).GetFrame(1); + var method = frame.GetMethod(); + var type = method.DeclaringType; + var name = method.Name; + int lineNumber = frame.GetFileLineNumber(); + + if (logger.IsWarnEnabled) Log(logType.Warning, type.Name + "." + name + " [line " + lineNumber + "] " + msg, ex: ex); + } + + public static void Error(string msg, Exception ex) + { + StackFrame frame = (new StackTrace(true)).GetFrame(1); + var method = frame.GetMethod(); + var type = method.DeclaringType; + var name = method.Name; + int lineNumber = frame.GetFileLineNumber(); + + if (logger.IsErrorEnabled) Log(logType.Error, type.Name + "." + name + " [line " + lineNumber + "] " + msg, ex: ex); + } + + public static void Fatal(string msg, Exception ex) + { + StackFrame frame = (new StackTrace(true)).GetFrame(1); + var method = frame.GetMethod(); + var type = method.DeclaringType; + var name = method.Name; + int lineNumber = frame.GetFileLineNumber(); + + if (logger.IsFatalEnabled) Log(logType.Fatal, type.Name + "." + name + " [line " + lineNumber + "] " + msg, ex: ex); + } + + // log4net 설정 파일 로드 App.xml.cs OnStartup + //XmlConfigurator.Configure(new System.IO.FileInfo("log4net.config")); + // + //각 클래스 마다 + //static ILog Logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + // + //private static ILog? _log; + //private static ILog log + //{ + // get + // { + // if (_log == null) + // { + // _log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); + // } + // return _log; + // } + //} + + + private enum logType : ushort + { + Debug = 0x10, + Info = 0x01, + Warning = 0x02, + Error = 0x04, + Fatal = 0x08 + } + private static Dictionary> logAction = + new Dictionary>() + { + { logType.Debug, (msg,info) => logger.Debug(msg)}, + { logType.Info, (msg,info) => logger.Info(msg)}, + { logType.Warning, (info,ex) => logger.Warn(info,ex)}, + { logType.Error, (info,ex) => logger.Error(info,ex)}, + { logType.Fatal, (info,ex) => logger.Fatal(info,ex)}, + }; + + private static void Log(logType type, string msg = default(string), string info = default(string), Exception ex = default(Exception)) + { + if ((logType.Error | logType.Warning | logType.Fatal).HasFlag(type)) + //logAction[type](info, ex); + ThreadPool.QueueUserWorkItem(tmp => logAction[type](info, ex)); + else + //logAction[type](msg, null); + ThreadPool.QueueUserWorkItem(tmp => logAction[type](msg, null)); + } + + //static public void Debug(string msg) => Log(logType.Debug, msg); + //static public void Info(string msg) => Log(logType.Info, msg); + //static public void Warning(string info, Exception ex) => Log(logType.Warning, info, ex: ex); + //static public void Error(string info, Exception ex) => Log(logType.Error, info, ex: ex); + //static public void Fatal(string info, Exception ex) => Log(logType.Fatal, info, ex: ex); + + } +} \ No newline at end of file diff --git a/XRServer/MQTTService.cs b/XRServer/MQTTService.cs new file mode 100644 index 0000000..2573afd --- /dev/null +++ b/XRServer/MQTTService.cs @@ -0,0 +1,214 @@ +using MQTTnet; +using MQTTnet.Protocol; +using MQTTnet.Server; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Buffers; +using utils; + +namespace XRServer +{ + /// + /// MQTT 서버 초기화 + /// mqttPort 포트에서 실행되며, newXR 토픽에 매 1초당 SQliteSevice의 SelectBySecond 메서드로 데이터를 가져와서 메세지 발송 + /// 클라이언트가 시작 시간(00:00~59:59)을 지정할 수 있도록 함 + /// 시작 시간이 지정되지 않은 경우 기본값은 00:00 + /// 시작 시간부터 1초씩 증가하며, 59:59 이후에는 59:59 데이터를 계속 발송 + /// + public class MQTTService + { + private MqttServerFactory mqttServerFactory = new MqttServerFactory(); + private MqttServer mqttServer = null; + private System.Timers.Timer timer; + private int mqttPort = 1883; + private string topic = "newXR"; + private string startTimeTopic = "startTime"; + + private SQLiteService sqliteService; + public MQTTService(SQLiteService sqliteService) + { + this.sqliteService = sqliteService; + } + + public void SetConfig(int mqttPort = 1883, string topic = "newXR", string startTimeTopic = "startTime") + { + this.mqttPort = mqttPort; + this.topic = topic; + this.startTimeTopic = startTimeTopic; + } + + + public void Init() + { + // MQTT 서버 옵션 설정 + var optionsBuilder = new MqttServerOptionsBuilder() + .WithDefaultEndpoint() + .WithDefaultEndpointPort(mqttPort); + + // MQTT 서버 생성 + mqttServer = mqttServerFactory.CreateMqttServer(optionsBuilder.Build()); + + // 클라이언트의 현재 재생 시간을 저장할 딕셔너리 (클라이언트ID -> 현재 재생 시간) + var clientPlaybackTimes = new Dictionary(); + + // 클라이언트의 마지막 업데이트 시간을 저장하는 딕셔너리 (클라이언트ID -> DateTime.Now) + var clientLastUpdates = new Dictionary(); + + // 클라이언트 연결 이벤트 처리 + mqttServer.ClientConnectedAsync += e => + { + Logger.Debug($"클라이언트 연결됨: {e.ClientId}"); + // 클라이언트 연결 시 기본 시작 시간 설정 (00:00) + clientPlaybackTimes[e.ClientId] = TimeSpan.Zero; // 00:00 + clientLastUpdates[e.ClientId] = DateTime.Now; + Logger.Debug($"클라이언트 {e.ClientId}의 기본 시작 시간 설정: 00:00"); + return Task.CompletedTask; + }; + + // 클라이언트 메시지 수신 이벤트 처리 + mqttServer.InterceptingPublishAsync += async e => + { + // 클라이언트가 "startTime" 토픽으로 시작 시간을 지정할 경우 + if (e.ApplicationMessage.Topic == startTimeTopic) + { + var startTimePayload = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.Payload.ToArray()); + + // 시간 형식 검증 (00:00~59:59) + if (TimeSpan.TryParseExact(startTimePayload, @"mm\:ss", null, out TimeSpan startTimeSpan)) + { + clientPlaybackTimes[e.ClientId] = startTimeSpan; + clientLastUpdates[e.ClientId] = DateTime.Now; + Logger.Debug($"클라이언트 {e.ClientId}의 시작 시간 설정: {startTimePayload}"); + } + else + { + Logger.Debug($"클라이언트 {e.ClientId}가 잘못된 시간 형식 전송: {startTimePayload}"); + } + } + }; + + // 클라이언트 연결 해제 이벤트 처리 + mqttServer.ClientDisconnectedAsync += e => + { + clientPlaybackTimes.Remove(e.ClientId); + clientLastUpdates.Remove(e.ClientId); + Logger.Debug($"클라이언트 연결 해제: {e.ClientId}"); + return Task.CompletedTask; + }; + + + + // 1초마다 데이터 발행을 위한 타이머 설정 + timer = new System.Timers.Timer(1000); + timer.Elapsed += async (sender, e) => + { + try + { + // 각 클라이언트에 대해 현재 재생 시간에 맞는 데이터 전송 + foreach (var clientEntry in new Dictionary(clientPlaybackTimes)) + { + string clientId = clientEntry.Key; + + // 마지막 업데이트 시간과 현재 시간의 차이 계산 (초 단위) + DateTime now = DateTime.Now; + TimeSpan elapsedSinceUpdate = now - clientLastUpdates[clientId]; + + // 1초 이상 지났으면 클라이언트 재생 시간 업데이트 + if (elapsedSinceUpdate.TotalSeconds >= 1) + { + // 재생 시간 업데이트 (1초 증가) + clientPlaybackTimes[clientId] += TimeSpan.FromSeconds(1); + clientLastUpdates[clientId] = now; + + // 59:59를 넘어가면 59:59로 고정 + if (clientPlaybackTimes[clientId] > TimeSpan.FromMinutes(59).Add(TimeSpan.FromSeconds(59))) + { + clientPlaybackTimes[clientId] = TimeSpan.FromMinutes(59).Add(TimeSpan.FromSeconds(59)); + } + + // 현재 재생 시간으로 mm:ss 형식 문자열 생성 + string currentPlaybackTime = clientPlaybackTimes[clientId].ToString(@"mm\:ss"); + + // SQLiteService를 통해 데이터 가져오기 (현재 재생 시간의 데이터, 1초 분량) + List data = await sqliteService.SelectBySecond(currentPlaybackTime, 1); + + if (data != null && data.Count > 0) + { + //json 변환 + JObject? jsonObject = JsonConvert.DeserializeObject(data[0].data); + if (jsonObject != null) + { + string topicString = ""; + //Dictionary의 모든 키와 값을 키를 topic 값을 payload로 발송 + foreach (var property in jsonObject.Properties()) + { + topicString += property.Name + ", "; + // 속성 값을 문자열로 직렬화 + string propertyValue = property.Value.ToString(Formatting.None); + // 클라이언트별 MQTT 메시지 생성 + var message = new MqttApplicationMessageBuilder() + .WithTopic(property.Name) + .WithPayload(propertyValue) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(false) + .Build(); + + // 메시지 발송 + await mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(message)); + } + Logger.Debug($"클라이언트 {clientId}에 MQTT 메시지 발송: 1개 항목, 토픽: {topicString} 재생시간: {currentPlaybackTime}"); + } + + var messageAll = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(data[0].data) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(false) + .Build(); + Logger.Debug($"클라이언트 {clientId}에 MQTT 메시지 발송: 1개 항목, 토픽: {topic} 재생시간: {currentPlaybackTime}"); + + // 메시지 발송 + await mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(messageAll)); + } + else + { + Logger.Debug($"클라이언트 {clientId}의 재생시간 {currentPlaybackTime}에 해당하는 데이터가 없습니다."); + } + } + } + } + catch (Exception ex) + { + Logger.Debug($"MQTT 메시지 발송 중 오류 발생: {ex.Message}"); + } + }; + + + } + + public async Task Start() + { + if (mqttServer != null) + { + await mqttServer.StartAsync(); + timer.Start(); + Logger.Debug($"MQTT 서버가 시작되었습니다."); + } + else + { + Logger.Debug("MQTT 서버가 초기화되지 않았습니다."); + } + } + + public async Task Stop() + { + timer?.Stop(); + if (mqttServer != null) + { + await mqttServer.StopAsync(); + } + Logger.Debug("MQTT 서버가 중지되었습니다."); + } + + } +} diff --git a/XRServer/MainWindow.xaml b/XRServer/MainWindow.xaml new file mode 100644 index 0000000..800e7a5 --- /dev/null +++ b/XRServer/MainWindow.xaml @@ -0,0 +1,55 @@ + + +