16 changed files with 1332 additions and 52 deletions
Binary file not shown.
@ -0,0 +1,58 @@ |
|||||
|
using System.Data; |
||||
|
using Microsoft.Data.SqlClient; |
||||
|
|
||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public sealed class LoginAccountRepository |
||||
|
{ |
||||
|
private readonly DatabaseSettings _databaseSettings; |
||||
|
private readonly LoginSettings _loginSettings; |
||||
|
|
||||
|
public LoginAccountRepository(DatabaseSettings databaseSettings, LoginSettings loginSettings) |
||||
|
{ |
||||
|
_databaseSettings = databaseSettings; |
||||
|
_loginSettings = loginSettings; |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> ExistsAsync(string loginId, string password) |
||||
|
{ |
||||
|
using var connection = new SqlConnection(CreateConnectionString()); |
||||
|
using var command = connection.CreateCommand(); |
||||
|
|
||||
|
command.CommandText = $@"
|
||||
|
SELECT TOP (1) 1 |
||||
|
FROM {_loginSettings.GetQuotedTableName()} |
||||
|
WHERE {_loginSettings.GetQuotedIdColumn()} = @LoginId |
||||
|
AND {_loginSettings.GetQuotedPasswordColumn()} = @Password;";
|
||||
|
|
||||
|
command.Parameters.Add(new SqlParameter("@LoginId", SqlDbType.NVarChar, 50) { Value = loginId }); |
||||
|
command.Parameters.Add(new SqlParameter("@Password", SqlDbType.NVarChar, 50) { Value = password }); |
||||
|
|
||||
|
await connection.OpenAsync(); |
||||
|
var result = await command.ExecuteScalarAsync(); |
||||
|
return result is not null; |
||||
|
} |
||||
|
|
||||
|
private string CreateConnectionString() |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(_databaseSettings.Ip) || |
||||
|
string.IsNullOrWhiteSpace(_databaseSettings.Database) || |
||||
|
string.IsNullOrWhiteSpace(_databaseSettings.DbId)) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Database.ini의 IP, Database, DbId 값을 확인하세요."); |
||||
|
} |
||||
|
|
||||
|
var builder = new SqlConnectionStringBuilder |
||||
|
{ |
||||
|
DataSource = _databaseSettings.Ip, |
||||
|
InitialCatalog = _databaseSettings.Database, |
||||
|
UserID = _databaseSettings.DbId, |
||||
|
Password = _databaseSettings.DbPw, |
||||
|
Encrypt = _databaseSettings.Encrypt, |
||||
|
TrustServerCertificate = _databaseSettings.TrustServerCertificate, |
||||
|
ConnectTimeout = _databaseSettings.Timeout |
||||
|
}; |
||||
|
|
||||
|
return builder.ConnectionString; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
using System.IO; |
||||
|
using System.Text.Json; |
||||
|
|
||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public sealed class LoginHistoryStore |
||||
|
{ |
||||
|
private const int MaxHistoryCount = 3; |
||||
|
private readonly string _filePath; |
||||
|
private readonly LoginHistoryData _data; |
||||
|
|
||||
|
private LoginHistoryStore(string filePath, LoginHistoryData data) |
||||
|
{ |
||||
|
_filePath = filePath; |
||||
|
_data = data; |
||||
|
} |
||||
|
|
||||
|
public static LoginHistoryStore Load(string filePath) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
if (!File.Exists(filePath)) |
||||
|
{ |
||||
|
return new LoginHistoryStore(filePath, new LoginHistoryData()); |
||||
|
} |
||||
|
|
||||
|
var json = File.ReadAllText(filePath); |
||||
|
var data = JsonSerializer.Deserialize<LoginHistoryData>(json) ?? new LoginHistoryData(); |
||||
|
data.Fields ??= new Dictionary<string, List<string>>(); |
||||
|
return new LoginHistoryStore(filePath, data); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return new LoginHistoryStore(filePath, new LoginHistoryData()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IReadOnlyList<string> GetValues(string fieldName) |
||||
|
{ |
||||
|
return _data.Fields.TryGetValue(fieldName, out var values) |
||||
|
? values.Where(value => !string.IsNullOrWhiteSpace(value)).Take(MaxHistoryCount).ToArray() |
||||
|
: Array.Empty<string>(); |
||||
|
} |
||||
|
|
||||
|
public void Remember(string fieldName, string value) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(value)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!_data.Fields.TryGetValue(fieldName, out var values)) |
||||
|
{ |
||||
|
values = new List<string>(); |
||||
|
_data.Fields[fieldName] = values; |
||||
|
} |
||||
|
|
||||
|
values.RemoveAll(item => string.Equals(item, value, StringComparison.OrdinalIgnoreCase)); |
||||
|
values.Insert(0, value); |
||||
|
if (values.Count > MaxHistoryCount) |
||||
|
{ |
||||
|
values.RemoveRange(MaxHistoryCount, values.Count - MaxHistoryCount); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool Forget(string fieldName, string value) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(value) || |
||||
|
!_data.Fields.TryGetValue(fieldName, out var values)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var removedCount = values.RemoveAll(item => string.Equals(item, value, StringComparison.OrdinalIgnoreCase)); |
||||
|
if (values.Count == 0) |
||||
|
{ |
||||
|
_data.Fields.Remove(fieldName); |
||||
|
} |
||||
|
|
||||
|
return removedCount > 0; |
||||
|
} |
||||
|
|
||||
|
public void Save() |
||||
|
{ |
||||
|
var directory = Path.GetDirectoryName(_filePath); |
||||
|
if (!string.IsNullOrWhiteSpace(directory)) |
||||
|
{ |
||||
|
Directory.CreateDirectory(directory); |
||||
|
} |
||||
|
|
||||
|
var json = JsonSerializer.Serialize(_data, new JsonSerializerOptions { WriteIndented = true }); |
||||
|
File.WriteAllText(_filePath, json); |
||||
|
} |
||||
|
|
||||
|
private sealed class LoginHistoryData |
||||
|
{ |
||||
|
public Dictionary<string, List<string>> Fields { get; set; } = new(StringComparer.OrdinalIgnoreCase); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
using System.IO; |
||||
|
using System.Text.RegularExpressions; |
||||
|
|
||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public sealed class LoginSettings |
||||
|
{ |
||||
|
public string Table { get; set; } = "dbo.Housing_Assembly"; |
||||
|
public string IdColumn { get; set; } = "Operator"; |
||||
|
public string PasswordColumn { get; set; } = "Password"; |
||||
|
public bool OfflinePreview { get; set; } |
||||
|
|
||||
|
public static LoginSettings Load(string filePath) |
||||
|
{ |
||||
|
if (!File.Exists(filePath)) |
||||
|
{ |
||||
|
throw new FileNotFoundException("Database.ini 파일을 찾을 수 없습니다.", filePath); |
||||
|
} |
||||
|
|
||||
|
var values = IniFile.LoadSection(filePath, "Login"); |
||||
|
return new LoginSettings |
||||
|
{ |
||||
|
Table = GetString(values, "Table", "dbo.Housing_Assembly"), |
||||
|
IdColumn = GetString(values, "IdColumn", "Operator"), |
||||
|
PasswordColumn = GetString(values, "PasswordColumn", "Password"), |
||||
|
OfflinePreview = GetBool(values, "OfflinePreview", false) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public string GetQuotedTableName() |
||||
|
{ |
||||
|
var parts = Table |
||||
|
.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) |
||||
|
.Select(QuoteIdentifier) |
||||
|
.ToArray(); |
||||
|
|
||||
|
if (parts.Length is 0 or > 3) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Database.ini [Login] Table 값을 확인하세요."); |
||||
|
} |
||||
|
|
||||
|
return string.Join(".", parts); |
||||
|
} |
||||
|
|
||||
|
public string GetQuotedIdColumn() |
||||
|
{ |
||||
|
return QuoteIdentifier(IdColumn); |
||||
|
} |
||||
|
|
||||
|
public string GetQuotedPasswordColumn() |
||||
|
{ |
||||
|
return QuoteIdentifier(PasswordColumn); |
||||
|
} |
||||
|
|
||||
|
private static string GetString(Dictionary<string, string> values, string key, string defaultValue) |
||||
|
{ |
||||
|
return values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) |
||||
|
? value.Trim() |
||||
|
: defaultValue; |
||||
|
} |
||||
|
|
||||
|
private static bool GetBool(Dictionary<string, string> values, string key, bool defaultValue) |
||||
|
{ |
||||
|
if (!values.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) |
||||
|
{ |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
return value.Trim().ToUpperInvariant() switch |
||||
|
{ |
||||
|
"1" or "TRUE" or "YES" or "ON" => true, |
||||
|
"0" or "FALSE" or "NO" or "OFF" => false, |
||||
|
_ => defaultValue |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static string QuoteIdentifier(string identifier) |
||||
|
{ |
||||
|
if (Regex.IsMatch(identifier, @"^[A-Za-z_][A-Za-z0-9_]*$")) |
||||
|
{ |
||||
|
return $"[{identifier}]"; |
||||
|
} |
||||
|
|
||||
|
throw new InvalidOperationException($"DB 식별자 값이 올바르지 않습니다: {identifier}"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,191 @@ |
|||||
|
using System.Diagnostics; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public sealed class Ni6501StartSignalWatcher |
||||
|
{ |
||||
|
private readonly StartSignalSettings _settings; |
||||
|
|
||||
|
public Ni6501StartSignalWatcher(StartSignalSettings settings) |
||||
|
{ |
||||
|
_settings = settings; |
||||
|
} |
||||
|
|
||||
|
public async Task WaitForStartAsync(CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (!_settings.Enabled) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await Task.Run(() => WaitForStart(cancellationToken), cancellationToken); |
||||
|
} |
||||
|
catch (DllNotFoundException ex) |
||||
|
{ |
||||
|
throw new InvalidOperationException("NI-DAQmx 드라이버(nicaiu.dll)를 찾을 수 없습니다. NI-DAQmx Runtime 설치와 NI-6501 인식 상태를 확인하세요.", ex); |
||||
|
} |
||||
|
catch (EntryPointNotFoundException ex) |
||||
|
{ |
||||
|
throw new InvalidOperationException("NI-DAQmx DLL에서 필요한 함수가 보이지 않습니다. NI-DAQmx Runtime 버전을 확인하세요.", ex); |
||||
|
} |
||||
|
catch (BadImageFormatException ex) |
||||
|
{ |
||||
|
throw new InvalidOperationException("NI-DAQmx DLL 비트 수가 현재 프로그램과 맞지 않습니다. x64/x86 Runtime과 실행 설정을 확인하세요.", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void WaitForStart(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(_settings.PhysicalChannel)) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Hardware.ini [StartSignal] PhysicalChannel 값을 설정하세요."); |
||||
|
} |
||||
|
|
||||
|
using var input = new DaqmxDigitalInput(_settings.PhysicalChannel); |
||||
|
var stopwatch = Stopwatch.StartNew(); |
||||
|
|
||||
|
if (_settings.RequireInactiveBeforeStart) |
||||
|
{ |
||||
|
while (_settings.IsActive(input.ReadSingleLine())) |
||||
|
{ |
||||
|
ThrowIfTimedOut(stopwatch, "기존 신호 해제 대기"); |
||||
|
Delay(cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
while (!_settings.IsActive(input.ReadSingleLine())) |
||||
|
{ |
||||
|
ThrowIfTimedOut(stopwatch, "시작 신호 대기"); |
||||
|
Delay(cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void ThrowIfTimedOut(Stopwatch stopwatch, string state) |
||||
|
{ |
||||
|
if (_settings.TimeoutMilliseconds <= 0 || |
||||
|
stopwatch.ElapsedMilliseconds <= _settings.TimeoutMilliseconds) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
throw new TimeoutException( |
||||
|
$"NI-6501 시작 신호 대기 시간 초과: {state}, 채널={_settings.PhysicalChannel}, 제한={_settings.TimeoutMilliseconds}ms"); |
||||
|
} |
||||
|
|
||||
|
private void Delay(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (cancellationToken.WaitHandle.WaitOne(_settings.PollIntervalMilliseconds)) |
||||
|
{ |
||||
|
throw new OperationCanceledException(cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private sealed class DaqmxDigitalInput : IDisposable |
||||
|
{ |
||||
|
private const int DaqmxValChanPerLine = 0; |
||||
|
private const int DaqmxValGroupByChannel = 0; |
||||
|
|
||||
|
private nint _taskHandle; |
||||
|
|
||||
|
public DaqmxDigitalInput(string physicalChannel) |
||||
|
{ |
||||
|
Check(DaqmxCreateTask("", out _taskHandle), "NI-DAQmx Task 생성 실패"); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
Check(DaqmxCreateDIChan(_taskHandle, physicalChannel, "", DaqmxValChanPerLine), "NI-6501 DI 채널 생성 실패"); |
||||
|
Check(DaqmxStartTask(_taskHandle), "NI-6501 DI Task 시작 실패"); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
Dispose(); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool ReadSingleLine() |
||||
|
{ |
||||
|
var readArray = new byte[1]; |
||||
|
Check( |
||||
|
DaqmxReadDigitalLines( |
||||
|
_taskHandle, |
||||
|
1, |
||||
|
1.0, |
||||
|
DaqmxValGroupByChannel, |
||||
|
readArray, |
||||
|
(uint)readArray.Length, |
||||
|
out _, |
||||
|
out _, |
||||
|
nint.Zero), |
||||
|
"NI-6501 DI 읽기 실패"); |
||||
|
|
||||
|
return readArray[0] != 0; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
if (_taskHandle == nint.Zero) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
DaqmxStopTask(_taskHandle); |
||||
|
DaqmxClearTask(_taskHandle); |
||||
|
_taskHandle = nint.Zero; |
||||
|
} |
||||
|
|
||||
|
private static void Check(int errorCode, string message) |
||||
|
{ |
||||
|
if (errorCode >= 0) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var detail = GetExtendedErrorInfo(); |
||||
|
throw new InvalidOperationException(string.IsNullOrWhiteSpace(detail) |
||||
|
? message |
||||
|
: $"{message}: {detail}"); |
||||
|
} |
||||
|
|
||||
|
private static string GetExtendedErrorInfo() |
||||
|
{ |
||||
|
var errorMessage = new StringBuilder(2048); |
||||
|
var errorCode = DaqmxGetExtendedErrorInfo(errorMessage, (uint)errorMessage.Capacity); |
||||
|
return errorCode == 0 ? errorMessage.ToString().Trim() : string.Empty; |
||||
|
} |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxCreateTask", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
|
private static extern int DaqmxCreateTask(string taskName, out nint taskHandle); |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxCreateDIChan", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
|
private static extern int DaqmxCreateDIChan(nint taskHandle, string lines, string nameToAssignToLines, int lineGrouping); |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxStartTask", CallingConvention = CallingConvention.Cdecl)] |
||||
|
private static extern int DaqmxStartTask(nint taskHandle); |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxReadDigitalLines", CallingConvention = CallingConvention.Cdecl)] |
||||
|
private static extern int DaqmxReadDigitalLines( |
||||
|
nint taskHandle, |
||||
|
int numSampsPerChan, |
||||
|
double timeout, |
||||
|
int fillMode, |
||||
|
byte[] readArray, |
||||
|
uint arraySizeInBytes, |
||||
|
out int sampsPerChanRead, |
||||
|
out int numBytesPerSamp, |
||||
|
nint reserved); |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxStopTask", CallingConvention = CallingConvention.Cdecl)] |
||||
|
private static extern int DaqmxStopTask(nint taskHandle); |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxClearTask", CallingConvention = CallingConvention.Cdecl)] |
||||
|
private static extern int DaqmxClearTask(nint taskHandle); |
||||
|
|
||||
|
[DllImport("nicaiu.dll", EntryPoint = "DAQmxGetExtendedErrorInfo", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] |
||||
|
private static extern int DaqmxGetExtendedErrorInfo(StringBuilder errorString, uint bufferSize); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
using System.IO; |
||||
|
|
||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public sealed class StartSignalSettings |
||||
|
{ |
||||
|
public bool Enabled { get; set; } |
||||
|
public string Connection { get; set; } = "Ni6501"; |
||||
|
public string PhysicalChannel { get; set; } = "Dev1/port0/line0"; |
||||
|
public string ActiveState { get; set; } = "High"; |
||||
|
public string Host { get; set; } = string.Empty; |
||||
|
public int Port { get; set; } = 5000; |
||||
|
public string ReadCommand { get; set; } = string.Empty; |
||||
|
public string ActiveResponse { get; set; } = "START,1,ON"; |
||||
|
public string ResponseMatch { get; set; } = "Contains"; |
||||
|
public string Terminator { get; set; } = "CRLF"; |
||||
|
public int ReadTimeoutMilliseconds { get; set; } = 1000; |
||||
|
public int PollIntervalMilliseconds { get; set; } = 50; |
||||
|
public int TimeoutMilliseconds { get; set; } = 30000; |
||||
|
public int PostSignalDelayMilliseconds { get; set; } = 2000; |
||||
|
public bool RequireInactiveBeforeStart { get; set; } = true; |
||||
|
|
||||
|
public bool IsActive(bool lineValue) |
||||
|
{ |
||||
|
return string.Equals(ActiveState, "Low", StringComparison.OrdinalIgnoreCase) |
||||
|
? !lineValue |
||||
|
: lineValue; |
||||
|
} |
||||
|
|
||||
|
public static StartSignalSettings Load(string filePath) |
||||
|
{ |
||||
|
if (!File.Exists(filePath)) |
||||
|
{ |
||||
|
throw new FileNotFoundException("Hardware.ini 파일을 찾을 수 없습니다.", filePath); |
||||
|
} |
||||
|
|
||||
|
var values = IniFile.LoadSection(filePath, "StartSignal"); |
||||
|
return new StartSignalSettings |
||||
|
{ |
||||
|
Enabled = GetBool(values, "Enabled", false), |
||||
|
Connection = GetString(values, "Connection", "Ni6501"), |
||||
|
PhysicalChannel = GetString(values, "PhysicalChannel", "Dev1/port0/line0"), |
||||
|
ActiveState = GetString(values, "ActiveState", "High"), |
||||
|
Host = GetString(values, "Host", string.Empty), |
||||
|
Port = GetInt(values, "Port", 5000), |
||||
|
ReadCommand = GetString(values, "ReadCommand", string.Empty), |
||||
|
ActiveResponse = GetString(values, "ActiveResponse", "START,1,ON"), |
||||
|
ResponseMatch = GetString(values, "ResponseMatch", "Contains"), |
||||
|
Terminator = GetString(values, "Terminator", "CRLF"), |
||||
|
ReadTimeoutMilliseconds = Math.Max(100, GetInt(values, "ReadTimeoutMilliseconds", 1000)), |
||||
|
PollIntervalMilliseconds = Math.Max(10, GetInt(values, "PollIntervalMilliseconds", 50)), |
||||
|
TimeoutMilliseconds = Math.Max(0, GetInt(values, "TimeoutMilliseconds", 30000)), |
||||
|
PostSignalDelayMilliseconds = Math.Max(0, GetInt(values, "PostSignalDelayMilliseconds", 2000)), |
||||
|
RequireInactiveBeforeStart = GetBool(values, "RequireInactiveBeforeStart", true) |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public string[] GetActiveResponses() |
||||
|
{ |
||||
|
return ActiveResponse |
||||
|
.Split(',', ';') |
||||
|
.Select(value => value.Trim()) |
||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)) |
||||
|
.ToArray(); |
||||
|
} |
||||
|
|
||||
|
public string GetTerminator() |
||||
|
{ |
||||
|
return Terminator.Trim().ToUpperInvariant() switch |
||||
|
{ |
||||
|
"CRLF" => "\r\n", |
||||
|
"CR" => "\r", |
||||
|
"LF" => "\n", |
||||
|
"NONE" or "EMPTY" => string.Empty, |
||||
|
_ => Terminator |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public bool IsActive(string response) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(response)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
var activeResponses = GetActiveResponses(); |
||||
|
if (activeResponses.Length == 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
return string.Equals(ResponseMatch, "Equals", StringComparison.OrdinalIgnoreCase) |
||||
|
? activeResponses.Any(value => string.Equals(response.Trim(), value, StringComparison.OrdinalIgnoreCase)) |
||||
|
: activeResponses.Any(value => response.Contains(value, StringComparison.OrdinalIgnoreCase)); |
||||
|
} |
||||
|
|
||||
|
private static string GetString(Dictionary<string, string> values, string key, string defaultValue) |
||||
|
{ |
||||
|
return values.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) |
||||
|
? value.Trim() |
||||
|
: defaultValue; |
||||
|
} |
||||
|
|
||||
|
private static int GetInt(Dictionary<string, string> values, string key, int defaultValue) |
||||
|
{ |
||||
|
return values.TryGetValue(key, out var value) && int.TryParse(value, out var result) |
||||
|
? result |
||||
|
: defaultValue; |
||||
|
} |
||||
|
|
||||
|
private static bool GetBool(Dictionary<string, string> values, string key, bool defaultValue) |
||||
|
{ |
||||
|
if (!values.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) |
||||
|
{ |
||||
|
return defaultValue; |
||||
|
} |
||||
|
|
||||
|
return value.Trim().ToUpperInvariant() switch |
||||
|
{ |
||||
|
"1" or "TRUE" or "YES" or "ON" => true, |
||||
|
"0" or "FALSE" or "NO" or "OFF" => false, |
||||
|
_ => defaultValue |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public static class StartSignalWatcher |
||||
|
{ |
||||
|
public static Task WaitForStartAsync(StartSignalSettings settings, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (!settings.Enabled) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
var connection = GetConnection(settings); |
||||
|
return connection switch |
||||
|
{ |
||||
|
"NI6501" or "NI-6501" or "NIDAQ" or "DAQ" => new Ni6501StartSignalWatcher(settings).WaitForStartAsync(cancellationToken), |
||||
|
"TCP" or "LAN" or "ETHERNET" => new TcpStartSignalWatcher(settings).WaitForStartAsync(cancellationToken), |
||||
|
_ => throw new InvalidOperationException("Hardware.ini [StartSignal] Connection은 Ni6501 또는 Tcp로 설정하세요.") |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static string Describe(StartSignalSettings settings) |
||||
|
{ |
||||
|
var connection = GetConnection(settings); |
||||
|
return connection switch |
||||
|
{ |
||||
|
"NI6501" or "NI-6501" or "NIDAQ" or "DAQ" => $"NI-6501 {settings.PhysicalChannel}", |
||||
|
"TCP" or "LAN" or "ETHERNET" => string.IsNullOrWhiteSpace(settings.ReadCommand) |
||||
|
? $"LAN {settings.Host}:{settings.Port} 수신 대기" |
||||
|
: $"LAN {settings.Host}:{settings.Port} 명령: {settings.ReadCommand}", |
||||
|
_ => connection |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static string GetConnection(StartSignalSettings settings) |
||||
|
{ |
||||
|
return string.IsNullOrWhiteSpace(settings.Connection) |
||||
|
? "NI6501" |
||||
|
: settings.Connection.Trim().ToUpperInvariant(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
using System.Diagnostics; |
||||
|
using System.Net.Sockets; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Housing.Services; |
||||
|
|
||||
|
public sealed class TcpStartSignalWatcher |
||||
|
{ |
||||
|
private readonly StartSignalSettings _settings; |
||||
|
|
||||
|
public TcpStartSignalWatcher(StartSignalSettings settings) |
||||
|
{ |
||||
|
_settings = settings; |
||||
|
} |
||||
|
|
||||
|
public async Task WaitForStartAsync(CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(_settings.Host)) |
||||
|
{ |
||||
|
throw new InvalidOperationException("Hardware.ini [StartSignal] Host 값을 LAN 장비 IP로 설정하세요."); |
||||
|
} |
||||
|
|
||||
|
using var client = new TcpClient(); |
||||
|
await ConnectAsync(client, cancellationToken); |
||||
|
|
||||
|
using var stream = client.GetStream(); |
||||
|
var stopwatch = Stopwatch.StartNew(); |
||||
|
|
||||
|
if (string.IsNullOrWhiteSpace(_settings.ReadCommand)) |
||||
|
{ |
||||
|
await WaitForEventMessageAsync(stream, stopwatch, cancellationToken); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await WaitForPolledResponseAsync(stream, stopwatch, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
private async Task ConnectAsync(TcpClient client, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); |
||||
|
if (_settings.TimeoutMilliseconds > 0) |
||||
|
{ |
||||
|
timeout.CancelAfter(_settings.TimeoutMilliseconds); |
||||
|
} |
||||
|
|
||||
|
await client.ConnectAsync(_settings.Host, _settings.Port, timeout.Token); |
||||
|
} |
||||
|
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
throw new TimeoutException($"LAN 시작 신호 장비 연결 시간 초과: {_settings.Host}:{_settings.Port}", ex); |
||||
|
} |
||||
|
catch (SocketException ex) |
||||
|
{ |
||||
|
throw new InvalidOperationException($"LAN 시작 신호 장비 연결 실패: {_settings.Host}:{_settings.Port}, {ex.Message}", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task WaitForEventMessageAsync(NetworkStream stream, Stopwatch stopwatch, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
ThrowIfTimedOut(stopwatch, "LAN 메시지 수신 대기"); |
||||
|
|
||||
|
var response = await ReadResponseAsync(stream, cancellationToken); |
||||
|
if (_settings.IsActive(response)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task WaitForPolledResponseAsync(NetworkStream stream, Stopwatch stopwatch, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
if (_settings.RequireInactiveBeforeStart) |
||||
|
{ |
||||
|
while (_settings.IsActive(await QueryAsync(stream, cancellationToken))) |
||||
|
{ |
||||
|
ThrowIfTimedOut(stopwatch, "기존 LAN 시작 상태 해제 대기"); |
||||
|
await DelayAsync(cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
while (!_settings.IsActive(await QueryAsync(stream, cancellationToken))) |
||||
|
{ |
||||
|
ThrowIfTimedOut(stopwatch, "LAN 시작 응답 대기"); |
||||
|
await DelayAsync(cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task<string> QueryAsync(NetworkStream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var command = _settings.ReadCommand.Trim() + _settings.GetTerminator(); |
||||
|
var bytes = Encoding.ASCII.GetBytes(command); |
||||
|
await stream.WriteAsync(bytes, cancellationToken); |
||||
|
await stream.FlushAsync(cancellationToken); |
||||
|
return await ReadResponseAsync(stream, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
private async Task<string> ReadResponseAsync(NetworkStream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var buffer = new byte[1024]; |
||||
|
var response = new StringBuilder(); |
||||
|
|
||||
|
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); |
||||
|
timeout.CancelAfter(_settings.ReadTimeoutMilliseconds); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
var count = await stream.ReadAsync(buffer, timeout.Token); |
||||
|
if (count == 0) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
var chunk = Encoding.ASCII.GetString(buffer, 0, count); |
||||
|
response.Append(chunk); |
||||
|
if (chunk.Contains('\n') || chunk.Contains('\r') || !stream.DataAvailable) |
||||
|
{ |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
return response.ToString(); |
||||
|
} |
||||
|
|
||||
|
return response.ToString(); |
||||
|
} |
||||
|
|
||||
|
private async Task DelayAsync(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
await Task.Delay(_settings.PollIntervalMilliseconds, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
private void ThrowIfTimedOut(Stopwatch stopwatch, string state) |
||||
|
{ |
||||
|
if (_settings.TimeoutMilliseconds <= 0 || |
||||
|
stopwatch.ElapsedMilliseconds <= _settings.TimeoutMilliseconds) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
throw new TimeoutException( |
||||
|
$"LAN 시작 신호 대기 시간 초과: {state}, 대상={_settings.Host}:{_settings.Port}, 제한={_settings.TimeoutMilliseconds}ms"); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue