아모센스 마킹 gui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

454 lines
22 KiB

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace marking_gui.Services
{
/// <summary>
/// DMM(전압+저항, .10) + DMM(전류, .11) + Power Supply(출력제어, .12) SCPI 계측기 연동 서비스.
/// Setting.ini 의 [DMM] / [CurrentDMM] / [PowerSupply] 섹션에서 연결 설정을 읽습니다.
/// </summary>
public sealed class EquipmentMeasurementService : IDisposable
{
// ── 설정 내부 데이터 클래스 ──────────────────────────────────────
private sealed class ScpiChannelSettings
{
public string Connection { get; set; } = string.Empty;
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = 5025;
public string IdnMatch { get; set; } = string.Empty;
public string PortName { get; set; } = string.Empty;
public int BaudRate { get; set; } = 9600;
public string Terminator { get; set; } = "LF";
public string SetupCommand { get; set; } = string.Empty;
public string CleanupCommand { get; set; } = string.Empty;
public string ReadCommand { get; set; } = string.Empty;
public bool IsDisabled =>
string.IsNullOrWhiteSpace(Connection) ||
(Connection.ToUpperInvariant() is not ("LAN" or "TCP" or "ETHERNET" or "SERIAL" or "COM") &&
string.IsNullOrWhiteSpace(Host));
public string[] GetIdnMatches() =>
string.IsNullOrWhiteSpace(IdnMatch)
? Array.Empty<string>()
: IdnMatch.Split(',', StringSplitOptions.RemoveEmptyEntries);
public char GetTerminator() =>
Terminator.ToUpperInvariant() == "CR" ? '\r' : '\n';
}
// ── 필드 ────────────────────────────────────────────────────────
private int _timeoutMs = 5000;
private int _settleMs = 300;
private string _preBoardCommand = string.Empty;
private int _preBoardDelayMs = 2000;
private string _logoutCommand = string.Empty;
private string _resistanceReadCommand = "MEAS:RES?";
private ScpiChannelSettings _voltageDmm; // 전압 측정 전용 DMM 계측기
private ScpiChannelSettings _currentDmm; // 전류 측정 전용 DMM 계측기
private ScpiChannelSettings _resistanceDmm; // 저항 측정 전용 DMM 계측기
private ScpiChannelSettings _psu; // 출력 제어 Power Supply (전원 및 상시전원 인가)
private IScpiClient _voltageDmmClient;
private IScpiClient _currentDmmClient;
private IScpiClient _resistanceDmmClient;
private IScpiClient _psuClient;
private bool _disposed;
// ── 생성자 ──────────────────────────────────────────────────────
public EquipmentMeasurementService(string iniPath)
{
LoadSettings(iniPath);
}
private IScpiClient GetPsuClient()
{
if (_psuClient == null && !_psu.IsDisabled)
{
_psuClient = CreateClient("PSU", _psu);
}
return _psuClient;
}
private IScpiClient GetVoltageClient()
{
if (_voltageDmmClient == null && !_voltageDmm.IsDisabled)
{
_voltageDmmClient = CreateClient("VOLTAGE_DMM", _voltageDmm);
}
return _voltageDmmClient;
}
private IScpiClient GetCurrentDmmClient()
{
if (_currentDmmClient == null && !_currentDmm.IsDisabled)
{
_currentDmmClient = CreateClient("CURRENT_DMM", _currentDmm);
}
return _currentDmmClient;
}
private IScpiClient GetResistanceDmmClient()
{
if (_resistanceDmmClient == null && !_resistanceDmm.IsDisabled)
{
_resistanceDmmClient = CreateClient("RESISTANCE_DMM", _resistanceDmm);
}
// 저항 전용 DMM이 비활성화되어 있는 경우, 전압 DMM 클라이언트를 대체 사용
return _resistanceDmmClient ?? GetVoltageClient();
}
// ── 설정 로드 ────────────────────────────────────────────────────
private void LoadSettings(string iniPath)
{
var values = ParseIni(iniPath);
_timeoutMs = GetInt(values, "Timeout", 5000);
_settleMs = GetInt(values, "SettleMilliseconds", 300);
_preBoardCommand = GetString(values, "PreBoardCommand", string.Empty);
_preBoardDelayMs = GetInt(values, "PreBoardDelayMilliseconds", 2000);
_logoutCommand = GetString(values, "LogoutCommand", string.Empty);
_resistanceReadCommand = GetString(values, "ResistanceReadCommand", "MEAS:RES?");
_voltageDmm = LoadChannel(values, "Voltage", "MEAS:VOLT:DC?", "34460,34461,34465,34470");
_currentDmm = LoadChannel(values, "CurrentDMM", "MEAS:CURR:DC?", "34460,34461,34465,34470");
_resistanceDmm = LoadChannel(values, "ResistanceDMM", "MEAS:RES?", "34460,34461,34465,34470");
_psu = LoadChannel(values, "Current", string.Empty, "E362");
}
private static ScpiChannelSettings LoadChannel(
Dictionary<string, string> values,
string prefix,
string defaultReadCommand,
string defaultIdnMatch)
{
return new ScpiChannelSettings
{
Connection = GetString(values, prefix + "Connection", string.Empty),
Host = GetString(values, prefix + "Host", string.Empty),
Port = GetInt(values, prefix + "Port", 5025),
IdnMatch = GetString(values, prefix + "IdnMatch", defaultIdnMatch),
PortName = GetString(values, prefix + "PortName", string.Empty),
BaudRate = GetInt(values, prefix + "BaudRate", 9600),
Terminator = GetString(values, prefix + "Terminator", "LF"),
SetupCommand = GetString(values, prefix + "SetupCommand", string.Empty),
CleanupCommand = GetString(values, prefix + "CleanupCommand", string.Empty),
ReadCommand = GetString(values, prefix + "ReadCommand", defaultReadCommand),
};
}
// ── Public API ───────────────────────────────────────────────────
/// <summary>
/// 전압 DMM + 저항 DMM + 전류 DMM(또는 PSU) 독립 계측기 3-장비 측정 시퀀스.
/// </summary>
public async Task<(double Voltage, double Current, double Resistance)> MeasureAsync(Action<double?, double?, double?> onUpdate = null)
{
var log = new StringBuilder();
IScpiClient psuClient = GetPsuClient();
IScpiClient voltageClient = GetVoltageClient();
IScpiClient resistanceClient = GetResistanceDmmClient();
IScpiClient currentDmmClient = GetCurrentDmmClient();
try
{
if (psuClient == null)
throw new InvalidOperationException("PSU 파워서플라이 장치가 설정되지 않았거나 비활성화 상태입니다.");
if (voltageClient == null)
throw new InvalidOperationException("전압 측정 DMM 장치가 설정되지 않았거나 비활성화 상태입니다.");
// 1. 파워서플라이(PSU) CH1 출력 ON
log.AppendLine($"> PSU CONNECT ({_psu.Host})");
string psuSetup = !string.IsNullOrWhiteSpace(_psu.SetupCommand) ? _psu.SetupCommand : "OUTP ON, (@1)";
log.AppendLine($"> PSU SETUP: {psuSetup}");
await SendCommandListAsync(psuClient, psuSetup);
// settle 지연 대기
await Task.Delay(_settleMs).ConfigureAwait(false);
// 2. 전압 측정 (전압 전용 계측기에서 MEAS:VOLT:DC? 로 측정)
log.AppendLine($"> 전압 DMM CONNECT ({_voltageDmm.Host})");
if (!string.IsNullOrWhiteSpace(_voltageDmm.SetupCommand))
{
log.AppendLine($"> 전압 DMM SETUP: {_voltageDmm.SetupCommand}");
await SendCommandListAsync(voltageClient, _voltageDmm.SetupCommand);
}
string voltageReadCmd = !string.IsNullOrWhiteSpace(_voltageDmm.ReadCommand) ? _voltageDmm.ReadCommand : "MEAS:VOLT:DC?";
log.AppendLine($"> 전압 DMM SEND: {voltageReadCmd}");
string vResponse = await voltageClient.QueryAsync(voltageReadCmd);
log.AppendLine($" 응답: {vResponse.Trim()}");
double voltage = (double)ExtractFirstDecimal(vResponse, "VOLTAGE");
// 실시간 전압 업데이트
onUpdate?.Invoke(voltage, null, null);
// 3. 저항 측정 (저항 전용 계측기에서 MEAS:RES? 로 측정)
log.AppendLine($"> 저항 DMM CONNECT ({_resistanceDmm.Host ?? _voltageDmm.Host})");
if (resistanceClient != null && !string.IsNullOrWhiteSpace(_resistanceDmm.SetupCommand))
{
log.AppendLine($"> 저항 DMM SETUP: {_resistanceDmm.SetupCommand}");
await SendCommandListAsync(resistanceClient, _resistanceDmm.SetupCommand);
}
string resReadCmd = !string.IsNullOrWhiteSpace(_resistanceDmm.ReadCommand) ? _resistanceDmm.ReadCommand : _resistanceReadCommand;
log.AppendLine($"> 저항 DMM SEND: {resReadCmd}");
string rResponse = await (resistanceClient ?? voltageClient).QueryAsync(resReadCmd);
log.AppendLine($" 응답: {rResponse.Trim()}");
double rawResistance = (double)ExtractFirstDecimal(rResponse, "RESISTANCE");
double resistance = rawResistance / 1000.0; // Ohms -> kOhms 변환 (kΩ 단위 표출)
// 실시간 저항 업데이트
onUpdate?.Invoke(voltage, null, resistance);
// 4. 전류 측정 (파워서플라이 CH1에서 MEAS:CURR? (@1) 로 측정)
log.AppendLine($"> PSU 전류 측정 CONNECT ({_psu.Host})");
string currentReadCmd = !string.IsNullOrWhiteSpace(_psu.ReadCommand) ? _psu.ReadCommand : "MEAS:CURR? (@1)";
log.AppendLine($"> PSU SEND: {currentReadCmd}");
string aResponse = await psuClient.QueryAsync(currentReadCmd);
log.AppendLine($" 응답: {aResponse.Trim()}");
double rawCurrent = (double)ExtractFirstDecimal(aResponse, "CURRENT");
double current = rawCurrent * 1000.0; // Amperes -> mA 변환
// 실시간 전류 및 최종 업데이트
onUpdate?.Invoke(voltage, current, resistance);
return (voltage, current, resistance);
}
finally
{
// 5. 파워서플라이 CH1 출력 OFF
if (psuClient != null)
{
try
{
string psuCleanup = !string.IsNullOrWhiteSpace(_psu.CleanupCommand) ? _psu.CleanupCommand : "OUTP OFF, (@1)";
log.AppendLine($"> PSU CLEANUP: {psuCleanup}");
await SendCommandListAsync(psuClient, psuCleanup);
}
catch (Exception ex)
{
log.AppendLine($"> PSU OUTP OFF FAILED: {ex.Message}");
}
}
LoggerService.Info("[Equipment] 독립 계측기 측정 시퀀스 로그:\n" + log);
}
}
/// <summary>검사 전 보드 초기화 커맨드 실행 (PreBoardCommand).</summary>
public async Task RunPreBoardCommandAsync()
{
if (_psu.IsDisabled)
{
LoggerService.Warn("[Equipment] PreBoard 커맨드 실행 불가: Power Supply 설정이 비활성화되어 있거나 올바르지 않습니다.");
throw new InvalidOperationException("Power Supply 장치가 비활성화 상태이거나 [PowerSupply] 설정이 올바르지 않습니다.");
}
if (string.IsNullOrWhiteSpace(_preBoardCommand))
{
LoggerService.Warn("[Equipment] PreBoard 커맨드 실행 불가: Setting.ini 에 PreBoardCommand 설정이 없습니다.");
throw new InvalidOperationException("Setting.ini [Equipment] 섹션의 PreBoardCommand 설정이 비어있습니다.");
}
var client = GetPsuClient();
if (client == null)
{
LoggerService.Error("[Equipment] PreBoard 커맨드 실행 불가: Power Supply 클라이언트 생성 실패");
throw new InvalidOperationException("Power Supply 클라이언트를 생성할 수 없습니다 (연동 설식을 확인하세요).");
}
LoggerService.Info($"[Equipment] PreBoard 커맨드 실행 시작: {_preBoardCommand}");
await SendCommandListAsync(client, _preBoardCommand);
await Task.Delay(_preBoardDelayMs).ConfigureAwait(false);
LoggerService.Info("[Equipment] PreBoard 커맨드(상시전원 ON) 실행 완료");
}
/// <summary>로그아웃 시 보드 전원 차단 (LogoutCommand).</summary>
public async Task RunLogoutCommandAsync()
{
if (_psu.IsDisabled || string.IsNullOrWhiteSpace(_logoutCommand))
{
LoggerService.Warn("[Equipment] Logout 커맨드 실행 스킵: Power Supply 비활성화 상태이거나 LogoutCommand 설정이 없습니다.");
return;
}
var client = GetPsuClient();
if (client == null)
{
LoggerService.Error("[Equipment] Logout 커맨드 실행 실패: Power Supply 클라이언트 생성 불가");
return;
}
LoggerService.Info($"[Equipment] Logout 커맨드 실행: {_logoutCommand}");
await SendCommandListAsync(client, _logoutCommand);
}
/// <summary>
/// 검사 중단(STOP) 또는 긴급 복구 시 PSU 출력을 안전하게 OFF 합니다.
/// MeasureAsync의 finally와 독립적으로 동작하여, 시퀀스 강제 취소 시에도
/// 파워서플라이 출력이 항상 OFF 상태가 되도록 보장합니다.
/// </summary>
public async Task ResetAsync()
{
if (_psu.IsDisabled)
{
LoggerService.Warn("[Equipment] ResetAsync 스킵: Power Supply 설정이 비활성화 상태입니다.");
return;
}
var client = GetPsuClient();
if (client == null)
{
LoggerService.Warn("[Equipment] ResetAsync 스킵: Power Supply 클라이언트를 생성할 수 없습니다.");
return;
}
try
{
string cleanupCmd = !string.IsNullOrWhiteSpace(_psu.CleanupCommand)
? _psu.CleanupCommand
: "OUTP OFF, (@1)";
LoggerService.Info($"[Equipment] ResetAsync: PSU 출력 OFF 강제 실행 ({cleanupCmd})");
await SendCommandListAsync(client, cleanupCmd);
LoggerService.Info("[Equipment] ResetAsync 완료: PSU 출력 OFF 확인됨");
}
catch (Exception ex)
{
LoggerService.Warn($"[Equipment] ResetAsync 실패 (PSU 통신 오류): {ex.Message}");
}
}
// ── 내부 측정 헬퍼 ──────────────────────────────────────────────
private async Task<double> ReadChannelAsync(
string label,
ScpiChannelSettings ch,
StringBuilder log)
{
if (ch.IsDisabled)
{
log.AppendLine($"> {label} SKIP: Setting.ini 에서 비활성화됨");
return 0.0;
}
if (string.IsNullOrWhiteSpace(ch.ReadCommand))
throw new InvalidOperationException($"Setting.ini [{label}] ReadCommand 값을 확인하세요.");
var client = label == "VOLTAGE" ? GetVoltageClient() : GetPsuClient();
if (client == null) throw new InvalidOperationException($"{label} 장치 클라이언트를 생성할 수 없습니다.");
return (double)await ReadChannelWithClientAsync(label, ch, client, log);
}
private static async Task<decimal> ReadChannelWithClientAsync(
string label,
ScpiChannelSettings ch,
IScpiClient client,
StringBuilder log,
bool skipSetupCommand = false)
{
log.AppendLine($"> {label} CONNECT OK");
if (!skipSetupCommand && !string.IsNullOrWhiteSpace(ch.SetupCommand))
{
log.AppendLine($"> {label} SETUP: {ch.SetupCommand}");
await SendCommandListAsync(client, ch.SetupCommand);
}
log.AppendLine($"> {label} READ: {ch.ReadCommand}");
string response = await client.QueryAsync(ch.ReadCommand);
log.AppendLine($" 응답: {response.Trim()}");
return ExtractFirstDecimal(response, label);
}
private IScpiClient CreateClient(string label, ScpiChannelSettings ch)
{
string conn = ch.Connection.ToUpperInvariant();
if (conn is "LAN" or "TCP" or "ETHERNET")
{
return new TcpScpiClient(ch.Host, ch.Port, _timeoutMs, ch.GetIdnMatches(), ch.GetTerminator());
}
throw new InvalidOperationException(
$"{label} 장비 연결 방식 '{ch.Connection}' 은 지원되지 않습니다. LAN/TCP 만 지원됩니다.");
}
private static async Task SendCommandListAsync(IScpiClient client, string commands)
{
foreach (var cmd in commands.Split(';', StringSplitOptions.RemoveEmptyEntries))
{
string trimmed = cmd.Trim();
if (string.IsNullOrWhiteSpace(trimmed)) continue;
// (@2) 채널에 OUTP OFF 명령은 보호 대상 — 실수로 보드 전원 차단 방지
if (Regex.IsMatch(trimmed, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase))
continue;
await client.SendAsync(trimmed);
await Task.Delay(100).ConfigureAwait(false);
}
}
private static decimal ExtractFirstDecimal(string response, string label)
{
if (string.IsNullOrWhiteSpace(response))
throw new InvalidOperationException($"[{label}] 계측기 응답이 비어있습니다 (응답 수신 실패).");
var match = Regex.Match(response, @"[-+]?\d+(?:\.\d+)?(?:[Ee][-+]?\d+)?");
if (!match.Success)
throw new InvalidOperationException($"[{label}] 수치 파싱 실패. 원본 응답: [{response.Trim()}]");
if (!decimal.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal val))
throw new InvalidOperationException($"[{label}] 수치 변환 실패 (값: '{match.Value}'). 원본 응답: [{response.Trim()}]");
return val;
}
// ── INI 파서 ────────────────────────────────────────────────────
private static Dictionary<string, string> ParseIni(string path)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (!File.Exists(path)) return result;
foreach (var raw in File.ReadAllLines(path))
{
string line = raw.Trim();
if (string.IsNullOrEmpty(line) || line.StartsWith(";") || line.StartsWith("[")) continue;
int eq = line.IndexOf('=');
if (eq <= 0) continue;
string key = line.Substring(0, eq).Trim();
string value = line.Substring(eq + 1).Trim();
result[key] = value;
}
return result;
}
private static string GetString(Dictionary<string, string> d, string key, string def)
=> d.TryGetValue(key, out var v) && !string.IsNullOrWhiteSpace(v) ? v : def;
private static int GetInt(Dictionary<string, string> d, string key, int def)
=> d.TryGetValue(key, out var v) && int.TryParse(v, out int n) ? n : def;
// ── IDisposable ──────────────────────────────────────────────────
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try { _voltageDmmClient?.Dispose(); } catch { }
try { _currentDmmClient?.Dispose(); } catch { }
try { _resistanceDmmClient?.Dispose(); } catch { }
try { _psuClient?.Dispose(); } catch { }
_voltageDmmClient = null;
_currentDmmClient = null;
_resistanceDmmClient = null;
_psuClient = null;
}
}
}