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:
107
scripts/build_controllers_config.py
Normal file
107
scripts/build_controllers_config.py
Normal 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}")
|
||||
Reference in New Issue
Block a user