using System; using System.Collections.Generic; using System.IO; // using System.Linq; using System.Text; //using System.Threading; using System.Threading.Tasks; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using Opc.Ua; using Opc.Ua.Client; using Npgsql; // ๐Ÿ‘ˆ PostgreSQL ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€ namespace OpcConnectionTest { public class StatusCodeInfo { public string Name { get; set; } = ""; public string Hex { get; set; } = ""; public ulong Decimal { get; set; } public string Description { get; set; } = ""; } public class TagMaster { public string TagName {get; set; } = string.Empty; public string FullNodeId {get; set; }= string.Empty; public string NodeClass {get; set; }= string.Empty; public string DataType {get; set; }= string.Empty; public int Level {get; set; } } public class HoneywellCrawler { private readonly Session _session; private readonly List _discoveredTags = []; // IDE0028 ์ ์šฉ public HoneywellCrawler(Session session) { // ์˜์กด์„ฑ ์ฃผ์ž… ๋ฐ Null ์ฒดํฌ _session = session ?? throw new ArgumentNullException(nameof(session)); } /// /// ์ €์ธ๋ง ํƒ์‚ฌ ์‹œ์ž‘์  /// public async Task RunAsync(string startNodeId) { Console.WriteLine($"\n๐Ÿš€ ๋น„๋™๊ธฐ ์ €์ธ๋ง ํƒ์‚ฌ ์‹œ์ž‘: {startNodeId}"); try { NodeId rootNode = NodeId.Parse(startNodeId); await BrowseRecursiveAsync(rootNode, 0); Console.WriteLine("\n==============================================="); Console.WriteLine($"โœ… ํƒ์‚ฌ ์™„๋ฃŒ! ์ด {_discoveredTags.Count}๊ฐœ์˜ ํ•ญ๋ชฉ ๋ฐœ๊ฒฌ."); Console.WriteLine("==============================================="); SaveToCsv(); } catch (Exception ex) { Console.WriteLine($"โŒ ์‹คํ–‰ ์ค‘ ์น˜๋ช…์  ์˜ค๋ฅ˜: {ex.Message}"); } } private async Task BrowseRecursiveAsync(NodeId nodeId, int level) { try { BrowseDescription description = new() { NodeId = nodeId, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, NodeClassMask = (uint)(NodeClass.Variable | NodeClass.Object), ResultMask = (uint)BrowseResultMask.All }; // 1. BrowseAsync๋Š” ๊ฐ์ฒด ๋ฐฉ์‹ (์—๋Ÿฌ CS1061/CS8129 ๋ฐฉ์ง€) BrowseResponse response = await _session.BrowseAsync(null, null, 0, [description], default); if (response?.Results == null || response.Results.Count == 0) return; foreach (var result in response.Results) { await ProcessReferencesAsync(result.References, level); byte[] cp = result.ContinuationPoint; while (cp != null && cp.Length > 0) { // 2. BrowseNextAsync๋Š” ํŠœํ”Œ ๋ฐฉ์‹ (์—๋Ÿฌ CS0029 ๋ฐฉ์ง€) var (nextHeader, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, cp, default); await ProcessReferencesAsync(nextRefs, level); cp = nextCp; } } } catch (Exception) { // ํŠน์ • ๋…ธ๋“œ ์ ‘๊ทผ ๊ถŒํ•œ ์—๋Ÿฌ ๋“ฑ์€ ๋ฌด์‹œํ•˜๊ณ  ์ง„ํ–‰ // Console.WriteLine($"โš ๏ธ [Level {level}] {nodeId} ํƒ์ƒ‰ ๊ฑด๋„ˆ๋œ€: {ex.Message}"); } } private async Task ProcessReferencesAsync(ReferenceDescriptionCollection references, int level) { if (references == null || references.Count == 0) return; foreach (var rd in references) { // ExpandedNodeId๋ฅผ ์‹ค์ œ NodeId๋กœ ๋ณ€ํ™˜ (Namespace ๊ด€๋ฆฌ์šฉ) NodeId childId = ExpandedNodeId.ToNodeId(rd.NodeId, _session.NamespaceUris); // ๋งˆ์Šคํ„ฐ ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ _discoveredTags.Add(new TagMaster { TagName = rd.BrowseName.Name ?? "Unknown", FullNodeId = childId.ToString(), NodeClass = rd.NodeClass.ToString(), Level = level }); // ์ฝ˜์†” ์ถœ๋ ฅ (์ง„ํ–‰ ์ƒํ™ฉ ํ™•์ธ์šฉ) string indent = new string(' ', level * 2); Console.WriteLine($"{indent} [{rd.NodeClass}] {rd.BrowseName.Name} (ID: {childId})"); // 3. Object(ํด๋”/ํƒœ๊ทธ)์ธ ๊ฒฝ์šฐ ์žฌ๊ท€ ํƒ์ƒ‰ (ํ•˜๋‹ˆ์›ฐ ๊ตฌ์กฐ์— ๋งž์ถฐ ๊นŠ์ด 5๋‹จ๊ณ„ ์ œํ•œ) if (rd.NodeClass == NodeClass.Object && level < 5) { await BrowseRecursiveAsync(childId, level + 1); } } } private void SaveToCsv() { try { using StreamWriter sw = new("Honeywell_FullMap.csv"); sw.WriteLine("Level,Class,Name,NodeId"); foreach (var tag in _discoveredTags) { sw.WriteLine($"{tag.Level},{tag.NodeClass},{tag.TagName},{tag.FullNodeId}"); } Console.WriteLine($"๐Ÿ’พ CSV ์ €์žฅ ์™„๋ฃŒ: {Path.GetFullPath("Honeywell_FullMap.csv")}"); } catch (Exception ex) { Console.WriteLine($"โŒ CSV ์ €์žฅ ์‹คํŒจ: {ex.Message}"); } } } class Program { static Dictionary _statusCodeMap = new(StringComparer.OrdinalIgnoreCase); // 1. DB ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด (๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ณธ์ธ ์„ค์ •์— ๋งž๊ฒŒ ์ˆ˜์ •ํ•˜์„ธ์š”) static string dbConnString = "Host=localhost;Username=postgres;Password=postgres;Database=opcdb"; static void LoadStatusCodes() { string path = Path.Combine(Directory.GetCurrentDirectory(), "statuscode.json"); if (File.Exists(path)) { try { var json = File.ReadAllText(path); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var list = JsonSerializer.Deserialize>(json, options); if (list != null) { foreach (var item in list) _statusCodeMap[item.Hex] = item; Console.WriteLine($"โœ… {_statusCodeMap.Count}๊ฐœ์˜ ์—๋Ÿฌ ์ฝ”๋“œ ์ •์˜ ๋กœ๋“œ ์™„๋ฃŒ."); } } catch { } } } // 2. DB ์ €์žฅ ํ•จ์ˆ˜ static async Task SaveToDatabase(string tagName, double val, string status) { try { using var conn = new NpgsqlConnection(dbConnString); await conn.OpenAsync(); string sql = "INSERT INTO opc_history (tag_name, tag_value, status_code) VALUES (@tag, @val, @status)"; using var cmd = new NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("tag", tagName); cmd.Parameters.AddWithValue("val", val); cmd.Parameters.AddWithValue("status", status); await cmd.ExecuteNonQueryAsync(); Console.WriteLine("๐Ÿ’พ DB ์ €์žฅ ์™„๋ฃŒ."); } catch (Exception ex) { Console.WriteLine($"โŒ DB ์ €์žฅ ์‹คํŒจ: {ex.Message}"); } } static async Task Main(string[] args) { LoadStatusCodes(); string serverHostName = "192.168.0.20"; string clientHostName = "dbsvr"; string endpointUrl = $"opc.tcp://{serverHostName}:4840"; string applicationUri = $"urn:{clientHostName}:OpcTestClient"; string pfxPath = Path.Combine(Directory.GetCurrentDirectory(), "pki/own/certs/OpcTestClient.pfx"); string pfxPassword = ""; string userName = "mngr"; string password = "mngr"; Directory.CreateDirectory(Path.GetDirectoryName(pfxPath)!); Directory.CreateDirectory("pki/trusted/certs"); Directory.CreateDirectory("pki/issuers/certs"); Directory.CreateDirectory("pki/rejected/certs"); X509Certificate2? clientCert = new X509Certificate2(pfxPath, pfxPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet); var config = new ApplicationConfiguration { ApplicationName = "OpcTestClient", ApplicationType = ApplicationType.Client, ApplicationUri = applicationUri, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { Certificate = clientCert }, TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/trusted") }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/issuers") }, RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = Path.GetFullPath("pki/rejected") }, AutoAcceptUntrustedCertificates = true, AddAppCertToTrustedStore = true }, TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } }; await config.ValidateAsync(ApplicationType.Client); config.CertificateValidator.CertificateValidation += (v, e) => { if (e.Error.StatusCode != StatusCodes.Good) e.Accept = true; }; ISession? session = null; try { Console.WriteLine($"Step 1: Connecting to {endpointUrl}..."); var endpointConfig = EndpointConfiguration.Create(config); using var discovery = await DiscoveryClient.CreateAsync(config, new Uri(endpointUrl), DiagnosticsMasks.All, CancellationToken.None); var endpoints = await discovery.GetEndpointsAsync(null); var selected = endpoints.OrderByDescending(e => e.SecurityLevel) .FirstOrDefault(e => e.SecurityPolicyUri.Contains("Basic256Sha256")) ?? endpoints[0]; var endpoint = new ConfiguredEndpoint(null, selected, endpointConfig); var identity = new UserIdentity(userName, Encoding.UTF8.GetBytes(password)); #pragma warning disable CS0618 session = await Session.Create( config, endpoint, false, "OpcTestSession", 60000, identity, null ); #pragma warning restore CS0618 Console.WriteLine($"โœ… Connected! SessionID: {session.SessionId}"); Console.WriteLine("\n๐Ÿ” ํ•˜๋‹ˆ์›ฐ ์ „์ฒด ๋…ธ๋“œ ํƒ์‚ฌ(์ €์ธ๋ง) ์‹œ์ž‘..."); // ISession์„ ์‹ค์ œ Session ํด๋ž˜์Šค๋กœ ํ˜•๋ณ€ํ™˜ํ•˜์—ฌ ์ „๋‹ฌ if (session is Session clientSession) { HoneywellCrawler crawler = new HoneywellCrawler(clientSession); // 'shinam' ๋…ธ๋“œ๋ฅผ ๊ธฐ์ ์œผ๋กœ ๋ฐ”๋‹ฅ๊นŒ์ง€ ํ›‘๊ณ  CSV ์ €์žฅ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. await crawler.RunAsync("ns=1;s=$assetmodel"); } // 3. ๋ฐ์ดํ„ฐ ์ฝ๊ธฐ ๋ฐ DB ์ €์žฅ ๋ฃจํ”„ (ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ 5ํšŒ ๋ฐ˜๋ณต) string nodeID = "ns=1;s=shinam:p-6102.hzset.fieldvalue"; for (int i = 0; i < 5; i++) { var result = await session.ReadValueAsync(nodeID); double val = Convert.ToDouble(result.Value); string status = result.StatusCode.ToString(); Console.WriteLine($"[{i+1}] TAG: {val} (Status: {status})"); // DB์— ์ €์žฅ await SaveToDatabase("p-6102", val, status); await Task.Delay(2000); // 2์ดˆ ๋Œ€๊ธฐ } } catch (Exception ex) { Console.WriteLine($"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ex.Message}"); } finally { if (session != null) { await session.CloseAsync(); session.Dispose(); } } Console.WriteLine("\n์ž‘์—… ์™„๋ฃŒ. ์—”ํ„ฐ๋ฅผ ๋ˆ„๋ฅด์„ธ์š”..."); Console.ReadLine(); } } }