refactor: 태그 대소문자 register-map 기준 대문자로 전역 통일
- FeedforwardSupervisor: PvTag() ToUpperInvariant + empty FeedTag 가드 - FeedforwardConfigStore: 모든 ToLowerInvariant() 제거 - FeedRampAdvisorService: ToLowerInvariant 제거 + StringComparison.OrdinalIgnoreCase - SimOverrideStore: ToLowerInvariant 제거 - Hc900RealtimeService: HealthCheck SERVING 판정, mapping 없는 태그 대소문자 유지 - PidExtractorService: ToLowerInvariant → OrdinalIgnoreCase 비교 - Hc900Entities: 주석 업데이트 (대문자 표준) - load_state_labels.py: 소문자 변환 금지, controller_id 파라미터 추가 - Hc900Controllers: 대소문자 무시 정렬 - write.js: .MODE → AutoManState/RemLocSPState/SP_SelectState/TuneSetState - setup.js/html: 중복 함수 제거, C5 컨트롤러 placeholder
This commit is contained in:
@@ -1,15 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
xlsx의 StatusPoint DescriptorState0~7 → hc900.tag_metadata 로드
|
xlsx의 StatusPoint DescriptorState0~7 → hc900.tag_metadata 로드
|
||||||
실행: python3 scripts/load_state_labels.py
|
실행: python3 scripts/load_state_labels.py [--controller C1]
|
||||||
|
태그명은 Experion ItemName 원형 대소문자 유지 (ToLower 금지).
|
||||||
"""
|
"""
|
||||||
import openpyxl
|
import openpyxl
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
XLSX_PATH = Path(__file__).parent.parent / "docs" / "Sinam_Tag_all.xlsx"
|
XLSX_PATH = Path(__file__).parent.parent / "docs" / "Sinam_Tag_all.xlsx"
|
||||||
DB_DSN = "host=localhost port=5432 dbname=iiot_platform user=postgres password=postgres"
|
DB_DSN = "host=localhost port=5432 dbname=iiot_platform user=postgres password=postgres"
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Load StatusPoint state labels into tag_metadata')
|
||||||
|
parser.add_argument('--controller', default='HC1', help='Controller ID (default: HC1)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
wb = openpyxl.load_workbook(XLSX_PATH, read_only=True, data_only=True)
|
wb = openpyxl.load_workbook(XLSX_PATH, read_only=True, data_only=True)
|
||||||
ws = wb['Sheet1']
|
ws = wb['Sheet1']
|
||||||
rows = list(ws.iter_rows(values_only=True))
|
rows = list(ws.iter_rows(values_only=True))
|
||||||
@@ -26,7 +32,8 @@ for row in rows[2:]:
|
|||||||
if cls != 'StatusPoint' or not name:
|
if cls != 'StatusPoint' or not name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
base_tag = name.lower()
|
# 원형(ItemName) 유지 — 소문자 변환 금지
|
||||||
|
base_tag = name
|
||||||
|
|
||||||
for i in range(8):
|
for i in range(8):
|
||||||
key = f'DescriptorState{i}'
|
key = f'DescriptorState{i}'
|
||||||
@@ -36,13 +43,13 @@ for row in rows[2:]:
|
|||||||
if val is None or val == '':
|
if val is None or val == '':
|
||||||
continue
|
continue
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO hc900.tag_metadata (base_tag, attribute, value)
|
INSERT INTO hc900.tag_metadata (base_tag, attribute, value, controller_id)
|
||||||
VALUES (%s, %s, %s)
|
VALUES (%s, %s, %s, %s)
|
||||||
ON CONFLICT (base_tag, attribute) DO UPDATE SET value = EXCLUDED.value
|
ON CONFLICT (base_tag, attribute) DO UPDATE SET value = EXCLUDED.value, loaded_at = NOW()
|
||||||
""", (base_tag, f'state{i}', str(val)))
|
""", (base_tag, f'state{i}', str(val), args.controller))
|
||||||
inserted += 1
|
inserted += 1
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
print(f"완료: {inserted}개 상태 레이블 저장")
|
print(f"완료: {inserted}개 상태 레이블 저장 (controller={args.controller})")
|
||||||
|
|||||||
@@ -378,10 +378,10 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
|
|
||||||
private async Task<RealtimePoint?> FindFallbackTagAsync(string tagNo)
|
private async Task<RealtimePoint?> FindFallbackTagAsync(string tagNo)
|
||||||
{
|
{
|
||||||
var normalized = tagNo.Split('.')[0].ToLowerInvariant();
|
var baseTag = tagNo.Split('.')[0];
|
||||||
return await _dbContext.RealtimePoints
|
return await _dbContext.RealtimePoints
|
||||||
.FirstOrDefaultAsync(t => t.TagName.ToLower() == normalized
|
.FirstOrDefaultAsync(t => t.TagName.Equals(baseTag, StringComparison.OrdinalIgnoreCase)
|
||||||
|| t.TagName.ToLower().StartsWith(normalized + "."));
|
|| t.TagName.StartsWith(baseTag + ".", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(int Total, IEnumerable<PidEquipment> Items)> GetEquipmentAsync(
|
public async Task<(int Total, IEnumerable<PidEquipment> Items)> GetEquipmentAsync(
|
||||||
@@ -425,15 +425,15 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
// batch-load sub_area from tag_metadata
|
// batch-load sub_area from tag_metadata
|
||||||
if (items.Count > 0)
|
if (items.Count > 0)
|
||||||
{
|
{
|
||||||
var tagNos = items.Select(e => e.TagNo.ToLowerInvariant()).ToHashSet();
|
var tagNos = items.Select(e => e.TagNo).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
var subAreas = await _dbContext.TagMetadata
|
var subAreas = await _dbContext.TagMetadata
|
||||||
.Where(m => tagNos.Contains(m.BaseTag) && m.Attribute == "sub_area")
|
.Where(m => tagNos.Contains(m.BaseTag) && m.Attribute == "sub_area")
|
||||||
.Select(m => new { m.BaseTag, m.Value })
|
.Select(m => new { m.BaseTag, m.Value })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var subMap = subAreas.ToDictionary(sa => sa.BaseTag, sa => sa.Value);
|
var subMap = subAreas.ToDictionary(sa => sa.BaseTag, sa => sa.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var e in items)
|
foreach (var e in items)
|
||||||
{
|
{
|
||||||
if (subMap.TryGetValue(e.TagNo.ToLowerInvariant(), out var sa))
|
if (subMap.TryGetValue(e.TagNo, out var sa))
|
||||||
e.SubArea = sa;
|
e.SubArea = sa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,7 +471,7 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
{
|
{
|
||||||
var e = new PidEquipment
|
var e = new PidEquipment
|
||||||
{
|
{
|
||||||
TagNo = request.TagNo.ToLowerInvariant(),
|
TagNo = request.TagNo,
|
||||||
EquipmentName = request.EquipmentName,
|
EquipmentName = request.EquipmentName,
|
||||||
InstrumentType = request.InstrumentType,
|
InstrumentType = request.InstrumentType,
|
||||||
Category = request.Category,
|
Category = request.Category,
|
||||||
@@ -499,7 +499,7 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
{
|
{
|
||||||
var e = await _dbContext.PidEquipment.FindAsync(id);
|
var e = await _dbContext.PidEquipment.FindAsync(id);
|
||||||
if (e == null) return false;
|
if (e == null) return false;
|
||||||
if (request.TagNo != null) e.TagNo = request.TagNo.ToLowerInvariant();
|
if (request.TagNo != null) e.TagNo = request.TagNo;
|
||||||
if (request.EquipmentName != null) e.EquipmentName = request.EquipmentName;
|
if (request.EquipmentName != null) e.EquipmentName = request.EquipmentName;
|
||||||
if (request.InstrumentType != null) e.InstrumentType = request.InstrumentType;
|
if (request.InstrumentType != null) e.InstrumentType = request.InstrumentType;
|
||||||
if (request.Category != null) e.Category = request.Category;
|
if (request.Category != null) e.Category = request.Category;
|
||||||
@@ -518,7 +518,7 @@ public class PidExtractorService : IPidExtractorService
|
|||||||
await _dbContext.SaveChangesAsync();
|
await _dbContext.SaveChangesAsync();
|
||||||
if (request.SubArea != null)
|
if (request.SubArea != null)
|
||||||
{
|
{
|
||||||
var baseTag = e.TagNo.ToLowerInvariant();
|
var baseTag = e.TagNo;
|
||||||
if (string.IsNullOrWhiteSpace(request.SubArea))
|
if (string.IsNullOrWhiteSpace(request.SubArea))
|
||||||
await _dbContext.Database.ExecuteSqlRawAsync(
|
await _dbContext.Database.ExecuteSqlRawAsync(
|
||||||
"DELETE FROM tag_metadata WHERE base_tag = {0} AND attribute = 'sub_area'", baseTag);
|
"DELETE FROM tag_metadata WHERE base_tag = {0} AND attribute = 'sub_area'", baseTag);
|
||||||
|
|||||||
@@ -186,12 +186,12 @@ public class Hc900MapEntry
|
|||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>OPC UA 스타일 이름 (realtime_table 기준, e.g. "ficq-6101.pv")</summary>
|
/// <summary>태그명 (realtime_table 기준) = hc900_tag 와 동일한 Experion ItemName, e.g. "FICQ-6101.PV". 대소문자 변환 없음.</summary>
|
||||||
[Column("tagname")]
|
[Column("tagname")]
|
||||||
[Required]
|
[Required]
|
||||||
public string TagName { get; set; } = string.Empty;
|
public string TagName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>HC900 레지스터 태그명 (register-map.json 기준, e.g. "FICQ3101.PV")</summary>
|
/// <summary>HC900 게이트웨이 register-map 태그명 (= tagname), e.g. "FICQ-6101.PV"</summary>
|
||||||
[Column("hc900_tag")]
|
[Column("hc900_tag")]
|
||||||
[Required]
|
[Required]
|
||||||
public string Hc900Tag { get; set; } = string.Empty;
|
public string Hc900Tag { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -247,7 +247,24 @@ public class Hc900TagManagerController : ControllerBase
|
|||||||
if (!string.IsNullOrEmpty(search))
|
if (!string.IsNullOrEmpty(search))
|
||||||
q = q.Where(x => x.TagName.Contains(search) || x.Hc900Tag.Contains(search));
|
q = q.Where(x => x.TagName.Contains(search) || x.Hc900Tag.Contains(search));
|
||||||
|
|
||||||
var entries = await q.OrderBy(x => x.LoopNo).ThenBy(x => x.TagName).ToListAsync();
|
var entries = await q.ToListAsync();
|
||||||
|
entries.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
var ca = char.ToUpper(a.TagName[0]);
|
||||||
|
var cb = char.ToUpper(b.TagName[0]);
|
||||||
|
if (ca != cb) return ca.CompareTo(cb);
|
||||||
|
|
||||||
|
var dotA = a.TagName.LastIndexOf('.');
|
||||||
|
var dotB = b.TagName.LastIndexOf('.');
|
||||||
|
var baseA = dotA >= 0 ? a.TagName[..dotA] : a.TagName;
|
||||||
|
var baseB = dotB >= 0 ? b.TagName[..dotB] : b.TagName;
|
||||||
|
var propA = dotA >= 0 ? a.TagName[(dotA + 1)..] : "";
|
||||||
|
var propB = dotB >= 0 ? b.TagName[(dotB + 1)..] : "";
|
||||||
|
|
||||||
|
var cmp = string.Compare(baseA, baseB, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (cmp != 0) return cmp;
|
||||||
|
return string.Compare(propA, propB, StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
|
||||||
var tagNames = entries.Select(e => e.TagName).ToList();
|
var tagNames = entries.Select(e => e.TagName).ToList();
|
||||||
var liveValues = await _ctx.RealtimePoints
|
var liveValues = await _ctx.RealtimePoints
|
||||||
|
|||||||
@@ -239,73 +239,7 @@ async function addController() {
|
|||||||
document.getElementById('new-regmap').value = '';
|
document.getElementById('new-regmap').value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function saveCtrl(id) {
|
|
||||||
const cfg = await _setupApi('GET', '/config');
|
|
||||||
const ctrl = _ctrls(cfg).find(c => (c.Id ?? c.id) === id);
|
|
||||||
if (!ctrl) return;
|
|
||||||
const ipEl = document.getElementById('ip-' + id);
|
|
||||||
if (ipEl && ipEl.value.trim()) ctrl.controllerIp = ipEl.value.trim();
|
|
||||||
const mbEl = document.getElementById('mbport-' + id);
|
|
||||||
if (mbEl) ctrl.controllerPort = parseInt(mbEl.value) || 502;
|
|
||||||
const grEl = document.getElementById('grpc-' + id);
|
|
||||||
if (grEl) ctrl.grpcPort = parseInt(grEl.value) || 50051;
|
|
||||||
const poEl = document.getElementById('poll-' + id);
|
|
||||||
if (poEl) ctrl.pollIntervalMs = parseInt(poEl.value) || 1000;
|
|
||||||
const rmEl = document.getElementById('regmap-' + id);
|
|
||||||
if (rmEl && rmEl.value.trim()) ctrl.registerMapPath = rmEl.value.trim();
|
|
||||||
const enEl = document.getElementById('enabled-' + id);
|
|
||||||
if (enEl) ctrl.enabled = enEl.checked;
|
|
||||||
const r = await _setupApi('POST', '/config', cfg);
|
|
||||||
_setupMsg('msg-' + id, r.success, r.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteCtrl(id) {
|
|
||||||
if (!confirm(`Delete controller ${id}?`)) return;
|
|
||||||
const cfg = await _setupApi('GET', '/config');
|
|
||||||
const arr = _ctrls(cfg).filter(c => (c.Id ?? c.id) !== id);
|
|
||||||
if (arr.length === _ctrls(cfg).length) return;
|
|
||||||
cfg.Controllers = cfg.Controllers ?? cfg.controllers;
|
|
||||||
cfg.controllers = arr;
|
|
||||||
const r = await _setupApi('POST', '/config', cfg);
|
|
||||||
_setupMsg('add-msg', r.success, r.message);
|
|
||||||
if (r.success) loadAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSharedConfig() {
|
|
||||||
const cfg = await _setupApi('GET', '/config');
|
|
||||||
cfg.Shared = cfg.Shared ?? cfg.shared ?? {};
|
|
||||||
cfg.shared = cfg.Shared;
|
|
||||||
cfg.shared.binaryPath = document.getElementById('cfg-bin').value.trim();
|
|
||||||
cfg.shared.ldLibraryPath = document.getElementById('cfg-ld').value.trim();
|
|
||||||
cfg.shared.logDir = document.getElementById('cfg-logdir').value.trim();
|
|
||||||
const r = await _setupApi('POST', '/config', cfg);
|
|
||||||
_setupMsg('shared-msg', r.success, r.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addController() {
|
|
||||||
const cfg = await _setupApi('GET', '/config');
|
|
||||||
const controllers = cfg.Controllers ?? cfg.controllers ?? [];
|
|
||||||
controllers.push({
|
|
||||||
id: document.getElementById('new-id').value.trim() || 'HCX',
|
|
||||||
name: document.getElementById('new-name').value.trim(),
|
|
||||||
controllerIp: document.getElementById('new-ip').value.trim(),
|
|
||||||
controllerPort: parseInt(document.getElementById('new-port').value) || 502,
|
|
||||||
grpcPort: parseInt(document.getElementById('new-grpc').value) || 50051,
|
|
||||||
pollIntervalMs: parseInt(document.getElementById('new-poll').value) || 1000,
|
|
||||||
registerMapPath: document.getElementById('new-regmap').value.trim(),
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
cfg.controllers = controllers;
|
|
||||||
const r = await _setupApi('POST', '/config', cfg);
|
|
||||||
_setupMsg('add-msg', r.success, r.message);
|
|
||||||
if (r.success) {
|
|
||||||
loadAll();
|
|
||||||
document.getElementById('new-id').value = '';
|
|
||||||
document.getElementById('new-name').value = '';
|
|
||||||
document.getElementById('new-ip').value = '';
|
|
||||||
document.getElementById('new-regmap').value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshLog() {
|
async function refreshLog() {
|
||||||
const sel = document.getElementById('log-ctrl-select');
|
const sel = document.getElementById('log-ctrl-select');
|
||||||
|
|||||||
@@ -43,10 +43,17 @@ async function wrSetMode() {
|
|||||||
const mode = document.getElementById('wr-mode').value;
|
const mode = document.getElementById('wr-mode').value;
|
||||||
if (!tagName) return log('wr-log', [{ c: 'err', t: '❌ 태그명을 입력하세요.' }]);
|
if (!tagName) return log('wr-log', [{ c: 'err', t: '❌ 태그명을 입력하세요.' }]);
|
||||||
|
|
||||||
|
// 모드별 R/W 태그 매핑 (register-map의 .MD(읽기전용)이 아님)
|
||||||
|
// Auto/Manual → AutoManState, LSP/RSP → RemLocSPState, SP1/SP2 → SP_SelectState, Tune → TuneSetState
|
||||||
|
const modeMap = { '0': 'AutoManState', '1': 'AutoManState', '2': 'RemLocSPState', '3': 'SP_SelectState', '4': 'TuneSetState' };
|
||||||
|
const valueMap = { '0': 0, '1': 1, '2': 1, '3': 1, '4': 1 }; // Auto=1, RSP=1, RSP/LSP=1, Tune=1
|
||||||
|
const modeTag = modeMap[mode] ?? 'AutoManState';
|
||||||
|
const writeValue = mode === '0' ? 0 : 1; // 0=Manual/LSP, 1=Auto/RSP
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const d = await api('POST', '/api/gateway/write', { controllerId, tagName: tagName + '.MODE', value: parseInt(mode) });
|
const d = await api('POST', '/api/gateway/write', { controllerId, tagName: tagName + '.' + modeTag, value: writeValue });
|
||||||
log('wr-log', [
|
log('wr-log', [
|
||||||
{ c: d.success ? 'ok' : 'err', t: (d.success ? '✅ ' : '❌ ') + 'Mode ' + esc(tagName) + ' → ' + mode + (d.error ? ' → ' + esc(d.error) : '') },
|
{ c: d.success ? 'ok' : 'err', t: (d.success ? '✅ ' : '❌ ') + 'Mode ' + esc(tagName) + ' → ' + mode + ' (' + modeTag + ')' + (d.error ? ' → ' + esc(d.error) : '') },
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('wr-log', [{ c: 'err', t: '❌ ' + e.message }]);
|
log('wr-log', [{ c: 'err', t: '❌ ' + e.message }]);
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
.setup-dot.yellow { background:#cc3; box-shadow:0 0 6px #cc3; }
|
.setup-dot.yellow { background:#cc3; box-shadow:0 0 6px #cc3; }
|
||||||
|
|
||||||
.setup-field { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
|
.setup-field { display:flex; align-items:center; gap:8px; margin-bottom:6px; }
|
||||||
.setup-field label { width:110px; font-size:12px; color:#888; flex-shrink:0; }
|
.setup-field label { width:70px; font-size:12px; color:#888; flex-shrink:0; }
|
||||||
.setup-field input, .setup-field select { flex:1; background:#111; border:1px solid #444; color:#eee; padding:5px 8px;
|
.setup-field input, .setup-field select { flex:1; background:#111; border:1px solid #444; color:#eee; padding:5px 8px;
|
||||||
border-radius:3px; font-family:monospace; font-size:12px; }
|
border-radius:3px; font-family:monospace; font-size:12px; }
|
||||||
.setup-field input:focus { outline:none; border-color:#4af; }
|
.setup-field input:focus { outline:none; border-color:#4af; }
|
||||||
.setup-field input.slim { width:90px; flex:none; }
|
.setup-field input.slim { width:64px; flex:none; }
|
||||||
.setup-field .val { font-size:12px; color:#bbb; }
|
.setup-field .val { font-size:12px; color:#bbb; }
|
||||||
|
|
||||||
.setup-btn { padding:5px 12px; border:none; border-radius:3px; cursor:pointer; font-family:monospace;
|
.setup-btn { padding:5px 12px; border:none; border-radius:3px; cursor:pointer; font-family:monospace;
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
<h3>➕ Add Controller</h3>
|
<h3>➕ Add Controller</h3>
|
||||||
<div class="setup-field">
|
<div class="setup-field">
|
||||||
<label>Controller ID</label>
|
<label>Controller ID</label>
|
||||||
<input id="new-id" type="text" placeholder="HC2" style="width:80px;flex:none">
|
<input id="new-id" type="text" placeholder="C5" style="width:80px;flex:none">
|
||||||
<label style="width:auto">Name</label>
|
<label style="width:auto">Name</label>
|
||||||
<input id="new-name" type="text" placeholder="Column 2" style="flex:1">
|
<input id="new-name" type="text" placeholder="HC900 C5 Controller" style="flex:1">
|
||||||
</div>
|
</div>
|
||||||
<div class="setup-field">
|
<div class="setup-field">
|
||||||
<label>IP</label>
|
<label>IP</label>
|
||||||
@@ -116,13 +116,13 @@
|
|||||||
<label>Modbus Port</label>
|
<label>Modbus Port</label>
|
||||||
<input id="new-port" type="number" value="502" class="slim">
|
<input id="new-port" type="number" value="502" class="slim">
|
||||||
<label style="width:auto">gRPC Port</label>
|
<label style="width:auto">gRPC Port</label>
|
||||||
<input id="new-grpc" type="number" value="50052" class="slim">
|
<input id="new-grpc" type="number" value="50055" class="slim">
|
||||||
<label style="width:auto">Poll (ms)</label>
|
<label style="width:auto">Poll (ms)</label>
|
||||||
<input id="new-poll" type="number" value="1000" class="slim">
|
<input id="new-poll" type="number" value="1000" class="slim">
|
||||||
</div>
|
</div>
|
||||||
<div class="setup-field">
|
<div class="setup-field">
|
||||||
<label>Register Map</label>
|
<label>Register Map</label>
|
||||||
<input id="new-regmap" type="text" placeholder="docs/register-map.json">
|
<input id="new-regmap" type="text" placeholder="docs/register-map-c5.json">
|
||||||
</div>
|
</div>
|
||||||
<div class="setup-btn-row">
|
<div class="setup-btn-row">
|
||||||
<button class="setup-btn success" onclick="addController()">➕ Add</button>
|
<button class="setup-btn success" onclick="addController()">➕ Add</button>
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ h2 { color: #4af; margin: 0 0 20px; }
|
|||||||
|
|
||||||
/* ── Fields ── */
|
/* ── Fields ── */
|
||||||
.field { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
.field { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
||||||
.field label { width: 110px; font-size: 12px; color: #888; flex-shrink: 0; }
|
.field label { width: 70px; font-size: 12px; color: #888; flex-shrink: 0; }
|
||||||
.field input, .field select { flex: 1; background: #111; border: 1px solid #444; color: #eee; padding: 5px 8px;
|
.field input, .field select { flex: 1; background: #111; border: 1px solid #444; color: #eee; padding: 5px 8px;
|
||||||
border-radius: 3px; font-family: monospace; font-size: 12px; }
|
border-radius: 3px; font-family: monospace; font-size: 12px; }
|
||||||
.field input:focus { outline: none; border-color: #4af; }
|
.field input:focus { outline: none; border-color: #4af; }
|
||||||
.field input.slim { width: 90px; flex: none; }
|
.field input.slim { width: 64px; flex: none; }
|
||||||
.field .val { font-size: 12px; color: #bbb; }
|
.field .val { font-size: 12px; color: #bbb; }
|
||||||
|
|
||||||
/* ── Buttons ── */
|
/* ── Buttons ── */
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class FeedRampAdvisorService
|
|||||||
if (cfg is null) return null;
|
if (cfg is null) return null;
|
||||||
|
|
||||||
static string PvTag(string baseTag)
|
static string PvTag(string baseTag)
|
||||||
{ var t = baseTag.ToLowerInvariant(); return t.EndsWith(".pv") ? t : t + ".pv"; }
|
{ return baseTag.EndsWith(".pv", StringComparison.OrdinalIgnoreCase) ? baseTag : baseTag + ".pv"; }
|
||||||
|
|
||||||
string picaTag = PvTag(cfg.PressureTag ?? "pica-6111");
|
string picaTag = PvTag(cfg.PressureTag ?? "pica-6111");
|
||||||
const string piTag = "pi-6111b.pv", tiTag = "ti-6103.pv", steamTag = "ficq-6115.pv", opTag = "tica-6111a.op";
|
const string piTag = "pi-6111b.pv", tiTag = "ti-6103.pv", steamTag = "ficq-6115.pv", opTag = "tica-6111a.op";
|
||||||
@@ -34,11 +34,10 @@ public sealed class FeedRampAdvisorService
|
|||||||
tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag)));
|
tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag)));
|
||||||
|
|
||||||
var rows = (await _db.GetRealtimeRecordsByTagNamesAsync(tags))
|
var rows = (await _db.GetRealtimeRecordsByTagNamesAsync(tags))
|
||||||
.ToDictionary(r => r.TagName.ToLowerInvariant(), r => r);
|
.ToDictionary(r => r.TagName, r => r);
|
||||||
|
|
||||||
bool TryRead(string tag, out double v)
|
bool TryRead(string tag, out double v)
|
||||||
{
|
{
|
||||||
tag = tag.ToLowerInvariant();
|
|
||||||
if (_sim.Enabled && _sim.TryGet(tag, out v)) return true; // override(신선)
|
if (_sim.Enabled && _sim.TryGet(tag, out v)) return true; // override(신선)
|
||||||
if (rows.TryGetValue(tag, out var r) && r.LiveValue is not null
|
if (rows.TryGetValue(tag, out var r) && r.LiveValue is not null
|
||||||
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out v)
|
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out v)
|
||||||
|
|||||||
@@ -44,14 +44,12 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
var levelTags = rd.IsDBNull(5)
|
var levelTags = rd.IsDBNull(5)
|
||||||
? Array.Empty<string>()
|
? Array.Empty<string>()
|
||||||
: rd.GetString(5)
|
: rd.GetString(5)
|
||||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
.Select(t => t.ToLowerInvariant()).ToArray();
|
|
||||||
|
|
||||||
var rawTempTags = rd.IsDBNull(14) ? null : rd.GetString(14);
|
var rawTempTags = rd.IsDBNull(14) ? null : rd.GetString(14);
|
||||||
var tempTags = rawTempTags is null
|
var tempTags = rawTempTags is null
|
||||||
? Array.Empty<string>()
|
? Array.Empty<string>()
|
||||||
: rawTempTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
: rawTempTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
.Select(t => t.ToLowerInvariant()).ToArray();
|
|
||||||
|
|
||||||
var cfg = new ColumnConfig
|
var cfg = new ColumnConfig
|
||||||
{
|
{
|
||||||
@@ -59,8 +57,8 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
Name = rd.GetString(1),
|
Name = rd.GetString(1),
|
||||||
Enabled = rd.GetBoolean(2),
|
Enabled = rd.GetBoolean(2),
|
||||||
AdvisoryOnly = rd.GetBoolean(30),
|
AdvisoryOnly = rd.GetBoolean(30),
|
||||||
FeedTag = rd.GetString(3).ToLowerInvariant(),
|
FeedTag = rd.GetString(3),
|
||||||
PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4).ToLowerInvariant(),
|
PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4),
|
||||||
LevelTags = levelTags,
|
LevelTags = levelTags,
|
||||||
ScanSec = rd.GetDouble(6),
|
ScanSec = rd.GetDouble(6),
|
||||||
FeedFilterTauSec = rd.GetDouble(7),
|
FeedFilterTauSec = rd.GetDouble(7),
|
||||||
@@ -72,10 +70,10 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
ProductKey = rd.GetString(13),
|
ProductKey = rd.GetString(13),
|
||||||
Streams = Array.Empty<StreamConfig>(),
|
Streams = Array.Empty<StreamConfig>(),
|
||||||
TempTags = tempTags,
|
TempTags = tempTags,
|
||||||
SensitiveTrayTag = rd.IsDBNull(15) ? null : rd.GetString(15).ToLowerInvariant(),
|
SensitiveTrayTag = rd.IsDBNull(15) ? null : rd.GetString(15),
|
||||||
DTdP = rd.GetDouble(16),
|
DTdP = rd.GetDouble(16),
|
||||||
PRef = rd.IsDBNull(17) ? double.NaN : rd.GetDouble(17),
|
PRef = rd.IsDBNull(17) ? double.NaN : rd.GetDouble(17),
|
||||||
SteamOpTag = rd.IsDBNull(18) ? null : rd.GetString(18).ToLowerInvariant(),
|
SteamOpTag = rd.IsDBNull(18) ? null : rd.GetString(18),
|
||||||
ThetaAutoTune = rd.GetBoolean(19),
|
ThetaAutoTune = rd.GetBoolean(19),
|
||||||
BiasMaWindowSec = rd.GetDouble(20),
|
BiasMaWindowSec = rd.GetDouble(20),
|
||||||
RecoveryEnabled = rd.GetBoolean(21),
|
RecoveryEnabled = rd.GetBoolean(21),
|
||||||
@@ -85,7 +83,7 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
RecoverySettleSec = rd.GetDouble(25),
|
RecoverySettleSec = rd.GetDouble(25),
|
||||||
ReturnRampSec = rd.GetDouble(26),
|
ReturnRampSec = rd.GetDouble(26),
|
||||||
FeedRecoverySp = rd.GetDouble(27),
|
FeedRecoverySp = rd.GetDouble(27),
|
||||||
DeltaPTag = rd.IsDBNull(28) ? null : rd.GetString(28).ToLowerInvariant(),
|
DeltaPTag = rd.IsDBNull(28) ? null : rd.GetString(28),
|
||||||
DeltaPFloodLimit = rd.GetDouble(29),
|
DeltaPFloodLimit = rd.GetDouble(29),
|
||||||
TempHighLimit = rd.GetDouble(31),
|
TempHighLimit = rd.GetDouble(31),
|
||||||
};
|
};
|
||||||
@@ -111,9 +109,9 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
entry.streams.Add(new StreamConfig
|
entry.streams.Add(new StreamConfig
|
||||||
{
|
{
|
||||||
Key = rd.GetString(1),
|
Key = rd.GetString(1),
|
||||||
FlowTag = rd.GetString(2).ToLowerInvariant(),
|
FlowTag = rd.GetString(2),
|
||||||
Role = Enum.TryParse<StreamRole>(rd.GetString(3), true, out var role) ? role : StreamRole.Monitor,
|
Role = Enum.TryParse<StreamRole>(rd.GetString(3), true, out var role) ? role : StreamRole.Monitor,
|
||||||
LevelTag = rd.IsDBNull(14) ? null : rd.GetString(14).ToLowerInvariant(),
|
LevelTag = rd.IsDBNull(14) ? null : rd.GetString(14),
|
||||||
IsReflux = rd.GetBoolean(15),
|
IsReflux = rd.GetBoolean(15),
|
||||||
RecoverySp = rd.IsDBNull(16) ? double.NaN : rd.GetDouble(16),
|
RecoverySp = rd.IsDBNull(16) ? double.NaN : rd.GetDouble(16),
|
||||||
SpNodeId = rd.IsDBNull(17) ? null : rd.GetString(17),
|
SpNodeId = rd.IsDBNull(17) ? null : rd.GetString(17),
|
||||||
@@ -176,22 +174,22 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
@deltaPTag,@deltaPFlood,@tempHigh)
|
@deltaPTag,@deltaPFlood,@tempHigh)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""";
|
""";
|
||||||
P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled); P(cmd,"@feed",cfg.FeedTag.ToLowerInvariant());
|
P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled); P(cmd,"@feed",cfg.FeedTag);
|
||||||
P(cmd,"@pres",(object?)cfg.PressureTag?.ToLowerInvariant()); P(cmd,"@lvl",levelTags.ToLowerInvariant());
|
P(cmd,"@pres",(object?)cfg.PressureTag); P(cmd,"@lvl",levelTags);
|
||||||
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
||||||
P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec); P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin);
|
P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec); P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin);
|
||||||
P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand); P(cmd,"@settle",cfg.SettleSec);
|
P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand); P(cmd,"@settle",cfg.SettleSec);
|
||||||
P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
||||||
P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
|
P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
|
||||||
P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag?.ToLowerInvariant() ?? DBNull.Value);
|
P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag ?? DBNull.Value);
|
||||||
P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
|
P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
|
||||||
P(cmd,"@steamOp",(object?)cfg.SteamOpTag?.ToLowerInvariant() ?? DBNull.Value);
|
P(cmd,"@steamOp",(object?)cfg.SteamOpTag ?? DBNull.Value);
|
||||||
P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
|
P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
|
||||||
P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
||||||
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
||||||
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
||||||
P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
|
P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
|
||||||
P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag?.ToLowerInvariant() ?? DBNull.Value);
|
P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag ?? DBNull.Value);
|
||||||
P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
|
P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
|
||||||
P(cmd,"@tempHigh",cfg.TempHighLimit);
|
P(cmd,"@tempHigh",cfg.TempHighLimit);
|
||||||
id = Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
|
id = Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
|
||||||
@@ -215,21 +213,21 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
WHERE id=@id
|
WHERE id=@id
|
||||||
""";
|
""";
|
||||||
P(cmd,"@id",id); P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled);
|
P(cmd,"@id",id); P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled);
|
||||||
P(cmd,"@feed",cfg.FeedTag.ToLowerInvariant()); P(cmd,"@pres",(object?)cfg.PressureTag?.ToLowerInvariant());
|
P(cmd,"@feed",cfg.FeedTag); P(cmd,"@pres",(object?)cfg.PressureTag);
|
||||||
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
||||||
P(cmd,"@lvl",levelTags.ToLowerInvariant()); P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec);
|
P(cmd,"@lvl",levelTags); P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec);
|
||||||
P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin); P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand);
|
P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin); P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand);
|
||||||
P(cmd,"@settle",cfg.SettleSec); P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
P(cmd,"@settle",cfg.SettleSec); P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
||||||
P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
|
P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
|
||||||
P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag?.ToLowerInvariant() ?? DBNull.Value);
|
P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag ?? DBNull.Value);
|
||||||
P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
|
P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
|
||||||
P(cmd,"@steamOp",(object?)cfg.SteamOpTag?.ToLowerInvariant() ?? DBNull.Value);
|
P(cmd,"@steamOp",(object?)cfg.SteamOpTag ?? DBNull.Value);
|
||||||
P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
|
P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
|
||||||
P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
||||||
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
||||||
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
||||||
P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
|
P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
|
||||||
P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag?.ToLowerInvariant() ?? DBNull.Value);
|
P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag ?? DBNull.Value);
|
||||||
P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
|
P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
|
||||||
P(cmd,"@tempHigh",cfg.TempHighLimit);
|
P(cmd,"@tempHigh",cfg.TempHighLimit);
|
||||||
await cmd.ExecuteNonQueryAsync(ct);
|
await cmd.ExecuteNonQueryAsync(ct);
|
||||||
@@ -253,8 +251,8 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|||||||
VALUES (@cid,@key,@flow,@role,@k,@tup,@tdn,@tau,@smin,@smax,@rup,@rdn,@rfp,@grade,@lvlTag,
|
VALUES (@cid,@key,@flow,@role,@k,@tup,@tdn,@tau,@smin,@smax,@rup,@rdn,@rfp,@grade,@lvlTag,
|
||||||
@isReflux,@recSp,@spNode)
|
@isReflux,@recSp,@spNode)
|
||||||
""";
|
""";
|
||||||
P(ins,"@cid",id); P(ins,"@key",s.Key); P(ins,"@flow",s.FlowTag.ToLowerInvariant());
|
P(ins,"@cid",id); P(ins,"@key",s.Key); P(ins,"@flow",s.FlowTag);
|
||||||
P(ins,"@role",s.Role.ToString()); P(ins,"@lvlTag",(object?)s.LevelTag?.ToLowerInvariant() ?? DBNull.Value); P(ins,"@k",s.TargetCoeff); P(ins,"@tup",s.ThetaUpSec);
|
P(ins,"@role",s.Role.ToString()); P(ins,"@lvlTag",(object?)s.LevelTag ?? DBNull.Value); P(ins,"@k",s.TargetCoeff); P(ins,"@tup",s.ThetaUpSec);
|
||||||
P(ins,"@tdn",s.ThetaDnSec); P(ins,"@tau",s.TauSec); P(ins,"@smin",s.SpMin); P(ins,"@smax",s.SpMax);
|
P(ins,"@tdn",s.ThetaDnSec); P(ins,"@tau",s.TauSec); P(ins,"@smin",s.SpMin); P(ins,"@smax",s.SpMax);
|
||||||
P(ins,"@rup",s.RateUpPerMin); P(ins,"@rdn",s.RateDnPerMin); P(ins,"@rfp",s.RefluxFromProduct);
|
P(ins,"@rup",s.RateUpPerMin); P(ins,"@rdn",s.RateDnPerMin); P(ins,"@rfp",s.RefluxFromProduct);
|
||||||
P(ins,"@grade",s.Grade.ToString());
|
P(ins,"@grade",s.Grade.ToString());
|
||||||
|
|||||||
@@ -176,17 +176,25 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
|||||||
{
|
{
|
||||||
string PvTag(string baseTag)
|
string PvTag(string baseTag)
|
||||||
{
|
{
|
||||||
var t = baseTag.ToLowerInvariant();
|
if (string.IsNullOrWhiteSpace(baseTag)) return baseTag; // 빈 태그는 그대로(호출부가 처리)
|
||||||
return t.EndsWith(".pv") ? t : t + ".pv";
|
var result = baseTag.EndsWith(".pv", StringComparison.OrdinalIgnoreCase) ? baseTag : baseTag + ".pv";
|
||||||
|
return result.ToUpperInvariant(); // register map 기준 대문자 통일
|
||||||
}
|
}
|
||||||
var feedTag = PvTag(cfg.FeedTag);
|
var feedTag = PvTag(cfg.FeedTag);
|
||||||
|
if (string.IsNullOrWhiteSpace(feedTag))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("[FF] FeedTag 비어 있음 — column {Id} 처리 불가", cfg.Id);
|
||||||
|
return new PvSnapshot(
|
||||||
|
new TagSample("", double.NaN, Good: false, DateTime.MinValue),
|
||||||
|
null, Array.Empty<TagSample>(), new Dictionary<string, TagSample>());
|
||||||
|
}
|
||||||
var tags = new List<string> { feedTag };
|
var tags = new List<string> { feedTag };
|
||||||
if (cfg.PressureTag is not null) tags.Add(PvTag(cfg.PressureTag));
|
if (cfg.PressureTag is not null) tags.Add(PvTag(cfg.PressureTag));
|
||||||
tags.AddRange(cfg.LevelTags.Select(PvTag));
|
tags.AddRange(cfg.LevelTags.Select(PvTag));
|
||||||
tags.AddRange(cfg.Streams.Where(s => s.LevelTag is not null).Select(s => PvTag(s.LevelTag!)));
|
tags.AddRange(cfg.Streams.Where(s => s.LevelTag is not null).Select(s => PvTag(s.LevelTag!)));
|
||||||
tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag)));
|
tags.AddRange(cfg.Streams.Select(s => PvTag(s.FlowTag)));
|
||||||
tags.AddRange(cfg.TempTags.Select(PvTag)); // WO-2 온도 프로파일
|
tags.AddRange(cfg.TempTags.Select(PvTag)); // WO-2 온도 프로파일
|
||||||
if (cfg.SteamOpTag is not null) tags.Add(cfg.SteamOpTag.ToLowerInvariant()); // WO-3 스팀 OP(.op 그대로)
|
if (cfg.SteamOpTag is not null) tags.Add(cfg.SteamOpTag.ToUpperInvariant()); // WO-3 스팀 OP(.op)
|
||||||
if (cfg.DeltaPTag is not null) tags.Add(PvTag(cfg.DeltaPTag)); // WO-6 차압(.pv)
|
if (cfg.DeltaPTag is not null) tags.Add(PvTag(cfg.DeltaPTag)); // WO-6 차압(.pv)
|
||||||
|
|
||||||
// WP6: DeltaPTag 미지정 시 PressureTag로부터 탑저압 파생
|
// WP6: DeltaPTag 미지정 시 PressureTag로부터 탑저압 파생
|
||||||
@@ -194,8 +202,8 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
|||||||
if (cfg.DeltaPTag is null && cfg.PressureTag is not null)
|
if (cfg.DeltaPTag is null && cfg.PressureTag is not null)
|
||||||
{
|
{
|
||||||
// pica-{n} → pi-{n}b (C-6111 명명 규약)
|
// pica-{n} → pi-{n}b (C-6111 명명 규약)
|
||||||
var p = cfg.PressureTag.ToLowerInvariant();
|
var p = cfg.PressureTag;
|
||||||
if (p.StartsWith("pica-"))
|
if (p.StartsWith("pica-", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var num = p["pica-".Length..];
|
var num = p["pica-".Length..];
|
||||||
derivedBottomTag = "pi-" + num + "b";
|
derivedBottomTag = "pi-" + num + "b";
|
||||||
@@ -204,14 +212,14 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rows = (await db.GetRealtimeRecordsByTagNamesAsync(tags))
|
var rows = (await db.GetRealtimeRecordsByTagNamesAsync(tags))
|
||||||
.ToDictionary(r => r.TagName.ToLowerInvariant(), r => r);
|
.ToDictionary(r => r.TagName, r => r);
|
||||||
|
|
||||||
TagSample Sample(string baseTag)
|
TagSample Sample(string baseTag)
|
||||||
{
|
{
|
||||||
var tag = PvTag(baseTag);
|
var tag = PvTag(baseTag);
|
||||||
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선(신선 처리)
|
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선(신선 처리)
|
||||||
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
|
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
|
||||||
if (rows.TryGetValue(tag.ToLowerInvariant(), out var r)
|
if (rows.TryGetValue(tag, out var r)
|
||||||
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
|
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
|
||||||
{
|
{
|
||||||
bool fresh = (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec;
|
bool fresh = (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec;
|
||||||
@@ -223,7 +231,7 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
|||||||
// WO-3: .op 등 비-.pv 태그를 접미사 강제 없이 그대로 읽음
|
// WO-3: .op 등 비-.pv 태그를 접미사 강제 없이 그대로 읽음
|
||||||
TagSample SampleExact(string rawTag)
|
TagSample SampleExact(string rawTag)
|
||||||
{
|
{
|
||||||
var tag = rawTag.ToLowerInvariant();
|
var tag = rawTag.ToUpperInvariant(); // register map 기준 대문자 통일
|
||||||
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선
|
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선
|
||||||
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
|
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
|
||||||
if (rows.TryGetValue(tag, out var r)
|
if (rows.TryGetValue(tag, out var r)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class SimOverrideStore : ISimOverrideStore
|
|||||||
|
|
||||||
public void SetMany(bool enabled, IReadOnlyDictionary<string, double> values)
|
public void SetMany(bool enabled, IReadOnlyDictionary<string, double> values)
|
||||||
{
|
{
|
||||||
foreach (var kv in values) _values[kv.Key.ToLowerInvariant()] = kv.Value;
|
foreach (var kv in values) _values[kv.Key] = kv.Value;
|
||||||
_enabled = enabled;
|
_enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ public sealed class SimOverrideStore : ISimOverrideStore
|
|||||||
_enabled = false;
|
_enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGet(string tag, out double value) => _values.TryGetValue(tag.ToLowerInvariant(), out value);
|
public bool TryGet(string tag, out double value) => _values.TryGetValue(tag, out value);
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, double> Snapshot() => new Dictionary<string, double>(_values);
|
public IReadOnlyDictionary<string, double> Snapshot() => new Dictionary<string, double>(_values);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,10 +76,19 @@ public class Hc900RealtimeService : BackgroundService, IHc900RealtimeService
|
|||||||
? await client.ReadTagsAsync(new ReadTagsRequest { TagNames = { tagNames } })
|
? await client.ReadTagsAsync(new ReadTagsRequest { TagNames = { tagNames } })
|
||||||
: client.ReadTags(new ReadTagsRequest());
|
: client.ReadTags(new ReadTagsRequest());
|
||||||
|
|
||||||
_connected[controllerId] = true;
|
// 프로세스가 살아 있으면 ReadTags는 (캐시 반환으로) 성공하므로 그것만으로는
|
||||||
|
// 연결 판정이 안 된다. 실제 Modbus 연결 여부는 게이트웨이 HealthCheck(SERVING)로 판정.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var health = await client.HealthCheckAsync(new HealthCheckRequest(),
|
||||||
|
deadline: DateTime.UtcNow.AddSeconds(2));
|
||||||
|
_connected[controllerId] =
|
||||||
|
health.Status == HealthCheckResponse.Types.ServingStatus.Serving;
|
||||||
|
}
|
||||||
|
catch { _connected[controllerId] = false; }
|
||||||
_lastPollAt[controllerId] = DateTime.UtcNow;
|
_lastPollAt[controllerId] = DateTime.UtcNow;
|
||||||
|
|
||||||
if (resp.Values.Count > 0)
|
if (_connected[controllerId] && resp.Values.Count > 0)
|
||||||
await BatchUpdateRealtimeTableAsync(controllerId, resp.Values, mapping, ct);
|
await BatchUpdateRealtimeTableAsync(controllerId, resp.Values, mapping, ct);
|
||||||
|
|
||||||
// poll count는 gRPC ReadTags + DB write 모두 성공한 후에만 증가
|
// poll count는 gRPC ReadTags + DB write 모두 성공한 후에만 증가
|
||||||
@@ -201,7 +210,8 @@ public class Hc900RealtimeService : BackgroundService, IHc900RealtimeService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tagname = tv.TagName.ToLowerInvariant();
|
// No mapping row → use the gateway tag as-is (= the register/Experion name).
|
||||||
|
var tagname = tv.TagName;
|
||||||
rows.Add((tagname, FormatValue(tv, tagname), ts));
|
rows.Add((tagname, FormatValue(tv, tagname), ts));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user