From 442313076e44b1be53de582f055aa7e68db30a1b Mon Sep 17 00:00:00 2001 From: windpacer Date: Wed, 10 Jun 2026 08:12:01 +0900 Subject: [PATCH] =?UTF-8?q?chore:=20=EC=BD=94=EC=96=B4=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0/DB=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8/?= =?UTF-8?q?=EC=84=A4=EC=A0=95/UI=20=EC=85=B8=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .mcp.json | 4 +- CLAUDE.md | 122 ++--- src/Core/Application/DTOs/ExperionDtos.cs | 55 +++ src/Core/Domain/Entities/Hc900Entities.cs | 10 +- src/Hc900Crawler/Program.cs | 3 + src/Hc900Crawler/appsettings.json | 9 +- src/Hc900Crawler/wwwroot/index.html | 2 +- src/Infrastructure/Database/Hc900DbContext.cs | 429 ++++++++++++++++-- .../Hc900/Hc900RealtimeService.cs | 2 +- 9 files changed, 522 insertions(+), 114 deletions(-) diff --git a/.mcp.json b/.mcp.json index c7b9322..ebf8ea4 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,8 +1,8 @@ { "mcpServers": { "iiot-rag": { - "command": "/home/windpacer/projects/ExperionCrawler/mcp-server/.venv/bin/python", - "args": ["/home/windpacer/projects/ExperionCrawler/mcp-server/server.py"], + "command": "/home/windpacer/projects/hc900_ax/mcp-server/.venv/bin/python", + "args": ["/home/windpacer/projects/hc900_ax/mcp-server/server.py"], "type": "stdio" } } diff --git a/CLAUDE.md b/CLAUDE.md index e8b1679..5498f05 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +> **πŸ“Œ [`docs/λ² μ΄μ§μ•„ν‚€ν…μ²˜-νƒœκ·Έ-λ””μžμΈ-μ „λ©΄μž¬μ„€κ³„.md`](docs/λ² μ΄μ§μ•„ν‚€ν…μ²˜-νƒœκ·Έ-λ””μžμΈ-μ „λ©΄μž¬μ„€κ³„.md)** λ₯Ό λ¨Όμ € 읽을 것 β€” λͺ¨λ“  μ•„ν‚€ν…μ²˜ κ²°μ • 사항이 μ •μ˜λ˜μ–΄ 있음. ## Project Overview @@ -11,6 +11,13 @@ Before: HC900 ──Modbus TCP──▢ Experion R530 ──OPC UA──▢ Expe After: HC900 ──Modbus TCP──▢ C++ Gateway ──gRPC──▢ HC900Crawler ──▢ PostgreSQL ``` +**핡심 원칙:** +- 단일 μ§„μ‹€ 곡급원 = **`docs/Sinam_Tag_all.xlsx`** (νƒœκ·Έλͺ…Β·μ£Όμ†ŒΒ·λ©”νƒ€λ°μ΄ν„° μ „λΆ€ 포함) +- **λͺ¨λ“  νƒœκ·Έλͺ…은 λŒ€λ¬Έμž** (OPC UA κ°•μ œ μ†Œλ¬Έμž κ·œμΉ™ 폐기) +- **μ»¨νŠΈλ‘€λŸ¬λ³„ 독립 λ§΅** (`register-map-cN.json`) +- Loop 블둝은 **ν†΅μ§Έλ‘œ 읽기** (κ°œλ³„ νŒŒλΌλ―Έν„° κΈˆμ§€) +- `build_register_map.py` ν•˜λ‚˜λ‘œ **λ§΅ 생성 + 메타데이터 적재** λͺ¨λ‘ 처리 + Four active components: - **`industrial-comm/cpp/`** β€” C++ gateway: Modbus TCP poller + gRPC server (`hc900_gateway` binary) - **`src/Hc900Crawler/`** β€” C# .NET 8 ASP.NET Core web app: full monitoring platform (gRPC client + web UI + KB/P&ID/FF) @@ -35,7 +42,7 @@ Produces `build/hc900_gateway` and `build/libcomm_core.so`. Run the gateway: ```bash ./build/hc900_gateway [host] [register-map-path] [poll_ms] -# defaults: 192.168.0.240, docs/register-map.json, 1000 +# defaults: 192.168.0.240, docs/register-map-c3.json, 1000 ``` Log file: `/tmp/hc900_gateway.log`. gRPC listens on `0.0.0.0:50051`. @@ -50,22 +57,23 @@ dotnet run Configuration via `appsettings.json`: `Hc900.GatewayAddress` (default `http://localhost:50051`), `Hc900.PollIntervalMs`, `ConnectionStrings.DefaultConnection` (PostgreSQL, `Search Path=hc900`). Serves web UI at `http://0.0.0.0:5000`. -### Register Map Generation +### Register Map Generation (단일 슀크립트) -Converts HC Designer CSV exports β†’ `docs/register-map.json` used by the gateway at startup: +`docs/Sinam_Tag_all.xlsx` ν•˜λ‚˜λ‘œ λͺ¨λ“  처리: ```bash python3 scripts/build_register_map.py \ - --loop-csv docs/SummaryFucntionBlockReport.csv \ - --signal-csv docs/SignalTags.csv \ - --variable-csv docs/Variables.csv \ - -o docs/register-map.json + --xlsx docs/Sinam_Tag_all.xlsx \ + -o docs/register-map-c3.json \ + --controller C3 \ + [--db-conn "Host=...;Database=hc900;..."] ``` -Load state labels (StatusPoint descriptors from xlsx) into DB: -```bash -python3 scripts/load_state_labels.py -``` +좜λ ₯: + - `register-map-cN.json` β€” κ²Œμ΄νŠΈμ›¨μ΄μš© λ ˆμ§€μŠ€ν„° λ§΅ + - `tag_metadata` upsert (state labels, units, desc, area) + +**더 이상 `load_state_labels.py`λ₯Ό 별도 μ‹€ν–‰ν•  ν•„μš” μ—†μŒ.** ### Test Utilities @@ -74,99 +82,59 @@ python3 scripts/load_state_labels.py python3 test/modbus_sim.py # Read tags directly via Modbus TCP -python3 test/read_tags.py FICQ3101.PV FICQ3101.SP FICQ3101.MODE -python3 test/read_tags.py --port 5020 FICQ3101.PV # against simulator +python3 test/read_tags.py FICQ-6101.PV FICQ-6101.SP FICQ-6101.MODE +python3 test/read_tags.py --port 5020 FICQ-6101.PV # against simulator python3 test/read_tags.py --all --limit 50 - -# pymodbus must be available; it's expected at /tmp/hc900_venv -/tmp/hc900_venv/bin/python3 test/read_tags.py FICQ3101.PV ``` ## Architecture +**λ°˜λ“œμ‹œ [`docs/λ² μ΄μ§μ•„ν‚€ν…μ²˜-νƒœκ·Έ-λ””μžμΈ-μ „λ©΄μž¬μ„€κ³„.md`](docs/λ² μ΄μ§μ•„ν‚€ν…μ²˜-νƒœκ·Έ-λ””μžμΈ-μ „λ©΄μž¬μ„€κ³„.md) λ₯Ό 읽을 것.** μ—¬κΈ°μ„œλŠ” μš”μ•½λ§Œ 기술. + ### C++ Gateway (`industrial-comm/cpp/`) **`Hc900Gateway`** (`src/gateway.cpp`, `include/gateway.h`) is the core class: -- Loads `register-map.json` at startup into `registers_` (vector of `RegisterEntry`) and `tag_index_` (nameβ†’index map) -- Spawns a poll thread (`PollLoop`) that runs `ReadAllRegisters()` every `poll_interval_ms` -- `ReadAllRegisters()` groups consecutive registers into batches of ≀120 and issues one `read_raw()` call per batch (~48 batches total for a full HC900 config, ~117 ms round-trip) -- Cache (`cache_`, protected by `cache_mutex_`) stores `CachedValue` per tag; quality=192=good, quality=0=bad/stale -- `transport_mutex_` serializes all Modbus transport calls between the poll thread and gRPC `WriteTag` handlers +- Loads `register-map-cN.json` at startup into `registers_` and `tag_index_` +- Spawns poll thread (`PollLoop`) β†’ `ReadAllRegisters()` every `poll_interval_ms` +- Groups consecutive registers into batches of **≀120** (FC03), **Loop 블둝은 ν†΅μ§Έλ‘œ 192 regs** +- Cache (`cache_`, protected by `cache_mutex_`); quality=192=good, quality=0=bad/stale +- `transport_mutex_` serializes Modbus calls between poll thread and gRPC `WriteTag` -**Key gRPC operations** (all implemented in `gateway.cpp`): -- `ReadTags` β€” reads from cache, sub-millisecond, no Modbus I/O -- `WriteTag` β€” calls Modbus FC16 directly, then updates cache -- `StreamTags` β€” pushes cache snapshot at requested interval -- `ListTags` β€” returns metadata from in-memory register list -- `HealthCheck` β€” reports connection state, poll count, last poll duration +**Key gRPC operations:** +- `ReadTags` β€” from cache, no Modbus I/O +- `WriteTag` β€” FC16 directly, then update cache +- `StreamTags` β€” push cache at interval +- `ListTags` β€” metadata from register list +- `HealthCheck` β€” connection state, poll count, duration -**`Controller`** (`src/controller.cpp`, `include/controller.hpp`) wraps `ITransport` with typed read/write methods. **`ModbusTCP`** (`src/modbus_tcp.cpp`, `include/modbus_tcp.hpp`) implements the transport. +**`Controller`** β†’ wraps `ITransport` with typed read/write. **`ModbusTCP`** β†’ FC03/FC16. **Codec** β†’ FP_B format. -**Codec** (`src/codec.cpp`, `include/codec.hpp`) handles byte/word order for float32, int32, int64, double. HC900 uses `VendorFormat::HC900_FLOAT` = `{BigEndian, HighFirst, Normal}` (FP B format per manual). +### C# Crawler (`src/Hc900Crawler/`) -**Proto:** The generated files (`gen/modbus_gateway.pb.{cc,h}` and `gen/modbus_gateway.grpc.pb.{cc,h}`) are pre-built and committed. The source proto is at `proto/modbus_gateway.proto`. The C# copy lives at `src/Hc900Crawler/Proto/modbus_gateway.proto` and is compiled by MSBuild via `Grpc.Tools`. - -### C# Crawler (`src/Hc900Crawler/`) β€” 3-Layer Architecture - -**Project layout** (ExperionCrawler νŒ¨ν„΄ 적용): +**Project layout:** ``` src/ Core/ ← Domain entities, interfaces, application services Infrastructure/ ← DB (Hc900DbContext), HC900 services, Control, Kb, Mcp, Trend, Docs - Hc900Crawler/ ← ASP.NET Core web project (Controllers, wwwroot, Program.cs, csproj) -mcp-server/ ← Python MCP server (copied from ExperionCrawler) + Hc900Crawler/ ← ASP.NET Core web project ``` **BackgroundServices:** -- **`Hc900RealtimeService`** (`Infrastructure/Hc900/`) β€” gRPC 폴링 β†’ `hc900.realtime_table` upsert (500 rows/batch). μƒνƒœ λ…ΈμΆœ: `IsConnected`, `PollCount`, `LastPollAt` -- **`Hc900HistoryService`** β€” 60초 μ£ΌκΈ° `realtime_table` β†’ `history_table` μŠ€λƒ…μƒ· (IsConnected 확인 ν›„ μ‹€ν–‰) -- **`Hc900DigitalEventDetectorService`** β€” 1초 주기둜 realtime_table λ³€ν™” 감지 β†’ `event_history_table` 기둝 - -**`Hc900GatewayClient`** (`Infrastructure/Hc900/`) β€” `IHc900GatewayService` κ΅¬ν˜„. gRPC 채널 lazy 생성. `GetHealthAsync()`, `ListTagsAsync()`, `WriteTagAsync()`. - -**`Hc900WriteService`** β€” FeedforwardSupervisorΒ·FeedforwardControllerμ—μ„œ SP μ“°κΈ°μš©. - -**Value formatting**: μƒνƒœ λ ˆμ΄λΈ” 있으면 `{N | LABEL | }`, μ—†μœΌλ©΄ float/uint16 string. - -**Web API endpoints** (port 5000): -- `GET /api/gateway/health` β€” gRPC HealthCheck -- `GET /api/gateway/tags` β€” ListTags -- `POST /api/gateway/write` β€” WriteTag -- `GET /api/gateway/status` β€” Hc900RealtimeService μƒνƒœ -- `GET /api/realtime/points` β€” realtime_table -- `POST /api/history/query` β€” history_table 쑰회 -- `POST /api/events/query` β€” event_history_table 쑰회 -- `/api/pid/*`, `/api/kb/*`, `/api/ff/*`, `/api/t2s/*`, `/api/ollama/*` λ“± ExperionCrawler 동일 +- `Hc900RealtimeService` β€” gRPC poll β†’ `realtime_table` upsert (500 rows/batch) +- `Hc900HistoryService` β€” 60s snapshot `realtime_table` β†’ `history_table` +- `Hc900DigitalEventDetectorService` β€” 1s digital change detect β†’ `event_history_table` ### Database (PostgreSQL, schema `hc900`) -`Hc900DbContext` (`Infrastructure/Database/Hc900DbContext.cs`) β€” `HasDefaultSchema("hc900")` + `Search Path=hc900` μ—°κ²°λ¬Έμžμ—΄. `InitializeAsync()`μ—μ„œ λͺ¨λ“  ν…Œμ΄λΈ”Β·λ·°Β·TimeScaleDB ν•˜μ΄νΌν…Œμ΄λΈ” μžλ™ 생성. - | Table | Purpose | |---|---| -| `hc900_map_master` | OPC UA `tagname` ↔ `hc900_tag` λ§€ν•‘, Modbus addr, λ°μ΄ν„°νƒ€μž… | | `realtime_table` | μ‹€μ‹œκ°„ κ°’ (tagname, livevalue, timestamp) β€” upsert on conflict | | `history_table` | 60초 이λ ₯ μŠ€λƒ…μƒ· (TimeScaleDB hypertable) | | `event_history_table` | λ””μ§€ν„Έ νƒœκ·Έ μƒνƒœ λ³€κ²½ 이벀트 (TRIP/ALARM/RUN λ“±) | -| `tag_metadata` | νƒœκ·Έ 메타 (description, area, sub_area, state0-7 λ ˆμ΄λΈ”) | -| `pid_equipment`, `pid_prefix_rules` | P&ID μΆ”μΆœ 데이터 | -| `kb_*` | Knowledge Base (Qdrant RAG) | -| `ff_*` | Feedforward μ œμ–΄ μ„€μ •/감사 | - -### Register Map (`docs/register-map.json`) - -JSON file with a top-level `registers` array. Each entry: `tag`, `addr` (0-based Modbus holding register address), `count` (1=uint16, 2=float32), `type`, `access` ("R"/"RW"), `description`. - -Sources: -- **Loops** (PID): `SummaryFucntionBlockReport.csv` β†’ expanded into per-parameter entries using `LOOP_PARAM_OFFSETS` in `build_register_map.py`. Loop #N base address = `0x40 + (N-1)*0x100` (loops 1–24), `0x7840 + (N-25)*0x100` (loops 25–32). -- **Signal Tags** (read-only): `SignalTags.csv`, addresses `0x2000–0x25E4` -- **Variables** (R/W): `Variables.csv`, addresses `0x18C0–0x1A10` - -Float format is `FP_B` (IEEE 754 big-endian, wire order bytes 4,3,2,1). +| `tag_metadata` | νƒœκ·Έ 메타 (desc, area, sub_area, state0-7, units) | +| `hc900_map_master` | ν™œμ„± νƒœκ·Έ 관리 (ν–₯ν›„ λ‹¨μˆœν™” μ˜ˆμ •) | ### HC900 Hardware - Controller: HC900-C70, IP `192.168.0.240`, Modbus TCP port 502 -- Maximum simultaneous connections: 10 (R530 uses 1, leaving 9 available) -- Unit ID: 0x00 (not used in Modbus TCP mode) -- Float format must be set to **FP B** on the controller +- Unit ID: 1, Float format: **FP B** diff --git a/src/Core/Application/DTOs/ExperionDtos.cs b/src/Core/Application/DTOs/ExperionDtos.cs index 93520c1..5999e3a 100644 --- a/src/Core/Application/DTOs/ExperionDtos.cs +++ b/src/Core/Application/DTOs/ExperionDtos.cs @@ -93,6 +93,61 @@ public class PointBuilderApplyDto public List SelectedNodeIds { get; set; } = new(); } +// ── HC900 Point Builder DTOs ───────────────────────────────────────────────── + +public class Hc900PointBuilderGroupDto +{ + public List TagPatterns { get; set; } = new(); + public List ParamTypes { get; set; } = new(); + public string? DataType { get; set; } + public int? LoopNo { get; set; } +} + +public class Hc900PointBuilderBuildDto +{ + public Hc900PointBuilderGroupDto Loop { get; set; } = new(); + public Hc900PointBuilderGroupDto Signal { get; set; } = new(); + public Hc900PointBuilderGroupDto Digital { get; set; } = new(); + public Hc900PointBuilderGroupDto Variable { get; set; } = new(); + public Hc900PointBuilderGroupDto Custom { get; set; } = new(); +} + +public class Hc900PointBuilderPreviewItem +{ + [JsonPropertyName("tagName")] public string TagName { get; set; } = ""; + [JsonPropertyName("hc900Tag")] public string Hc900Tag { get; set; } = ""; + [JsonPropertyName("modbusAddr")] public int ModbusAddr { get; set; } + [JsonPropertyName("paramType")] public string ParamType { get; set; } = ""; + [JsonPropertyName("dataType")] public string DataType { get; set; } = ""; + [JsonPropertyName("loopNo")] public int? LoopNo { get; set; } + [JsonPropertyName("access")] public string Access { get; set; } = "R"; + [JsonPropertyName("controllerId")]public string ControllerId { get; set; } = "HC1"; + [JsonPropertyName("group")] public string Group { get; set; } = ""; + [JsonPropertyName("isActive")] public bool IsActive { get; set; } +} + +public class Hc900PointBuilderPreviewResult +{ + [JsonPropertyName("count")] public int Count { get; set; } + [JsonPropertyName("items")] public List Items { get; set; } = new(); +} + +public class Hc900PointBuilderApplyDto +{ + public List SelectedTagNames { get; set; } = new(); +} + +public class Hc900PointBuilderAddDto +{ + public string TagName { get; set; } = ""; + public string Hc900Tag { get; set; } = ""; + public int ModbusAddr { get; set; } + public string DataType { get; set; } = "float32"; + public string? ParamType { get; set; } + public string Access { get; set; } = "R"; + public string ControllerId { get; set; } = "HC1"; +} + // ── TimeScaleDB ν•˜μ΄νΌν…Œμ΄λΈ” ──────────────────────────────────────────────────── public class HypertableStatusDto diff --git a/src/Core/Domain/Entities/Hc900Entities.cs b/src/Core/Domain/Entities/Hc900Entities.cs index 5d1502f..a4a52cf 100644 --- a/src/Core/Domain/Entities/Hc900Entities.cs +++ b/src/Core/Domain/Entities/Hc900Entities.cs @@ -77,7 +77,7 @@ public class TagMetadata [Column("value")] public string? Value { get; set; } [Column("node_id")] public string? NodeId { get; set; } [Column("loaded_at")] public DateTime LoadedAt { get; set; } = DateTime.UtcNow; - [Column("controller_id")] public string ControllerId { get; set; } = "HC1"; + [Column("controller_id")] public string ControllerId { get; set; } = string.Empty; } // ── Knowledge Base (RAG) ───────────────────────────────────────────────────── @@ -216,4 +216,12 @@ public class Hc900MapEntry [Column("controller_id")] public string ControllerId { get; set; } = "HC1"; + + /// μ‹€μ‹œκ°„ 폴링 λŒ€μƒ μ—¬λΆ€ (Hc900RealtimeService poll λŒ€μƒ ν•„ν„°) + [Column("realtime_enabled")] + public bool RealtimeEnabled { get; set; } = true; + + /// history_table μ•„μΉ΄μ΄λΈŒ λŒ€μƒ μ—¬λΆ€ (SnapshotToHistoryAsync ν•„ν„°) + [Column("archive_enabled")] + public bool ArchiveEnabled { get; set; } = false; } diff --git a/src/Hc900Crawler/Program.cs b/src/Hc900Crawler/Program.cs index 7063dd8..05912a1 100644 --- a/src/Hc900Crawler/Program.cs +++ b/src/Hc900Crawler/Program.cs @@ -106,6 +106,9 @@ builder.Services.AddScoped(); +// λͺ¨λ“ˆ1 shadow κ²€μ¦μš© 인메λͺ¨λ¦¬ 버퍼 +builder.Services.AddSingleton(); // μž‘μ—… B: FEED λž¨ν”„ μ‹€ν–‰κΈ° + μž‘μ—… μ €μž₯μ†Œ builder.Services.AddSingleton(); diff --git a/src/Hc900Crawler/appsettings.json b/src/Hc900Crawler/appsettings.json index d88d33c..87fde6a 100644 --- a/src/Hc900Crawler/appsettings.json +++ b/src/Hc900Crawler/appsettings.json @@ -45,6 +45,13 @@ "McpServer": { "WorkingDirectory": "../../mcp-server" }, + "Sinam": { + "ScriptPath": "scripts/build_register_map_from_sinam.py", + "WorkingDirectory": "/home/windpacer/projects/hc900_ax", + "UploadDir": "docs/uploads", + "OutputDir": "docs", + "ProcessTimeoutSeconds": 120 + }, "DocBrowser": { "Root": "/home/windpacer/projects/hc900_ax", "MaxTextBytes": 2097152, @@ -65,7 +72,7 @@ } }, "SteamAdvisor": { - "ModelPath": "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6111_model.json", + "ModelPath": "/home/windpacer/projects/hc900_ax/scripts/analysis/C-6PG-Dump-20260605111_model.json", "PlotDataDir": "/home/windpacer/projects/hc900_ax/scripts/analysis", "ModelDir": "/home/windpacer/projects/hc900_ax/scripts/analysis", "DefaultColumn": "C-6111", diff --git a/src/Hc900Crawler/wwwroot/index.html b/src/Hc900Crawler/wwwroot/index.html index d575eb2..3e8ceee 100644 --- a/src/Hc900Crawler/wwwroot/index.html +++ b/src/Hc900Crawler/wwwroot/index.html @@ -47,7 +47,7 @@