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
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;
|
|
}
|
|
}
|
|
}
|
|
|