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
|
||||
"""
|
||||
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 psycopg2
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
XLSX_PATH = Path(__file__).parent.parent / "docs" / "Sinam_Tag_all.xlsx"
|
||||
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)
|
||||
ws = wb['Sheet1']
|
||||
rows = list(ws.iter_rows(values_only=True))
|
||||
@@ -26,7 +32,8 @@ for row in rows[2:]:
|
||||
if cls != 'StatusPoint' or not name:
|
||||
continue
|
||||
|
||||
base_tag = name.lower()
|
||||
# 원형(ItemName) 유지 — 소문자 변환 금지
|
||||
base_tag = name
|
||||
|
||||
for i in range(8):
|
||||
key = f'DescriptorState{i}'
|
||||
@@ -36,13 +43,13 @@ for row in rows[2:]:
|
||||
if val is None or val == '':
|
||||
continue
|
||||
cur.execute("""
|
||||
INSERT INTO hc900.tag_metadata (base_tag, attribute, value)
|
||||
VALUES (%s, %s, %s)
|
||||
ON CONFLICT (base_tag, attribute) DO UPDATE SET value = EXCLUDED.value
|
||||
""", (base_tag, f'state{i}', str(val)))
|
||||
INSERT INTO hc900.tag_metadata (base_tag, attribute, value, controller_id)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (base_tag, attribute) DO UPDATE SET value = EXCLUDED.value, loaded_at = NOW()
|
||||
""", (base_tag, f'state{i}', str(val), args.controller))
|
||||
inserted += 1
|
||||
|
||||
conn.commit()
|
||||
cur.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)
|
||||
{
|
||||
var normalized = tagNo.Split('.')[0].ToLowerInvariant();
|
||||
var baseTag = tagNo.Split('.')[0];
|
||||
return await _dbContext.RealtimePoints
|
||||
.FirstOrDefaultAsync(t => t.TagName.ToLower() == normalized
|
||||
|| t.TagName.ToLower().StartsWith(normalized + "."));
|
||||
.FirstOrDefaultAsync(t => t.TagName.Equals(baseTag, StringComparison.OrdinalIgnoreCase)
|
||||
|| t.TagName.StartsWith(baseTag + ".", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public async Task<(int Total, IEnumerable<PidEquipment> Items)> GetEquipmentAsync(
|
||||
@@ -425,15 +425,15 @@ public class PidExtractorService : IPidExtractorService
|
||||
// batch-load sub_area from tag_metadata
|
||||
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
|
||||
.Where(m => tagNos.Contains(m.BaseTag) && m.Attribute == "sub_area")
|
||||
.Select(m => new { m.BaseTag, m.Value })
|
||||
.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)
|
||||
{
|
||||
if (subMap.TryGetValue(e.TagNo.ToLowerInvariant(), out var sa))
|
||||
if (subMap.TryGetValue(e.TagNo, out var sa))
|
||||
e.SubArea = sa;
|
||||
}
|
||||
}
|
||||
@@ -471,7 +471,7 @@ public class PidExtractorService : IPidExtractorService
|
||||
{
|
||||
var e = new PidEquipment
|
||||
{
|
||||
TagNo = request.TagNo.ToLowerInvariant(),
|
||||
TagNo = request.TagNo,
|
||||
EquipmentName = request.EquipmentName,
|
||||
InstrumentType = request.InstrumentType,
|
||||
Category = request.Category,
|
||||
@@ -499,7 +499,7 @@ public class PidExtractorService : IPidExtractorService
|
||||
{
|
||||
var e = await _dbContext.PidEquipment.FindAsync(id);
|
||||
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.InstrumentType != null) e.InstrumentType = request.InstrumentType;
|
||||
if (request.Category != null) e.Category = request.Category;
|
||||
@@ -518,7 +518,7 @@ public class PidExtractorService : IPidExtractorService
|
||||
await _dbContext.SaveChangesAsync();
|
||||
if (request.SubArea != null)
|
||||
{
|
||||
var baseTag = e.TagNo.ToLowerInvariant();
|
||||
var baseTag = e.TagNo;
|
||||
if (string.IsNullOrWhiteSpace(request.SubArea))
|
||||
await _dbContext.Database.ExecuteSqlRawAsync(
|
||||
"DELETE FROM tag_metadata WHERE base_tag = {0} AND attribute = 'sub_area'", baseTag);
|
||||
|
||||
@@ -186,12 +186,12 @@ public class Hc900MapEntry
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
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")]
|
||||
[Required]
|
||||
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")]
|
||||
[Required]
|
||||
public string Hc900Tag { get; set; } = string.Empty;
|
||||
|
||||
@@ -247,7 +247,24 @@ public class Hc900TagManagerController : ControllerBase
|
||||
if (!string.IsNullOrEmpty(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 liveValues = await _ctx.RealtimePoints
|
||||
|
||||
@@ -239,73 +239,7 @@ async function addController() {
|
||||
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() {
|
||||
const sel = document.getElementById('log-ctrl-select');
|
||||
|
||||
@@ -43,10 +43,17 @@ async function wrSetMode() {
|
||||
const mode = document.getElementById('wr-mode').value;
|
||||
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 {
|
||||
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', [
|
||||
{ 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) {
|
||||
log('wr-log', [{ c: 'err', t: '❌ ' + e.message }]);
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
.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 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;
|
||||
border-radius:3px; font-family:monospace; font-size:12px; }
|
||||
.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-btn { padding:5px 12px; border:none; border-radius:3px; cursor:pointer; font-family:monospace;
|
||||
@@ -104,9 +104,9 @@
|
||||
<h3>➕ Add Controller</h3>
|
||||
<div class="setup-field">
|
||||
<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>
|
||||
<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 class="setup-field">
|
||||
<label>IP</label>
|
||||
@@ -116,13 +116,13 @@
|
||||
<label>Modbus Port</label>
|
||||
<input id="new-port" type="number" value="502" class="slim">
|
||||
<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>
|
||||
<input id="new-poll" type="number" value="1000" class="slim">
|
||||
</div>
|
||||
<div class="setup-field">
|
||||
<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 class="setup-btn-row">
|
||||
<button class="setup-btn success" onclick="addController()">➕ Add</button>
|
||||
|
||||
@@ -30,11 +30,11 @@ h2 { color: #4af; margin: 0 0 20px; }
|
||||
|
||||
/* ── Fields ── */
|
||||
.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;
|
||||
border-radius: 3px; font-family: monospace; font-size: 12px; }
|
||||
.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; }
|
||||
|
||||
/* ── Buttons ── */
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class FeedRampAdvisorService
|
||||
if (cfg is null) return null;
|
||||
|
||||
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");
|
||||
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)));
|
||||
|
||||
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)
|
||||
{
|
||||
tag = tag.ToLowerInvariant();
|
||||
if (_sim.Enabled && _sim.TryGet(tag, out v)) return true; // override(신선)
|
||||
if (rows.TryGetValue(tag, out var r) && r.LiveValue is not null
|
||||
&& double.TryParse(r.LiveValue, NumberStyles.Float, CultureInfo.InvariantCulture, out v)
|
||||
|
||||
@@ -44,14 +44,12 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
var levelTags = rd.IsDBNull(5)
|
||||
? Array.Empty<string>()
|
||||
: rd.GetString(5)
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => t.ToLowerInvariant()).ToArray();
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var rawTempTags = rd.IsDBNull(14) ? null : rd.GetString(14);
|
||||
var tempTags = rawTempTags is null
|
||||
? Array.Empty<string>()
|
||||
: rawTempTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => t.ToLowerInvariant()).ToArray();
|
||||
: rawTempTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var cfg = new ColumnConfig
|
||||
{
|
||||
@@ -59,8 +57,8 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
Name = rd.GetString(1),
|
||||
Enabled = rd.GetBoolean(2),
|
||||
AdvisoryOnly = rd.GetBoolean(30),
|
||||
FeedTag = rd.GetString(3).ToLowerInvariant(),
|
||||
PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4).ToLowerInvariant(),
|
||||
FeedTag = rd.GetString(3),
|
||||
PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4),
|
||||
LevelTags = levelTags,
|
||||
ScanSec = rd.GetDouble(6),
|
||||
FeedFilterTauSec = rd.GetDouble(7),
|
||||
@@ -72,10 +70,10 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
ProductKey = rd.GetString(13),
|
||||
Streams = Array.Empty<StreamConfig>(),
|
||||
TempTags = tempTags,
|
||||
SensitiveTrayTag = rd.IsDBNull(15) ? null : rd.GetString(15).ToLowerInvariant(),
|
||||
SensitiveTrayTag = rd.IsDBNull(15) ? null : rd.GetString(15),
|
||||
DTdP = rd.GetDouble(16),
|
||||
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),
|
||||
BiasMaWindowSec = rd.GetDouble(20),
|
||||
RecoveryEnabled = rd.GetBoolean(21),
|
||||
@@ -85,7 +83,7 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
RecoverySettleSec = rd.GetDouble(25),
|
||||
ReturnRampSec = rd.GetDouble(26),
|
||||
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),
|
||||
TempHighLimit = rd.GetDouble(31),
|
||||
};
|
||||
@@ -111,9 +109,9 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
entry.streams.Add(new StreamConfig
|
||||
{
|
||||
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,
|
||||
LevelTag = rd.IsDBNull(14) ? null : rd.GetString(14).ToLowerInvariant(),
|
||||
LevelTag = rd.IsDBNull(14) ? null : rd.GetString(14),
|
||||
IsReflux = rd.GetBoolean(15),
|
||||
RecoverySp = rd.IsDBNull(16) ? double.NaN : rd.GetDouble(16),
|
||||
SpNodeId = rd.IsDBNull(17) ? null : rd.GetString(17),
|
||||
@@ -176,22 +174,22 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
@deltaPTag,@deltaPFlood,@tempHigh)
|
||||
RETURNING 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,"@lvl",levelTags.ToLowerInvariant());
|
||||
P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled); P(cmd,"@feed",cfg.FeedTag);
|
||||
P(cmd,"@pres",(object?)cfg.PressureTag); P(cmd,"@lvl",levelTags);
|
||||
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
||||
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,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
||||
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,"@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,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
||||
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
||||
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
||||
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,"@tempHigh",cfg.TempHighLimit);
|
||||
id = Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
|
||||
@@ -215,21 +213,21 @@ public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
||||
WHERE id=@id
|
||||
""";
|
||||
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,"@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,"@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,"@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,"@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,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
||||
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
||||
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
||||
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,"@tempHigh",cfg.TempHighLimit);
|
||||
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,
|
||||
@isReflux,@recSp,@spNode)
|
||||
""";
|
||||
P(ins,"@cid",id); P(ins,"@key",s.Key); P(ins,"@flow",s.FlowTag.ToLowerInvariant());
|
||||
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,"@cid",id); P(ins,"@key",s.Key); P(ins,"@flow",s.FlowTag);
|
||||
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,"@rup",s.RateUpPerMin); P(ins,"@rdn",s.RateDnPerMin); P(ins,"@rfp",s.RefluxFromProduct);
|
||||
P(ins,"@grade",s.Grade.ToString());
|
||||
|
||||
@@ -176,17 +176,25 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
||||
{
|
||||
string PvTag(string baseTag)
|
||||
{
|
||||
var t = baseTag.ToLowerInvariant();
|
||||
return t.EndsWith(".pv") ? t : t + ".pv";
|
||||
if (string.IsNullOrWhiteSpace(baseTag)) return baseTag; // 빈 태그는 그대로(호출부가 처리)
|
||||
var result = baseTag.EndsWith(".pv", StringComparison.OrdinalIgnoreCase) ? baseTag : baseTag + ".pv";
|
||||
return result.ToUpperInvariant(); // register map 기준 대문자 통일
|
||||
}
|
||||
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 };
|
||||
if (cfg.PressureTag is not null) tags.Add(PvTag(cfg.PressureTag));
|
||||
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.Select(s => PvTag(s.FlowTag)));
|
||||
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)
|
||||
|
||||
// WP6: DeltaPTag 미지정 시 PressureTag로부터 탑저압 파생
|
||||
@@ -194,8 +202,8 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
||||
if (cfg.DeltaPTag is null && cfg.PressureTag is not null)
|
||||
{
|
||||
// pica-{n} → pi-{n}b (C-6111 명명 규약)
|
||||
var p = cfg.PressureTag.ToLowerInvariant();
|
||||
if (p.StartsWith("pica-"))
|
||||
var p = cfg.PressureTag;
|
||||
if (p.StartsWith("pica-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var num = p["pica-".Length..];
|
||||
derivedBottomTag = "pi-" + num + "b";
|
||||
@@ -204,14 +212,14 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
||||
}
|
||||
|
||||
var rows = (await db.GetRealtimeRecordsByTagNamesAsync(tags))
|
||||
.ToDictionary(r => r.TagName.ToLowerInvariant(), r => r);
|
||||
.ToDictionary(r => r.TagName, r => r);
|
||||
|
||||
TagSample Sample(string baseTag)
|
||||
{
|
||||
var tag = PvTag(baseTag);
|
||||
if (_sim.Enabled && _sim.TryGet(tag, out var sov)) // WP0 확장: override 우선(신선 처리)
|
||||
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))
|
||||
{
|
||||
bool fresh = (DateTime.UtcNow - r.Timestamp.ToUniversalTime()).TotalSeconds <= cfg.StaleSec;
|
||||
@@ -223,7 +231,7 @@ public sealed class FeedforwardSupervisor : BackgroundService
|
||||
// WO-3: .op 등 비-.pv 태그를 접미사 강제 없이 그대로 읽음
|
||||
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 우선
|
||||
return new TagSample(tag, sov, Good: true, DateTime.UtcNow);
|
||||
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)
|
||||
{
|
||||
foreach (var kv in values) _values[kv.Key.ToLowerInvariant()] = kv.Value;
|
||||
foreach (var kv in values) _values[kv.Key] = kv.Value;
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class SimOverrideStore : ISimOverrideStore
|
||||
_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);
|
||||
}
|
||||
|
||||
@@ -76,10 +76,19 @@ public class Hc900RealtimeService : BackgroundService, IHc900RealtimeService
|
||||
? await client.ReadTagsAsync(new ReadTagsRequest { TagNames = { tagNames } })
|
||||
: 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;
|
||||
|
||||
if (resp.Values.Count > 0)
|
||||
if (_connected[controllerId] && resp.Values.Count > 0)
|
||||
await BatchUpdateRealtimeTableAsync(controllerId, resp.Values, mapping, ct);
|
||||
|
||||
// poll count는 gRPC ReadTags + DB write 모두 성공한 후에만 증가
|
||||
@@ -201,7 +210,8 @@ public class Hc900RealtimeService : BackgroundService, IHc900RealtimeService
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user