feat: 컨트롤러 설정 생성 스크립트 + map_master 로더 + 스캔시간 측정

- build_controllers_config.py: gateway-config.json 생성
- load_map_master.py: hc900_map_master 테이블 적재
- measure_scan_time.py: HC900 스캔 시간 측정 유틸
This commit is contained in:
windpacer
2026-06-04 09:44:45 +09:00
parent b4606fd91d
commit 62a9631fe6
3 changed files with 297 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Build the C# gateway process config (config/gateway-config.json) from the
Experion controller export (Controller.csv).
The crawler's ControllerProcessManager reads this file and launches one
``hc900_gateway`` process per enabled controller:
hc900_gateway <controllerIp> <registerMapPath> <pollIntervalMs> <grpcPort> <controllerPort>
Each controller therefore gets its own gRPC port and its own register map — the
same SignalTag name may exist on several controllers (peer comms) without any
collision, because the namespaces are isolated per process.
Controller.csv has several sections (UniversalModbus, OPC UA, Modicon); we keep
only the HC900 ``UniversalModbusController`` rows and use IPAddress1 (the primary
192.168.0.x net; IPAddress2 is the redundant link).
The existing file's ``shared`` block and any per-controller overrides (grpcPort,
enabled) are preserved when --merge is given.
Usage:
python3 build_controllers_config.py --csv docs/Controller.csv \
-o config/gateway-config.json --merge
"""
import csv
import json
import argparse
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
DEFAULT_SHARED = {
"binaryPath": str(REPO / "industrial-comm/cpp/build/hc900_gateway"),
"ldLibraryPath": "/tmp/grpc_local/usr/lib/aarch64-linux-gnu:/tmp/absl_local/usr/lib/aarch64-linux-gnu",
"logDir": "/tmp",
}
def parse_controllers(csv_path: Path):
"""Yield (name, ip) for each HC900 UniversalModbusController in Controller.csv."""
header = None
with open(csv_path, encoding="utf-8-sig") as f:
for row in csv.reader(f):
if not row or all(not c.strip() for c in row):
continue
if row[0].strip() == "ItemName": # section header
header = {n.strip(): i for i, n in enumerate(row)}
continue
if header is None:
continue
if row[header.get("Class", 1)].strip() != "UniversalModbusController":
continue
name = row[header["ItemName"]].strip()
ip = row[header["IPAddress1"]].strip() if "IPAddress1" in header else ""
if name and ip:
yield name, ip
def build(csv_path: Path, map_dir: str, map_prefix: str, poll_ms: int,
base_grpc_port: int, existing: dict | None) -> dict:
shared = (existing or {}).get("shared") or dict(DEFAULT_SHARED)
# index existing controllers by id to preserve per-controller overrides
prev = {c.get("id"): c for c in (existing or {}).get("controllers", [])}
controllers = []
for i, (name, ip) in enumerate(parse_controllers(csv_path)):
old = prev.get(name, {})
controllers.append({
"id": name,
"name": old.get("name") or f"HC900 {name} Controller",
"controllerIp": ip,
"controllerPort": old.get("controllerPort", 502),
# grpcPort is assigned sequentially to guarantee uniqueness across
# the per-controller processes (one bind per port).
"grpcPort": base_grpc_port + i,
"pollIntervalMs": old.get("pollIntervalMs", poll_ms),
"registerMapPath": str(REPO / map_dir / f"{map_prefix}{name.lower()}.json"),
"enabled": old.get("enabled", True),
})
return {"shared": shared, "controllers": controllers}
if __name__ == "__main__":
p = argparse.ArgumentParser(description="Build config/gateway-config.json from Controller.csv")
p.add_argument("--csv", default="docs/Controller.csv")
p.add_argument("--map-dir", default="docs")
p.add_argument("--map-prefix", default="register-map-")
p.add_argument("--poll-ms", type=int, default=500)
p.add_argument("--base-grpc-port", type=int, default=50051)
p.add_argument("-o", "--output", default="config/gateway-config.json")
p.add_argument("--merge", action="store_true",
help="preserve shared block and per-controller overrides from the existing file")
args = p.parse_args()
out = Path(args.output)
existing = None
if args.merge and out.exists():
existing = json.loads(out.read_text(encoding="utf-8"))
cfg = build(Path(args.csv), args.map_dir, args.map_prefix, args.poll_ms,
args.base_grpc_port, existing)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(cfg, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"✓ Wrote {out} ({len(cfg['controllers'])} controllers)")
for c in cfg["controllers"]:
en = "" if c["enabled"] else " (disabled)"
print(f" {c['id']:4s} {c['controllerIp']:16s} grpc:{c['grpcPort']}{Path(c['registerMapPath']).name}{en}")