using System.Collections; namespace Housing.Services; public sealed class VisaScpiClient : IScpiClient { private readonly dynamic _io; private VisaScpiClient(dynamic io, string resourceName) { _io = io; ResourceName = resourceName; } public string ResourceName { get; } public static Task OpenAsync(string resourceName, int timeoutMilliseconds) { return Task.Run(() => Open(resourceName, timeoutMilliseconds)); } public static Task OpenMatchingAsync( string resourceName, IEnumerable idnMatches, int timeoutMilliseconds) { return string.IsNullOrWhiteSpace(resourceName) ? Task.Run(() => OpenFirstMatching(idnMatches, timeoutMilliseconds)) : OpenAsync(resourceName, timeoutMilliseconds); } private static VisaScpiClient Open(string resourceName, int timeoutMilliseconds) { if (string.IsNullOrWhiteSpace(resourceName)) { throw new InvalidOperationException("장비 VISA Resource 값을 확인하세요."); } var (resourceManagerType, formattedIoType) = GetVisaComTypes(); dynamic resourceManager = Activator.CreateInstance(resourceManagerType)!; return OpenResource(resourceManager, formattedIoType, resourceName.Trim(), timeoutMilliseconds); } private static VisaScpiClient OpenFirstMatching(IEnumerable idnMatches, int timeoutMilliseconds) { var tokens = NormalizeIdnMatches(idnMatches); var (resourceManagerType, formattedIoType) = GetVisaComTypes(); dynamic resourceManager = Activator.CreateInstance(resourceManagerType)!; var resourceNames = FindResourceNames(resourceManager); if (resourceNames.Length == 0) { throw new InvalidOperationException("VISA 장비를 찾을 수 없습니다. USB/LAN 장비 연결과 VISA 드라이버 설치를 확인하세요."); } var checkedResources = new List(); foreach (var resourceName in resourceNames) { VisaScpiClient? client = null; try { client = OpenResource(resourceManager, formattedIoType, resourceName, timeoutMilliseconds); var idn = client.Query("*IDN?"); checkedResources.Add($"{resourceName}: {idn}"); if (tokens.Length == 0 || tokens.Any(token => idn.Contains(token, StringComparison.OrdinalIgnoreCase))) { return client; } } catch (Exception ex) { checkedResources.Add($"{resourceName}: {ex.Message}"); } client?.Dispose(); } var tokenMessage = tokens.Length == 0 ? string.Empty : $" IDN 키워드: {string.Join(", ", tokens)}."; var checkedMessage = checkedResources.Count == 0 ? string.Empty : $" 확인 결과: {string.Join(" / ", checkedResources)}"; throw new InvalidOperationException($"VISA 장비 자동 검색 실패.{tokenMessage}{checkedMessage}"); } private static (Type ResourceManagerType, Type FormattedIoType) GetVisaComTypes() { var resourceManagerType = Type.GetTypeFromProgID("VISA.GlobalRM") ?? Type.GetTypeFromProgID("VisaComLib.ResourceManager"); var formattedIoType = Type.GetTypeFromProgID("VISA.FormattedIO488") ?? Type.GetTypeFromProgID("VisaComLib.FormattedIO488"); if (resourceManagerType is null || formattedIoType is null) { throw new InvalidOperationException("VISA 라이브러리를 찾을 수 없습니다. Keysight IO Libraries 또는 NI-VISA 설치가 필요합니다."); } return (resourceManagerType, formattedIoType); } private static VisaScpiClient OpenResource( dynamic resourceManager, Type formattedIoType, string resourceName, int timeoutMilliseconds) { dynamic session = resourceManager.Open(resourceName); try { session.Timeout = timeoutMilliseconds; } catch { // Some VISA COM sessions expose timeout through driver-specific properties only. } dynamic formattedIo = Activator.CreateInstance(formattedIoType)!; formattedIo.IO = session; return new VisaScpiClient(formattedIo, resourceName); } private static string[] FindResourceNames(dynamic resourceManager) { var resourceNames = new SortedSet(StringComparer.OrdinalIgnoreCase); foreach (var pattern in new[] { "?*INSTR", "USB?*INSTR", "TCPIP?*INSTR", "GPIB?*INSTR", "ASRL?*INSTR" }) { try { AddResourceNames(resourceNames, resourceManager.FindRsrc(pattern)); } catch { // Resource discovery support differs by VISA COM implementation and backend. } } return resourceNames.ToArray(); } private static void AddResourceNames(ISet resourceNames, object? value) { switch (value) { case null: return; case string resourceName: AddResourceName(resourceNames, resourceName); return; case IEnumerable enumerable: foreach (var item in enumerable) { AddResourceName(resourceNames, item?.ToString()); } return; default: AddResourceName(resourceNames, value.ToString()); return; } } private static void AddResourceName(ISet resourceNames, string? resourceName) { if (!string.IsNullOrWhiteSpace(resourceName)) { resourceNames.Add(resourceName.Trim()); } } private static string[] NormalizeIdnMatches(IEnumerable idnMatches) { return idnMatches .SelectMany(value => value.Split(',', ';')) .Select(value => value.Trim()) .Where(value => !string.IsNullOrWhiteSpace(value)) .ToArray(); } public Task SendAsync(string command) { return Task.Run(() => Send(command)); } public Task QueryAsync(string command) { return Task.Run(() => Query(command)); } private void Send(string command) { _io.WriteString(command.Trim() + "\n", true); } private string Query(string command) { Send(command); return ((string)_io.ReadString()).Trim(); } public void Dispose() { try { _io.IO.Close(); } catch { // Closing is best-effort because VISA COM implementations differ by vendor. } } }