초기 커밋.

This commit is contained in:
logonkhi
2025-06-02 11:41:39 +09:00
parent 9c15a742b6
commit 86eeb40101
16 changed files with 1583 additions and 0 deletions

28
XRServer.sln Normal file
View File

@@ -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

9
XRServer/App.xaml Normal file
View File

@@ -0,0 +1,9 @@
<Application x:Class="XRServer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:XRServer"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>

14
XRServer/App.xaml.cs Normal file
View File

@@ -0,0 +1,14 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace XRServer
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

10
XRServer/AssemblyInfo.cs Normal file
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)
)]

151
XRServer/BaseInfoHandler.cs Normal file
View File

@@ -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
{
/// <summary>
/// /baseinfo 요청을 처리하는 핸들러
/// </summary>
public class BaseInfoHandler : IHttpRequestHandler
{
private SQLiteService sqliteService;
public BaseInfoHandler(SQLiteService sqliteService)
{
this.sqliteService = sqliteService;
}
/// <summary>
/// /baseinfo 요청을 처리하는 메서드
///
/// </summary>
/// <param name="context"></param>
/// <param name="next"></param>
/// <returns></returns>
public async Task Handle(IHttpContext context, Func<Task> 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<SQLiteDataEntity> 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<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("Content-Type", "application/json; charset=utf-8"),
new KeyValuePair<string, string>("Content-Encoding", "gzip"),
new KeyValuePair<string, string>("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<JObject>(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<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("Content-Type", "application/json; charset=utf-8"),
new KeyValuePair<string, string>("Content-Encoding", "gzip"),
new KeyValuePair<string, string>("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;
}
}
}

117
XRServer/DateTimeUtil.cs Normal file
View File

@@ -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
/// <summary>
/// 헝가리 시간
/// </summary>
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;
/// <summary>
/// hungary - utc 시간 차이
/// </summary>
public static TimeSpan UtcHungaryGap { get => (utcHungaryGap == TimeSpan.Zero) ? ToHungaryDateTime(DateTime.UtcNow) - DateTime.UtcNow : utcHungaryGap; }
/// <summary>
/// UTC string을 UTC DateTime으로 변환. yyyy-MM-ddTHH:mm:ss.fffZ 포맷이라 제대로 변환 됨
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static DateTime UtcParse(string s) { return DateTime.Parse(s).ToUniversalTime(); }
/// <summary>
/// string을 DateTime으로 변환
/// </summary>
/// <param name="s"></param>
/// <param name="format"></param>
/// <returns></returns>
public static DateTime Parse(string s, string format) { return DateTime.ParseExact(s, format, CultureInfo.InvariantCulture); }
/// <summary>
/// UTC DateTime을 헝가리 DateTime으로 변환
/// </summary>
/// <param name="utcDateTime"></param>
/// <returns></returns>
public static DateTime ToHungaryDateTime(DateTime utcDateTime)
{
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, hungaryInfo);
}
/// <summary>
/// UTC string을 헝가리 DateTime으로 변환
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static DateTime UtcStringToHungaryDateTime(string s)
{
return ToHungaryDateTime(UtcParse(s));
}
/// <summary>
/// UTC string을 헝가리 Time String으로 변환
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static string UtcStringToHungaryTimeString(string s)
{
return FormatTime(UtcStringToHungaryDateTime(s));
}
/// <summary>
/// UTC DateTime을 한국 DateTime으로 변환
/// </summary>
/// <param name="utcDateTime"></param>
/// <returns></returns>
public static DateTime ToKoreaDateTime(DateTime utcDateTime)
{
return TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, koreadInfo);
}
/// <summary>
/// UTC string을 한국 DateTime으로 변환
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static DateTime UtcStringToKoreaDateTime(string s)
{
return ToKoreaDateTime(UtcParse(s));
}
/// <summary>
/// UTC string을 한국 Time String으로 변환
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
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");
}
}
}

243
XRServer/INIFile.cs Normal file
View File

@@ -0,0 +1,243 @@
using System.Runtime.InteropServices;
using System.Text;
namespace XRServer
{
/// <summary>
/// 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
///
/// </summary>
public class INIFile
{
//*********************************************************************************************************************
//
// properties
//
//*********************************************************************************************************************
/// <summary>
/// ini 파일명을 저장
/// </summary>
private string INIFileName;
/// <summary>
/// ini 파일을 지정하거나 가져올때 쓰는 속성
/// </summary>
public string FileName
{
get { return INIFileName; }
set { INIFileName = value; }
}
//*********************************************************************************************************************
//
// construct
//
//*********************************************************************************************************************
/// <summary>
/// 생성자 : 사용할 ini 파일을 지정
/// </summary>
/// <param name="FileName">사용할 파일명</param>
public INIFile(string FileName)
{
INIFileName = FileName;
}
//*********************************************************************************************************************
//
// methods
//
//*********************************************************************************************************************
/// <summary>
/// ini 파일에서 정보를 가져오기 위한 API 기초 함수
/// </summary>
[DllImport("kernel32.dll")]
private static extern int GetPrivateProfileString(
string section,
string key,
string def,
StringBuilder retVal,
int size,
string filePath);
/// <summary>
/// ini 파일에서 정보를 쓰기위한 위한 API 기초 함수
/// </summary>
[DllImport("kernel32.dll")]
private static extern long WritePrivateProfileString(
string section,
string key,
string val,
string filePath);
/// <summary>
/// ini 파일에 정보를 기록하기 위한 함수
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <param name="Value">기록할 값</param>
private void _IniWriteValue(string Section, string Key, string Value)
{
WritePrivateProfileString(Section, Key, Value, INIFileName);
}
/// <summary>
/// ini 파일에 정보를 가져오기 위한 함수
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <returns>가져온 값</returns>
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
//
//*********************************************************************************************************************
/// <summary>
/// 문자열 타입으로 값을 기록한다
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <param name="Value">기록 할 문자열</param>
public void SetString(string Section, string Key, string Value)
{
_IniWriteValue(Section, Key, Value.Trim());
}
/// <summary>
/// 정수 타입으로 값을 기록한다
/// </summary>
/// <param name="Section">섹션명 </param>
/// <param name="Key">키 명</param>
/// <param name="Value">기록 할 정수값</param>
///
public void SetInteger(string Section, string Key, int Value)
{
_IniWriteValue(Section, Key, Value.ToString().Trim());
}
/// <summary>
/// 소수 타입으로 값을 기록한다
/// </summary>
/// <param name="Section">섹션명 </param>
/// <param name="Key">키 명</param>
/// <param name="Value">기록 할 소수값</param>
///
public void SetNumber(string Section, string Key, float Value)
{
_IniWriteValue(Section, Key, Value.ToString().Trim());
}
/// <summary>
/// 논리 타입으로 값을 기록 한다.
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <param name="Value">기록 할 논리 값</param>
public void SetBoolean(string Section, string Key, bool Value)
{
_IniWriteValue(Section, Key, Value ? "1" : "0");
}
/// <summary>
/// 논리 타입으로 값을 가져온다
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 값</param>
/// <param name="def">기본값</param>
/// <returns>가져온 논리값</returns>
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;
}
/// <summary>
/// 문자열로 값을 가져 온다
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <returns>가져온 문자열</returns>
public string GetString(string Section, string Key)
{
return _IniReadValue(Section, Key).Trim();
}
/// <summary>
/// 정수 타입으로 값을 가져 온다
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <param name="def">기본값</param>
/// <returns>가져온 정수값</returns>
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;
}
/// <summary>
/// 소수 타입으로 값을 가져 온다
/// </summary>
/// <param name="Section">섹션명</param>
/// <param name="Key">키 명</param>
/// <param name="def">기본값</param>
/// <returns>가져온 소수값</returns>
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;
}
}
}

42
XRServer/Log4net.config Normal file
View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<log4net>
<root>
<level value="ALL"/>
<appender-ref ref="DebugAppender" />
<appender-ref ref="RollingFile" />
<appender-ref ref="FatalRollingFile" />
</root>
<appender name="DebugAppender" type="log4net.Appender.DebugAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread]] %-5level %message%newline" />
</layout>
</appender>
<!-- 지정한 Folder에 Log 파일을 기록 -->
<appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
<file value="log\" />
<datepattern value="yyyy\\MM\\yyyy-MM-dd'.log'"/>
<appendToFile value="true" />
<rollingStyle value="Date" />
<staticLogFileName value="false" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="[%date][Thread : %thread][%level] %message%newline" />
</layout>
</appender>
<appender name="FatalRollingFile" type="log4net.Appender.RollingFileAppender">
<file value="log\fatal.log" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="500" />
<maximumFileSize value="10MB" />
<staticLogFileName value="true" />
<filter type="log4net.Filter.LevelRangeFilter">
<param name="LevelMin" value="FATAL" />
<param name="LevelMax" value="FATAL" />
</filter>
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="[%date][Thread : %thread][%level] %message%newline" />
</layout>
</appender>
</log4net>
</configuration>

153
XRServer/Logger.cs Normal file
View File

@@ -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<logType, Action<string, Exception>> logAction =
new Dictionary<logType, Action<string, Exception>>()
{
{ 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);
}
}

214
XRServer/MQTTService.cs Normal file
View File

@@ -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
{
/// <summary>
/// MQTT 서버 초기화
/// mqttPort 포트에서 실행되며, newXR 토픽에 매 1초당 SQliteSevice의 SelectBySecond 메서드로 데이터를 가져와서 메세지 발송
/// 클라이언트가 시작 시간(00:00~59:59)을 지정할 수 있도록 함
/// 시작 시간이 지정되지 않은 경우 기본값은 00:00
/// 시작 시간부터 1초씩 증가하며, 59:59 이후에는 59:59 데이터를 계속 발송
/// </summary>
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<string, TimeSpan>();
// 클라이언트의 마지막 업데이트 시간을 저장하는 딕셔너리 (클라이언트ID -> DateTime.Now)
var clientLastUpdates = new Dictionary<string, DateTime>();
// 클라이언트 연결 이벤트 처리
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<string, TimeSpan>(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<SQLiteDataEntity> data = await sqliteService.SelectBySecond(currentPlaybackTime, 1);
if (data != null && data.Count > 0)
{
//json 변환
JObject? jsonObject = JsonConvert.DeserializeObject<JObject>(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 서버가 중지되었습니다.");
}
}
}

55
XRServer/MainWindow.xaml Normal file
View File

@@ -0,0 +1,55 @@
<Window x:Class="XRServer.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:local="clr-namespace:XRServer"
mc:Ignorable="d"
Title="XR Server" Height="350" Width="840" Closing="Window_Closing" Loaded="Window_Loaded">
<Grid>
<Button x:Name="startBtn" Content="Start" HorizontalAlignment="Left" Margin="20,222,0,0" VerticalAlignment="Top" Width="247" Height="35" Click="startBtn_Click"/>
<Label Content="HTTP Port" HorizontalAlignment="Left" Margin="20,74,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="httpPortTxt" HorizontalAlignment="Left" Margin="147,80,0,0" TextWrapping="Wrap" Text="8888" VerticalAlignment="Top" Width="120" Height="20" TextChanged="httpPortTxt_TextChanged"/>
<Label Content="MQTT Port" HorizontalAlignment="Left" Margin="20,105,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="mqttPortTxt" HorizontalAlignment="Left" Margin="147,108,0,0" TextWrapping="Wrap" Text="1883" VerticalAlignment="Top" Width="120" Height="20" TextChanged="mqttPortTxt_TextChanged"/>
<Label Content="MQTT Topic" HorizontalAlignment="Left" Margin="20,135,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="mqttTopicTxt" HorizontalAlignment="Left" Margin="147,138,0,0" TextWrapping="Wrap" Text="newXR" VerticalAlignment="Top" Width="120" Height="20" TextChanged="mqttPortTxt_TextChanged"/>
<Label Content="MQTT Start Topic" HorizontalAlignment="Left" Margin="20,165,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="mqttTopicTxt2" HorizontalAlignment="Left" Margin="147,168,0,0" TextWrapping="Wrap" Text="startTime" VerticalAlignment="Top" Width="120" Height="20" TextChanged="mqttPortTxt_TextChanged"/>
<Label Content="DB File" HorizontalAlignment="Left" Margin="20,20,0,0" VerticalAlignment="Top"/>
<TextBox x:Name="dbPathTxt" HorizontalAlignment="Left" Margin="20,43,0,0" VerticalAlignment="Top" TextWrapping="Wrap" Text="" Width="180" Height="20" IsReadOnly="True"/>
<Button x:Name="dbFileLoadBtn" Content="설정" HorizontalAlignment="Left" Margin="212,43,0,0" VerticalAlignment="Top" Width="55" Height="20" Click="dbFileLoadBtn_Click"/>
<ScrollViewer HorizontalAlignment="Left" Margin="300,43,0,0" VerticalAlignment="Top" Width="250" Height="210">
<RichTextBox x:Name="httpLinkTxtBlock" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto"
IsReadOnly="True" IsDocumentEnabled="True">
<FlowDocument>
<Paragraph x:Name="httpPara">
<Run Text="BaseInfo API"/>
<LineBreak />
<Run Text="데이터 요청 mm:ss(분:초)"/>
<LineBreak />
<Hyperlink x:Name="httpLink" NavigateUri="http://localhost:8888/baseinfo/00:00" IsEnabled="False" RequestNavigate="httpLink_RequestNavigate">
http://localhost:8888/baseinfo/00:00
</Hyperlink>
</Paragraph>
</FlowDocument>
</RichTextBox>
</ScrollViewer>
<ScrollViewer HorizontalAlignment="Left" Margin="560,43,0,0" VerticalAlignment="Top" Width="250" Height="210">
<RichTextBox x:Name="mqttLinkTxtBlock" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto"
IsReadOnly="True" IsDocumentEnabled="True">
<FlowDocument>
<Paragraph x:Name="mqttPara">
<Run Text="MQTT API"/>
<LineBreak />
<Run Text="시작시간 설정: startTime 토픽으로 mm:ss(분:초) 전달"/>
<LineBreak />
<Hyperlink x:Name="mqttLink" NavigateUri="mqtt://localhost:1883" IsEnabled="False" RequestNavigate="mqttLink_RequestNavigate">
mqtt://localhost:1883
</Hyperlink>
</Paragraph>
</FlowDocument>
</RichTextBox>
</ScrollViewer>
</Grid>
</Window>

322
XRServer/MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,322 @@
using MQTTnet;
using MQTTnet.Protocol;
using MQTTnet.Server;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Shapes;
using uhttpsharp;
using uhttpsharp.Handlers;
using uhttpsharp.Listeners;
using uhttpsharp.RequestProviders;
using utils;
namespace XRServer
{
/// <summary>
///
/// </summary>
public partial class MainWindow : Window
{
private bool isRunning = false;
private SQLiteService sqliteService = new SQLiteService();
// HTTP 서버 설정
private HttpServer httpServer = new HttpServer(new HttpRequestProvider());
private int httpPort = 8888; // HTTP 서버 포트
private MQTTService mqttService;
private INIFile ini;
private int mqttPort = 1883; // MQTT 서버 포트
private string sqliteDbPath = string.Empty; // SQLite 데이터베이스 경로
public MainWindow()
{
InitializeComponent();
LoadIniFile();
InitHttpServer();
mqttService = new MQTTService(sqliteService);
mqttService.Init();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
/*httpLinkTxtBlock.ContentEnd.InsertLineBreak();
httpLinkTxtBlock.ContentEnd.InsertLineBreak();
httpLinkTxtBlock.ContentEnd.InsertTextInRun($"1");
httpLinkTxtBlock.ContentEnd.InsertLineBreak();
httpLinkTxtBlock.ContentEnd.InsertTextInRun($"2");
httpLinkTxtBlock.ContentEnd.InsertLineBreak();
httpLinkTxtBlock.ContentEnd.InsertTextInRun($"3");*/
}
private void LoadIniFile()
{
// INI 파일 경로 설정
String iniFilePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, System.Diagnostics.Process.GetCurrentProcess().ProcessName + @".ini");
FileInfo f = new FileInfo(iniFilePath);
if (!f.Exists)
{
iniFilePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Application.Current.MainWindow.GetType().Assembly.GetName().Name + @".ini");
}
ini = new INIFile(iniFilePath);
// INI 파일에서 설정 값 읽기
sqliteDbPath = ini.GetString("CONFIG", "DB_PATH").Trim();
dbPathTxt.Text = sqliteDbPath;
if(dbPathTxt.Text == string.Empty || !File.Exists(dbPathTxt.Text))
{
startBtn.IsEnabled = false;
}
}
private void startBtn_Click(object sender, RoutedEventArgs e)
{
StartStop();
}
private async void StartStop()
{
if(sqliteDbPath == string.Empty || !File.Exists(sqliteDbPath))
{
Logger.Debug("SQLite DB 경로가 설정되지 않았습니다. DB File 경로를 확인하세요.");
MessageBox.Show("SQLite DB 경로가 설정되지 않았습니다. DB File 경로를 확인하세요.", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
if (!sqliteService.Connected)
{
sqliteService.Connect(sqliteDbPath);
AppendHttpPathLinkAsync();
AppendMQTTTopicLinkAsync();
}
if (!isRunning)
{
httpPort = int.Parse(httpPortTxt.Text);
mqttPort = int.Parse(mqttPortTxt.Text);
Logger.Debug($"Start Server httpPort:{httpPort} mqttPort:{mqttPort}");
httpServer.Start();
// MQTT 서버 시작
mqttService.SetConfig(mqttPort, mqttTopicTxt.Text, mqttTopicTxt2.Text);
await mqttService.Start();
startBtn.Content = "Stop";
httpPortTxt.IsEnabled = false;
mqttPortTxt.IsEnabled = false;
httpLink.IsEnabled = true;
mqttLink.IsEnabled = true;
mqttTopicTxt.IsEnabled = false;
mqttTopicTxt2.IsEnabled = false;
}
else
{
Logger.Debug("Stop Server");
httpServer.Dispose();
// MQTT 서버 정지
await mqttService.Stop();
Logger.Debug($"MQTT 서버가 포트 {mqttPort}에서 정지되었습니다.");
startBtn.Content = "Start";
httpPortTxt.IsEnabled = true;
mqttPortTxt.IsEnabled = true;
httpLink.IsEnabled = false;
mqttLink.IsEnabled = false;
mqttTopicTxt.IsEnabled = true;
mqttTopicTxt2.IsEnabled = true;
}
isRunning = !isRunning;
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
if (isRunning)
{
Logger.Debug("Stopping server on window close.");
httpServer.Dispose();
mqttService.Stop().Wait();
}
sqliteService.CloseDB();
}
private void InitHttpServer()
{
httpServer.Use(new TcpListenerAdapter(new TcpListener(IPAddress.Loopback, httpPort)));
httpServer.Use(new BaseInfoHandler(sqliteService));
}
private void dbFileLoadBtn_Click(object sender, RoutedEventArgs e)
{
// SQLite 데이터베이스 파일 선택 대화상자 열기
var dialog = new Microsoft.Win32.OpenFileDialog();
dialog.FileName = "SQLite"; // Default file name
dialog.DefaultExt = ".txt"; // Default file extension
dialog.Filter = "SQLite file (.sqlite)|*.sqlite|(.db)|*.db"; // Filter files by extension
// Show open file dialog box
bool? result = dialog.ShowDialog();
// Process open file dialog box results
if (result == true)
{
// Open document
sqliteDbPath = dialog.FileName.Trim();
dbPathTxt.Text = sqliteDbPath;
ini.SetString("CONFIG", "DB_PATH", sqliteDbPath);
startBtn.IsEnabled = dbPathTxt.Text != string.Empty && File.Exists(dbPathTxt.Text);
}
}
private void httpLink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
// 기본 브라우저로 HTTP 링크 열기
var psi = new ProcessStartInfo(e.Uri.AbsoluteUri)
{
UseShellExecute = true
};
Process.Start(psi);
e.Handled = true;
}
private void httpPortTxt_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
if (httpPortTxt.Text == string.Empty || httpLink == null) return;
string url = $"http://localhost:{httpPortTxt.Text}/baseinfo/00:00";
try
{
httpLink.NavigateUri = new Uri(url);
httpLink.Inlines.Clear();
httpLink.Inlines.Add(url);
}
catch (UriFormatException ex)
{
Logger.Debug($"Invalid URL format: {ex.Message}");
httpLink.NavigateUri = null;
httpLink.Inlines.Clear();
httpLink.Inlines.Add("Invalid URL format");
}
}
private void mqttLink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
// 기본 브라우저로 HTTP 링크 열기
var psi = new ProcessStartInfo(e.Uri.AbsoluteUri)
{
UseShellExecute = true
};
Process.Start(psi);
e.Handled = true;
}
private void mqttPortTxt_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
if (mqttPortTxt.Text == string.Empty || mqttLink == null) return;
string url = $"mqtt://localhost:{mqttPortTxt.Text}";
try
{
mqttLink.NavigateUri = new Uri(url);
mqttLink.Inlines.Clear();
mqttLink.Inlines.Add(url);
}
catch (UriFormatException ex)
{
Logger.Debug($"Invalid URL format: {ex.Message}");
mqttLink.NavigateUri = null;
mqttLink.Inlines.Clear();
mqttLink.Inlines.Add("Invalid URL format");
}
}
/// <summary>
/// sqlite에서 baseinfo 데이터를 조회하여 json 첫번째 항목을 확인해서 httpLinkTxtBlock에 추가 표시
/// </summary>
/// <returns></returns>
private async Task AppendHttpPathLinkAsync()
{
// 데이터 조회
List<SQLiteDataEntity> data = await sqliteService.SelectBySecondBaseInfo("00:00");
// 데이터가 null이 아니고, 데이터가 있는지 확인
if (data != null && data.Count > 0)
{
try
{
// Newtonsoft.Json을 사용하여 1-depth 키만 추출
using (StringReader stringReader = new StringReader(data[0].data))
using (JsonTextReader reader = new JsonTextReader(stringReader))
{
JObject jsonObject = JObject.Load(reader);
foreach (var property in jsonObject.Properties())
{
httpPara.Inlines.Add(new LineBreak());
string linkString = $"http://localhost:{httpPort}/{property.Name}/00:00";
Hyperlink hyperLink = new Hyperlink()
{
NavigateUri = new Uri(linkString)
};
hyperLink.Inlines.Add(linkString);
hyperLink.RequestNavigate += httpLink_RequestNavigate;
httpPara.Inlines.Add(hyperLink);
}
}
}
catch (Exception ex)
{
Logger.Debug($"JSON 파싱 오류: {ex.Message}");
}
}
}
private async Task AppendMQTTTopicLinkAsync()
{
// 데이터 조회
List<SQLiteDataEntity> data = await sqliteService.SelectBySecond("00:00", 1);
// 데이터가 null이 아니고, 데이터가 있는지 확인
if (data != null && data.Count > 0)
{
try
{
// Newtonsoft.Json을 사용하여 1-depth 키만 추출
using (StringReader stringReader = new StringReader(data[0].data))
using (JsonTextReader reader = new JsonTextReader(stringReader))
{
JObject jsonObject = JObject.Load(reader);
mqttPara.Inlines.Add(new LineBreak());
mqttPara.Inlines.Add(new Run($"제공하는 Topic명 ------"));
foreach (var property in jsonObject.Properties())
{
mqttPara.Inlines.Add(new LineBreak());
mqttPara.Inlines.Add(new Run(property.Name));
}
}
}
catch (Exception ex)
{
Logger.Debug($"JSON 파싱 오류: {ex.Message}");
}
}
}
}
}

191
XRServer/SQLiteService.cs Normal file
View File

@@ -0,0 +1,191 @@
using Microsoft.VisualBasic;
using SQLite;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using utils;
namespace XRServer
{
public class SQLiteService
{
//#region Singleton
//private static readonly SQLiteService instance = new SQLiteService();
//public static SQLiteService Instance => instance;
//static SQLiteService() { }
//#endregion
private SQLiteConnection? dbConnection;
public bool Connected { get => dbConnection != null; }
public void Connect(string dbFilePath)
{
var _dbPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dbFilePath);
var str = new SQLiteConnectionString(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex, false);
dbConnection = new SQLiteConnection(str);
}
public void CloseDB()
{
dbConnection?.Close();
dbConnection = null;
}
/// <summary>
/// 추가하기
/// </summary>
/// <param name="data"></param>
/// <param name="timeStamp">yyyy-MM-ddTHH:mm:ss.fffZ format string</param>
/// <param name="temp"></param>
/// <returns>데이터베이스에서 추가된 행 수</returns>
public int Insert(string data, string timeStamp, string temp = null)
{
var query = $"INSERT INTO realTime (data, timestamp, temp) VALUES ('{data}', '{timeStamp}', " + (temp == null ? "null" : "'" + temp + "'") + ");";
int changedRowLen = dbConnection!.Execute(query);
return changedRowLen;
}
private string yyyyMMddHH = "";
readonly string[] queryParts =
{
"SELECT * FROM realTime WHERE ",
"timestamp >= '",
"' AND timestamp < '",
"timestamp <= '",
"' AND timestamp > '",
" ORDER BY timestamp ",
" LIMIT ",
};
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// second > 0 : selectTime <= data < selectTime + second
/// second < 0 : selectTime + second < data <= selectTime
/// </summary>
/// <param name="selectTime">mm:ss format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
public async Task<List<SQLiteDataEntity>> SelectBySecond(string selectTime, int second, bool orderAsc = true, int limit = 0)
{
if (yyyyMMddHH.Length == 0)
{
List<SQLiteDataEntity> one = await Task.Run(() =>
{
return dbConnection!.Query<SQLiteDataEntity>("SELECT * FROM realTime LIMIT 1");
});
if (one.Count > 0)
{
yyyyMMddHH = one[0].timestamp.Substring(0, 14);
}
}
selectTime = yyyyMMddHH + selectTime + ".000Z";
StringBuilder queryBuilder = new();
DateTime date = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
string targetTime = DateTimeUtil.FormatTime(date);
//Debug.Log($"SelectBySecondBaseInfo {selectTime} {second} {targetTime} {date}");
queryBuilder.Append(queryParts[0]);
//second가 selectTime 보다 더 미래면
if (second > 0)
{
queryBuilder.Append($"{queryParts[1]}{selectTime}{queryParts[2]}{targetTime}'");
}
else
{
//second가 selectTime 보다 더 과거면
queryBuilder.Append($"{queryParts[3]}{selectTime}{queryParts[4]}{targetTime}'");
}
queryBuilder.Append($"{queryParts[5]}{(orderAsc ? "asc" : "desc")}");
if (limit > 0)
queryBuilder.Append($"{queryParts[6]}{limit}");
queryBuilder.Append(";");
//Debug.Log($"SelectBySecond {query}");
List<SQLiteDataEntity> result = await Task.Run(() =>
{
var query = queryBuilder.ToString();
queryBuilder.Clear();
return dbConnection!.Query<SQLiteDataEntity>(query);
});
return result;
}
private string yyyyMMddHHBaseInfo = "";
/// <summary>
/// selectTime보다 +- second 사이의 데이터 요청. selectTime, second 포함
/// second > 0 : selectTime <= data < selectTime + second
/// second < 0 : selectTime + second < data <= selectTime
/// </summary>
/// <param name="selectTime">mm:ss format string</param>
/// <param name="second"></param>
/// <param name="orderAsc">true: 오래된 시간이 먼저, false: 최근 시간이 먼저</param>
/// <param name="limit"></param>
/// <returns></returns>
public async Task<List<SQLiteDataEntity>> SelectBySecondBaseInfo(string selectTime, int second = 59, bool orderAsc = false, int limit = 1)
{
if(yyyyMMddHHBaseInfo.Length == 0)
{
List<SQLiteDataEntity> one = await Task.Run(() =>
{
return dbConnection.Query<SQLiteDataEntity>("SELECT * FROM baseInfo LIMIT 1;");
});
if(one.Count > 0)
{
yyyyMMddHHBaseInfo = one[0].timestamp.Substring(0, 14);
}
}
Logger.Debug($"yyyyMMddHHBaseInfo1: {yyyyMMddHHBaseInfo}");
selectTime = yyyyMMddHHBaseInfo + selectTime + ".000Z";
Logger.Debug($"yyyyMMddHHBaseInfo2: {yyyyMMddHHBaseInfo}");
StringBuilder queryBuilder = new();
DateTime date = DateTimeUtil.UtcParse(selectTime).AddSeconds(second);
string targetTime = DateTimeUtil.FormatTime(date);
queryBuilder.Append($"SELECT * FROM baseInfo WHERE ");
//second가 selectTime 보다 더 미래면
if (second > 0)
{
queryBuilder.Append($"timestamp >= '{selectTime}' AND timestamp < '{targetTime}'");
}
else
{
//second가 selectTime 보다 더 과거면
queryBuilder.Append($"timestamp <= '{selectTime}' AND timestamp > '{targetTime}'");
}
queryBuilder.Append($" ORDER BY timestamp {(orderAsc ? "asc" : "desc")}");
if (limit > 0)
queryBuilder.Append($" LIMIT {limit}");
queryBuilder.Append(";");
List<SQLiteDataEntity> result = await Task.Run(() =>
{
var query = queryBuilder.ToString();
Logger.Debug($"SelectBySecondBaseInfo {query}");
queryBuilder.Clear();
return dbConnection.Query<SQLiteDataEntity>(query);
});
return result;
}
}
[System.Serializable]
public class SQLiteDataEntity
{
public string data { get; set; }
[PrimaryKey]
public string timestamp { get; set; }
public DateTime timestampHungary { get => DateTimeUtil.UtcStringToHungaryDateTime(timestamp); }
public string temp { get; set; }
}
}

32
XRServer/XRServer.csproj Normal file
View File

@@ -0,0 +1,32 @@
<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;x86</Platforms>
<ApplicationIcon>icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="log4net" Version="3.1.0" />
<PackageReference Include="MQTTnet" Version="5.0.1.1416" />
<PackageReference Include="MQTTnet.Server" Version="5.0.1.1416" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
<PackageReference Include="uhttpsharp.Standard" Version="8.0.30703.1" />
</ItemGroup>
<ItemGroup>
<None Update="Log4net.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

2
XRServer/XRServer.ini Normal file
View File

@@ -0,0 +1,2 @@
[CONFIG]
DB_PATH=

BIN
XRServer/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB