Files
HC900-Crawler/mcp-server/legend_probe.py
windpacer 16fc7a2598 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>
2026-06-03 20:28:14 +09:00

130 lines
4.3 KiB
Python

"""
범례 심볼 프로브 — 좌표 박스 내 프리미티브 추출 + 정규화 시그니처 도출.
사용:
python3 mcp-server/legend_probe.py XMIN YMIN XMAX YMAX [라벨]
출력:
1) 원시 프리미티브 (LINE/ARC/CIRCLE/LWPOLYLINE)
2) 정규화 시그니처: 앵커=박스 좌하단 기준 상대좌표,
도형종류별 개수, 선분 길이·각도, 공유 정점(연결 토폴로지),
폐합 삼각형 등 패턴 단서
"""
import sys, math, collections
import ezdxf
from ezdxf import recover
DXF = "dxf-graph/No-10_Plant_PID.dxf"
def load():
try:
return ezdxf.readfile(DXF)
except ezdxf.DXFStructureError:
d, _ = recover.readfile(DXF)
return d
def main():
if len(sys.argv) < 5:
print("usage: legend_probe.py XMIN YMIN XMAX YMAX [label]")
sys.exit(1)
x0, y0, x1, y1 = map(float, sys.argv[1:5])
label = sys.argv[5] if len(sys.argv) > 5 else "?"
if x0 > x1:
x0, x1 = x1, x0
if y0 > y1:
y0, y1 = y1, y0
msp = load().modelspace()
def inb(x, y):
return x0 <= x <= x1 and y0 <= y <= y1
lines, arcs, circles, polys, texts = [], [], [], [], []
for e in msp:
t = e.dxftype()
try:
if t == "LINE":
s, en = e.dxf.start, e.dxf.end
if inb(s.x, s.y) or inb(en.x, en.y):
lines.append(((s.x, s.y), (en.x, en.y)))
elif t == "ARC":
c = e.dxf.center
if inb(c.x, c.y):
arcs.append((c.x, c.y, e.dxf.radius,
e.dxf.start_angle, e.dxf.end_angle))
elif t == "CIRCLE":
c = e.dxf.center
if inb(c.x, c.y):
circles.append((c.x, c.y, e.dxf.radius))
elif t == "LWPOLYLINE":
p = [(a, b) for a, b in e.get_points("xy")]
if p and inb(p[0][0], p[0][1]):
polys.append(p)
elif t in ("TEXT", "MTEXT"):
ip = e.dxf.insert
if inb(ip.x, ip.y):
v = (e.plain_text() if t == "MTEXT" else e.dxf.text).strip()
if v:
texts.append((round(ip.x, 1), round(ip.y, 1), v[:40]))
except Exception:
pass
print(f"=== '{label}' box=({x0:.1f},{y0:.1f})-({x1:.1f},{y1:.1f}) ===")
print(f"LINE={len(lines)} ARC={len(arcs)} CIRCLE={len(circles)} "
f"LWPOLY={len(polys)} TEXT={len(texts)}")
if texts:
print("텍스트:", [t[2] for t in texts])
# 앵커 = 비텍스트 프리미티브 최소 x,y
pts = []
for a, b in lines:
pts += [a, b]
for cx, cy, r, *_ in arcs:
pts += [(cx - r, cy - r), (cx + r, cy + r)]
for cx, cy, r in circles:
pts += [(cx - r, cy - r), (cx + r, cy + r)]
for p in polys:
pts += p
if not pts:
print("(비텍스트 프리미티브 없음)")
return
ax = min(p[0] for p in pts)
ay = min(p[1] for p in pts)
w = max(p[0] for p in pts) - ax
h = max(p[1] for p in pts) - ay
print(f"앵커=({ax:.2f},{ay:.2f}) 정규화 bbox= {w:.2f} x {h:.2f}")
def n(x, y):
return (round(x - ax, 2), round(y - ay, 2))
print("\n-- LINE (상대좌표 | 길이 | 각도°) --")
seg = []
for (sx, sy), (ex, ey) in sorted(lines):
ln = math.hypot(ex - sx, ey - sy)
ang = round(math.degrees(math.atan2(ey - sy, ex - sx)) % 180, 1)
print(f" {n(sx,sy)}{n(ex,ey)} len={ln:.2f} ang={ang}")
seg.append((n(sx, sy), n(ex, ey), round(ln, 2), ang))
for cx, cy, r in circles:
print(f"-- CIRCLE c={n(cx,cy)} r={r:.2f}")
for cx, cy, r, sa, ea in arcs:
print(f"-- ARC c={n(cx,cy)} r={r:.2f} {sa:.0f}°→{ea:.0f}°")
for p in polys:
print(f"-- LWPOLY {[n(x,y) for x,y in p]}")
# 공유 정점 (연결 토폴로지) — 0.3u 이내 동일점
vtx = collections.defaultdict(int)
for s, e, *_ in seg:
vtx[s] += 1
vtx[e] += 1
shared = {v: c for v, c in vtx.items() if c >= 3}
print(f"\n공유정점(차수≥3 = apex/junction): {shared}")
ang_hist = collections.Counter(s[3] for s in seg)
print(f"각도 분포: {dict(ang_hist)}")
print(f"선분 길이 분포: {sorted(s[2] for s in seg)}")
if __name__ == "__main__":
main()