Initial commit: HC900 Crawler

Honeywell HC900을 Modbus TCP로 직접 폴링 → gRPC → C# 크롤러 → PostgreSQL.
기존 Experion OPC UA 데이터 경로를 HC900 직접 통신으로 대체.

- industrial-comm/cpp: C++ Modbus 게이트웨이 (gRPC 서버)
- src: C# .NET 8 ASP.NET Core 크롤러 + 웹 UI (3-Layer)
- mcp-server: Python FastMCP (RAG/NL2SQL/P&ID)
- 다중 컨트롤러(N-Controller) 지원

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
windpacer
2026-06-03 20:28:14 +09:00
commit 16fc7a2598
325 changed files with 126583 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
-- ============================================================================
-- Migration: Add controller_id columns for multi-controller support
-- Run this once against the existing HC900 database.
-- ============================================================================
BEGIN;
-- 1. realtime_table: add controller_id + UNIQUE(controller_id, tagname)
ALTER TABLE realtime_table
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
-- Drop any existing unique constraint on tagname alone (idempotent wrapper)
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'realtime_table'
AND indexname = 'idx_realtime_table_tagname_unique'
) THEN
DROP INDEX IF EXISTS idx_realtime_table_tagname_unique;
END IF;
IF EXISTS (
SELECT 1 FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
WHERE t.relname = 'realtime_table' AND c.contype = 'u'
) THEN
-- There's a unique constraint; we can't easily drop it by name unless we know it.
-- Use a safe approach: drop any unique constraint on tagname
END IF;
END $$;
-- Add the new composite UNIQUE (required for ON CONFLICT (controller_id, tagname) DO UPDATE)
-- Skip if already exists
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_indexes
WHERE tablename = 'realtime_table'
AND indexname = 'idx_realtime_table_ctrl_tag_unique'
) THEN
CREATE UNIQUE INDEX idx_realtime_table_ctrl_tag_unique
ON realtime_table(controller_id, tagname);
END IF;
END $$;
-- 2. history_table: add controller_id with default 'HC1'
ALTER TABLE history_table
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
-- 3. event_history_table: add controller_id
ALTER TABLE event_history_table
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
-- 4. tag_metadata: add controller_id
ALTER TABLE tag_metadata
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
-- 5. hc900_map_master: add controller_id
ALTER TABLE hc900_map_master
ADD COLUMN IF NOT EXISTS controller_id TEXT NOT NULL DEFAULT 'HC1';
-- 6. v_tag_summary: recreate with controller_id
DROP VIEW IF EXISTS v_tag_summary CASCADE;
CREATE VIEW v_tag_summary AS
SELECT
rt_base.base_tag,
pv_rt.livevalue AS pv,
sp_rt.livevalue AS sp,
op_rt.livevalue AS op,
instate0_rt.livevalue AS instate0,
instate1_rt.livevalue AS instate1,
instate2_rt.livevalue AS instate2,
desc_md.value AS description,
area_md.value AS area,
sub_area_md.value AS sub_area,
rt_base.controller_id
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag, controller_id FROM realtime_table) rt_base
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv' AND pv_rt.controller_id = rt_base.controller_id
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp' AND sp_rt.controller_id = rt_base.controller_id
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op' AND op_rt.controller_id = rt_base.controller_id
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0' AND instate0_rt.controller_id = rt_base.controller_id
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1' AND instate1_rt.controller_id = rt_base.controller_id
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2' AND instate2_rt.controller_id = rt_base.controller_id
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
LEFT JOIN tag_metadata sub_area_md ON sub_area_md.base_tag = rt_base.base_tag AND sub_area_md.attribute = 'sub_area';
-- 7. v_plant_running_state: recreate (depends on v_tag_summary)
DROP VIEW IF EXISTS v_plant_running_state;
CREATE VIEW v_plant_running_state AS
WITH pump_state AS (
SELECT
trim(split_part(area, '|', 2)) AS area_code,
area AS area_raw,
base_tag,
pv,
controller_id
FROM v_tag_summary
WHERE area IS NOT NULL
AND (base_tag LIKE 'p-%' OR base_tag LIKE 'vp-%')
AND pv ~ '\|\s*(L-RUN|R-RUN|L-STOP|R-STOP|L-TRIP|R-TRIP)\s*\|'
)
SELECT
area_code,
MAX(area_raw) AS area_raw,
COUNT(*) AS total_pumps,
COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') AS running_pumps,
COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-TRIP\s*\|') AS tripped_pumps,
COUNT(*) FILTER (WHERE pv ~ '\|\s*(L-STOP|R-STOP)\s*\|') AS stopped_pumps,
CASE
WHEN COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') > 0 THEN 'RUNNING'
WHEN COUNT(*) FILTER (WHERE pv ~ '\|\s*[LR]-TRIP\s*\|') > 0 THEN 'TRIPPED'
ELSE 'STOPPED'
END AS status,
array_agg(base_tag) FILTER (WHERE pv ~ '\|\s*[LR]-RUN\s*\|') AS running_pump_tags
FROM pump_state
WHERE area_code IS NOT NULL AND area_code <> ''
GROUP BY area_code
ORDER BY area_code;
COMMIT;