Added PLC status and force data, plc data
This commit is contained in:
parent
e3ac53da44
commit
7614390b74
363
main.go
363
main.go
|
|
@ -47,9 +47,11 @@ type PLCConfig struct {
|
|||
}
|
||||
|
||||
type ThresholdsConfig struct {
|
||||
WarningPercent float64 `yaml:"warning_percent"`
|
||||
CriticalPercent float64 `yaml:"critical_percent"`
|
||||
GaugeMaxPercent float64 `yaml:"gauge_max_percent"`
|
||||
WarningPercent float64 `yaml:"warning_percent"`
|
||||
CriticalPercent float64 `yaml:"critical_percent"`
|
||||
GaugeMaxPercent float64 `yaml:"gauge_max_percent"`
|
||||
ImbalanceWarningPercent float64 `yaml:"imbalance_warning_percent"`
|
||||
ImbalanceCriticalPercent float64 `yaml:"imbalance_critical_percent"`
|
||||
|
||||
LegacyWarningKn float64 `yaml:"warning_kn,omitempty"`
|
||||
LegacyCriticalKn float64 `yaml:"critical_kn,omitempty"`
|
||||
|
|
@ -88,7 +90,9 @@ type DBConfig struct {
|
|||
|
||||
func defaultConfig() Config {
|
||||
return Config{
|
||||
Server: ServerConfig{ListenAddr: ":8080"},
|
||||
Server: ServerConfig{
|
||||
ListenAddr: ":8080",
|
||||
},
|
||||
PLC: PLCConfig{
|
||||
IP: "192.168.0.1",
|
||||
DBNum: 1001,
|
||||
|
|
@ -100,14 +104,20 @@ func defaultConfig() Config {
|
|||
ReconnectDelaySec: 5,
|
||||
},
|
||||
Thresholds: ThresholdsConfig{
|
||||
WarningPercent: 80,
|
||||
CriticalPercent: 95,
|
||||
GaugeMaxPercent: 130,
|
||||
WarningPercent: 80,
|
||||
CriticalPercent: 95,
|
||||
GaugeMaxPercent: 130,
|
||||
ImbalanceWarningPercent: 10,
|
||||
ImbalanceCriticalPercent: 20,
|
||||
},
|
||||
Trend: TrendConfig{
|
||||
Minutes: 5,
|
||||
},
|
||||
Press: PressConfig{
|
||||
MAX_TONNAGE: 64,
|
||||
},
|
||||
Trend: TrendConfig{Minutes: 5},
|
||||
Press: PressConfig{MAX_TONNAGE: 64},
|
||||
UI: UIConfig{
|
||||
Title: "Force Monitor v0.2.0 - pre-alpha",
|
||||
Title: "Force Monitor",
|
||||
Subtitle: "Siemens S7-1215C • Piezo peak/stroke input • PLC values in % • kN calculated from MAX_TONNAGE",
|
||||
LeftLabel: "LEVI STEBER",
|
||||
RightLabel: "DESNI STEBER",
|
||||
|
|
@ -163,6 +173,7 @@ func normalizeConfig(cfg *Config) {
|
|||
if cfg.Thresholds.GaugeMaxPercent <= 0 && cfg.Thresholds.LegacyMaxKn > 0 {
|
||||
cfg.Thresholds.GaugeMaxPercent = cfg.Thresholds.LegacyMaxKn
|
||||
}
|
||||
|
||||
if cfg.Thresholds.WarningPercent <= 0 {
|
||||
cfg.Thresholds.WarningPercent = def.Thresholds.WarningPercent
|
||||
}
|
||||
|
|
@ -172,6 +183,15 @@ func normalizeConfig(cfg *Config) {
|
|||
if cfg.Thresholds.GaugeMaxPercent <= 0 {
|
||||
cfg.Thresholds.GaugeMaxPercent = def.Thresholds.GaugeMaxPercent
|
||||
}
|
||||
if cfg.Thresholds.ImbalanceWarningPercent <= 0 {
|
||||
cfg.Thresholds.ImbalanceWarningPercent = def.Thresholds.ImbalanceWarningPercent
|
||||
}
|
||||
if cfg.Thresholds.ImbalanceCriticalPercent <= 0 {
|
||||
cfg.Thresholds.ImbalanceCriticalPercent = def.Thresholds.ImbalanceCriticalPercent
|
||||
}
|
||||
if cfg.Thresholds.ImbalanceCriticalPercent < cfg.Thresholds.ImbalanceWarningPercent {
|
||||
cfg.Thresholds.ImbalanceCriticalPercent = cfg.Thresholds.ImbalanceWarningPercent
|
||||
}
|
||||
|
||||
if cfg.Trend.Minutes <= 0 {
|
||||
cfg.Trend.Minutes = def.Trend.Minutes
|
||||
|
|
@ -316,18 +336,20 @@ type HistoryResponse struct {
|
|||
}
|
||||
|
||||
type PageData struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
LeftLabel string
|
||||
RightLabel string
|
||||
UnitForce string
|
||||
UnitPct string
|
||||
MaxTonnage float64
|
||||
WarningPercent float64
|
||||
CriticalPercent float64
|
||||
GaugeMaxPercent float64
|
||||
PollMs int
|
||||
DefaultWindow string
|
||||
Title string
|
||||
Subtitle string
|
||||
LeftLabel string
|
||||
RightLabel string
|
||||
UnitForce string
|
||||
UnitPct string
|
||||
MaxTonnage float64
|
||||
WarningPercent float64
|
||||
CriticalPercent float64
|
||||
GaugeMaxPercent float64
|
||||
ImbalanceWarningPercent float64
|
||||
ImbalanceCriticalPercent float64
|
||||
PollMs int
|
||||
DefaultWindow string
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -390,6 +412,41 @@ func enqueueSample(s Sample) {
|
|||
}
|
||||
}
|
||||
|
||||
func ensureColumn(database *sql.DB, tableName, columnName, definition string) error {
|
||||
rows, err := database.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
found := false
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name string
|
||||
var ctype string
|
||||
var notNull int
|
||||
var dfltValue sql.NullString
|
||||
var pk int
|
||||
|
||||
if err := rows.Scan(&cid, &name, &ctype, ¬Null, &dfltValue, &pk); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == columnName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = database.Exec(fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", tableName, columnName, definition))
|
||||
return err
|
||||
}
|
||||
|
||||
func initDatabase(dbPath string) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf("file:%s?_busy_timeout=%d&_foreign_keys=on", filepath.ToSlash(dbPath), cfg.DB.BusyTimeoutMs)
|
||||
database, err := sql.Open("sqlite3", dsn)
|
||||
|
|
@ -426,8 +483,8 @@ CREATE TABLE IF NOT EXISTS samples (
|
|||
sila_r_kn REAL NOT NULL,
|
||||
sum_pct REAL NOT NULL,
|
||||
sum_kn REAL NOT NULL,
|
||||
imbalance_pct REAL NOT NULL,
|
||||
bias_pct REAL NOT NULL
|
||||
imbalance_pct REAL NOT NULL DEFAULT 0,
|
||||
bias_pct REAL NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_samples_ts ON samples(ts);
|
||||
`
|
||||
|
|
@ -436,6 +493,15 @@ CREATE INDEX IF NOT EXISTS idx_samples_ts ON samples(ts);
|
|||
return nil, fmt.Errorf("create schema: %w", err)
|
||||
}
|
||||
|
||||
if err := ensureColumn(database, "samples", "imbalance_pct", "REAL NOT NULL DEFAULT 0"); err != nil {
|
||||
_ = database.Close()
|
||||
return nil, fmt.Errorf("ensure imbalance_pct column: %w", err)
|
||||
}
|
||||
if err := ensureColumn(database, "samples", "bias_pct", "REAL NOT NULL DEFAULT 0"); err != nil {
|
||||
_ = database.Close()
|
||||
return nil, fmt.Errorf("ensure bias_pct column: %w", err)
|
||||
}
|
||||
|
||||
return database, nil
|
||||
}
|
||||
|
||||
|
|
@ -664,7 +730,6 @@ func queryHistory(window time.Duration) ([]HistoryPoint, error) {
|
|||
if len(points) <= cfg.DB.MaxChartPoints {
|
||||
return points, nil
|
||||
}
|
||||
|
||||
return downsamplePoints(points, cfg.DB.MaxChartPoints), nil
|
||||
}
|
||||
|
||||
|
|
@ -726,18 +791,20 @@ func apiHistory(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func serveUI(w http.ResponseWriter, r *http.Request) {
|
||||
data := PageData{
|
||||
Title: cfg.UI.Title,
|
||||
Subtitle: cfg.UI.Subtitle,
|
||||
LeftLabel: cfg.UI.LeftLabel,
|
||||
RightLabel: cfg.UI.RightLabel,
|
||||
UnitForce: cfg.UI.UnitForce,
|
||||
UnitPct: cfg.UI.UnitPct,
|
||||
MaxTonnage: cfg.Press.MAX_TONNAGE,
|
||||
WarningPercent: cfg.Thresholds.WarningPercent,
|
||||
CriticalPercent: cfg.Thresholds.CriticalPercent,
|
||||
GaugeMaxPercent: cfg.Thresholds.GaugeMaxPercent,
|
||||
PollMs: cfg.PLC.PollMs,
|
||||
DefaultWindow: fmt.Sprintf("%dm", cfg.Trend.Minutes),
|
||||
Title: cfg.UI.Title,
|
||||
Subtitle: cfg.UI.Subtitle,
|
||||
LeftLabel: cfg.UI.LeftLabel,
|
||||
RightLabel: cfg.UI.RightLabel,
|
||||
UnitForce: cfg.UI.UnitForce,
|
||||
UnitPct: cfg.UI.UnitPct,
|
||||
MaxTonnage: cfg.Press.MAX_TONNAGE,
|
||||
WarningPercent: cfg.Thresholds.WarningPercent,
|
||||
CriticalPercent: cfg.Thresholds.CriticalPercent,
|
||||
GaugeMaxPercent: cfg.Thresholds.GaugeMaxPercent,
|
||||
ImbalanceWarningPercent: cfg.Thresholds.ImbalanceWarningPercent,
|
||||
ImbalanceCriticalPercent: cfg.Thresholds.ImbalanceCriticalPercent,
|
||||
PollMs: cfg.PLC.PollMs,
|
||||
DefaultWindow: fmt.Sprintf("%dm", cfg.Trend.Minutes),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
|
@ -835,7 +902,7 @@ const uiHTML = `<!DOCTYPE html>
|
|||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
height: 390px;
|
||||
height: 360px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
|
@ -856,6 +923,69 @@ const uiHTML = `<!DOCTYPE html>
|
|||
width: min(92vw, 1800px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-radius: 20px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid rgba(255,255,255,0.09);
|
||||
background: rgba(255,255,255,0.04);
|
||||
transition: 180ms ease;
|
||||
}
|
||||
|
||||
.summary-card.ok {
|
||||
border-color: rgba(34,197,94,0.35);
|
||||
box-shadow: 0 0 0 1px rgba(34,197,94,0.08) inset, 0 0 26px rgba(34,197,94,0.06);
|
||||
}
|
||||
|
||||
.summary-card.warning {
|
||||
border-color: rgba(234,179,8,0.35);
|
||||
box-shadow: 0 0 0 1px rgba(234,179,8,0.08) inset, 0 0 26px rgba(234,179,8,0.06);
|
||||
}
|
||||
|
||||
.summary-card.critical {
|
||||
border-color: rgba(239,68,68,0.35);
|
||||
box-shadow: 0 0 0 1px rgba(239,68,68,0.08) inset, 0 0 26px rgba(239,68,68,0.06);
|
||||
}
|
||||
|
||||
.summary-card.neutral {
|
||||
border-color: rgba(113,113,122,0.35);
|
||||
box-shadow: 0 0 0 1px rgba(113,113,122,0.08) inset;
|
||||
}
|
||||
|
||||
.summary-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.summary-dot.ok {
|
||||
background: #10b981;
|
||||
box-shadow: 0 0 14px rgba(16,185,129,0.55);
|
||||
}
|
||||
|
||||
.summary-dot.warning {
|
||||
background: #f59e0b;
|
||||
box-shadow: 0 0 14px rgba(245,158,11,0.55);
|
||||
}
|
||||
|
||||
.summary-dot.critical {
|
||||
background: #ef4444;
|
||||
box-shadow: 0 0 14px rgba(239,68,68,0.55);
|
||||
}
|
||||
|
||||
.summary-dot.neutral {
|
||||
background: #71717a;
|
||||
box-shadow: 0 0 12px rgba(113,113,122,0.35);
|
||||
}
|
||||
|
||||
.summary-status.ok { color: #34d399; }
|
||||
.summary-status.warning { color: #facc15; }
|
||||
.summary-status.critical { color: #f87171; }
|
||||
.summary-status.neutral { color: #a1a1aa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -863,7 +993,7 @@ const uiHTML = `<!DOCTYPE html>
|
|||
<div id="alarm-banner" class="hidden mb-6 bg-red-600/90 border border-red-500 text-white px-8 py-4 rounded-2xl flex items-center justify-between text-lg font-medium">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl">⚠️</span>
|
||||
<span id="alarm-text">CRITICAL FORCE ALARM ACTIVE</span>
|
||||
<span id="alarm-text">CRITICAL ALARM ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -886,6 +1016,41 @@ const uiHTML = `<!DOCTYPE html>
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-8">
|
||||
<div id="summary-force-card" class="summary-card neutral">
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="summary-force-dot" class="summary-dot neutral"></div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-[0.22em] text-zinc-500">FORCE</div>
|
||||
<div id="summary-force-text" class="summary-status neutral font-semibold mt-1">NO DATA</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="summary-force-value" class="font-mono text-zinc-200 text-lg">--</div>
|
||||
</div>
|
||||
|
||||
<div id="summary-imbalance-card" class="summary-card neutral">
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="summary-imbalance-dot" class="summary-dot neutral"></div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-[0.22em] text-zinc-500">IMBALANCE</div>
|
||||
<div id="summary-imbalance-text" class="summary-status neutral font-semibold mt-1">NO DATA</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="summary-imbalance-value" class="font-mono text-zinc-200 text-lg">--</div>
|
||||
</div>
|
||||
|
||||
<div id="summary-plc-card" class="summary-card neutral">
|
||||
<div class="flex items-center gap-3">
|
||||
<div id="summary-plc-dot" class="summary-dot neutral"></div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-[0.22em] text-zinc-500">PLC</div>
|
||||
<div id="summary-plc-text" class="summary-status neutral font-semibold mt-1">OFFLINE</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="summary-plc-value" class="font-mono text-zinc-200 text-lg">Disconnected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass border border-white/10 rounded-3xl p-6 md:p-8 mb-8">
|
||||
<div class="flex flex-col xl:flex-row xl:items-center xl:justify-between gap-6">
|
||||
<div>
|
||||
|
|
@ -915,9 +1080,9 @@ const uiHTML = `<!DOCTYPE html>
|
|||
</div>
|
||||
|
||||
<div class="bg-zinc-900/60 rounded-2xl px-5 py-4 border border-zinc-800">
|
||||
<div class="text-zinc-500 text-xs uppercase tracking-widest">DB</div>
|
||||
<div class="text-sm font-mono text-zinc-300 mt-2">SQLite WAL</div>
|
||||
<div class="text-xs text-zinc-500 mt-2 font-mono">non-blocking writer</div>
|
||||
<div class="text-zinc-500 text-xs uppercase tracking-widest">LIMITS</div>
|
||||
<div class="text-sm font-mono text-zinc-300 mt-2">Force W {{printf "%.0f" .WarningPercent}} / C {{printf "%.0f" .CriticalPercent}}</div>
|
||||
<div class="text-xs text-zinc-500 mt-2 font-mono">Imb W {{printf "%.0f" .ImbalanceWarningPercent}} / C {{printf "%.0f" .ImbalanceCriticalPercent}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1003,6 +1168,8 @@ const uiHTML = `<!DOCTYPE html>
|
|||
const WARNING_PERCENT = {{.WarningPercent}};
|
||||
const CRITICAL_PERCENT = {{.CriticalPercent}};
|
||||
const GAUGE_MAX_PERCENT = {{.GaugeMaxPercent}};
|
||||
const IMBALANCE_WARNING_PERCENT = {{.ImbalanceWarningPercent}};
|
||||
const IMBALANCE_CRITICAL_PERCENT = {{.ImbalanceCriticalPercent}};
|
||||
const UNIT_FORCE = '{{.UnitForce}}';
|
||||
const UNIT_PCT = '{{.UnitPct}}';
|
||||
const POLL_MS = {{.PollMs}};
|
||||
|
|
@ -1107,15 +1274,15 @@ const uiHTML = `<!DOCTYPE html>
|
|||
const { ctx, w, h } = prepCanvas(canvas);
|
||||
|
||||
const cx = w / 2;
|
||||
const cy = h * 0.55;
|
||||
const radius = Math.min(w, h) * 0.37;
|
||||
const cy = h * 0.57;
|
||||
const radius = Math.min(w, h) * 0.34;
|
||||
const trackWidth = Math.max(18, radius * 0.16);
|
||||
const value = clamp(Number(percentValue) || 0, 0, GAUGE_MAX_PERCENT);
|
||||
const valueAngle = valueToAngle(value);
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius + 24, 0, Math.PI * 2);
|
||||
ctx.arc(cx, cy, radius + 22, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.015)';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.45)';
|
||||
ctx.shadowBlur = 30;
|
||||
|
|
@ -1199,34 +1366,47 @@ const uiHTML = `<!DOCTYPE html>
|
|||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fill();
|
||||
|
||||
const majorTickInner = radius * 0.72;
|
||||
const centerPlateRadius = majorTickInner - 18;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy + 8, radius * 0.36, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(9,9,11,0.85)';
|
||||
ctx.arc(cx, cy + 8, centerPlateRadius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(9,9,11,0.90)';
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1;
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.10)';
|
||||
ctx.stroke();
|
||||
|
||||
const valueText = value.toFixed(1);
|
||||
let valueFontPx = 52;
|
||||
if (value >= 100) valueFontPx = 46;
|
||||
if (w < 420) valueFontPx -= 4;
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '700 48px Space Grotesk, Inter, sans-serif';
|
||||
ctx.fillText(value.toFixed(1), cx, cy - 8);
|
||||
ctx.font = '700 ' + valueFontPx + 'px Space Grotesk, Inter, sans-serif';
|
||||
ctx.fillText(valueText, cx, cy - 6);
|
||||
|
||||
ctx.fillStyle = sideAccent;
|
||||
ctx.font = '700 18px Inter, sans-serif';
|
||||
ctx.fillText(UNIT_PCT, cx, cy + 26);
|
||||
ctx.fillText(UNIT_PCT, cx, cy + 28);
|
||||
|
||||
ctx.fillStyle = '#a1a1aa';
|
||||
ctx.font = '600 16px Inter, sans-serif';
|
||||
ctx.fillText((Number(knValue) || 0).toFixed(1) + ' ' + UNIT_FORCE, cx, cy + 52);
|
||||
ctx.fillText((Number(knValue) || 0).toFixed(1) + ' ' + UNIT_FORCE, cx, cy + 54);
|
||||
}
|
||||
|
||||
function getZone(percentValue) {
|
||||
if (percentValue >= CRITICAL_PERCENT) return 'critical';
|
||||
if (percentValue >= WARNING_PERCENT) return 'warning';
|
||||
return 'normal';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function getImbalanceZone(value) {
|
||||
if (value >= IMBALANCE_CRITICAL_PERCENT) return 'critical';
|
||||
if (value >= IMBALANCE_WARNING_PERCENT) return 'warning';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function setStatusConnected(connected) {
|
||||
|
|
@ -1283,25 +1463,73 @@ const uiHTML = `<!DOCTYPE html>
|
|||
return 'Last update: ' + hh + ':' + mm + ':' + ss + '.' + ms;
|
||||
}
|
||||
|
||||
function updateAlarmBanner(leftPercent, rightPercent) {
|
||||
function setSummaryCard(kind, zone, text, value) {
|
||||
const card = document.getElementById('summary-' + kind + '-card');
|
||||
const dot = document.getElementById('summary-' + kind + '-dot');
|
||||
const status = document.getElementById('summary-' + kind + '-text');
|
||||
const val = document.getElementById('summary-' + kind + '-value');
|
||||
|
||||
card.className = 'summary-card ' + zone;
|
||||
dot.className = 'summary-dot ' + zone;
|
||||
status.className = 'summary-status ' + zone + ' font-semibold mt-1';
|
||||
status.textContent = text;
|
||||
val.textContent = value;
|
||||
}
|
||||
|
||||
function updateSummaryBar(connected, leftPercent, rightPercent, imbalance) {
|
||||
if (!connected) {
|
||||
setSummaryCard('force', 'neutral', 'NO DATA', '--');
|
||||
setSummaryCard('imbalance', 'neutral', 'NO DATA', '--');
|
||||
setSummaryCard('plc', 'critical', 'OFFLINE', 'Disconnected');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxForce = Math.max(leftPercent, rightPercent);
|
||||
const forceZone = getZone(maxForce);
|
||||
const dominantSide = leftPercent >= rightPercent ? 'L' : 'R';
|
||||
const forceText = forceZone === 'ok' ? 'OK' : forceZone === 'warning' ? 'WARNING' : 'CRITICAL';
|
||||
setSummaryCard('force', forceZone, forceText, 'Max ' + maxForce.toFixed(1) + UNIT_PCT + ' (' + dominantSide + ')');
|
||||
|
||||
const imbalanceZone = getImbalanceZone(imbalance);
|
||||
const imbalanceText = imbalanceZone === 'ok' ? 'OK' : imbalanceZone === 'warning' ? 'WARNING' : 'CRITICAL';
|
||||
setSummaryCard('imbalance', imbalanceZone, imbalanceText, imbalance.toFixed(1) + UNIT_PCT);
|
||||
|
||||
setSummaryCard('plc', 'ok', 'OK', 'Online');
|
||||
}
|
||||
|
||||
function updateAlarmBanner(leftPercent, rightPercent, imbalancePercent, connected) {
|
||||
const banner = document.getElementById('alarm-banner');
|
||||
const text = document.getElementById('alarm-text');
|
||||
|
||||
if (!connected) {
|
||||
banner.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const leftCritical = leftPercent >= CRITICAL_PERCENT;
|
||||
const rightCritical = rightPercent >= CRITICAL_PERCENT;
|
||||
const imbalanceCritical = imbalancePercent >= IMBALANCE_CRITICAL_PERCENT;
|
||||
|
||||
if (leftCritical || rightCritical) {
|
||||
if (leftCritical && rightCritical) {
|
||||
text.textContent = 'CRITICAL FORCE ALARM ACTIVE • LEFT + RIGHT';
|
||||
} else if (leftCritical) {
|
||||
text.textContent = 'CRITICAL FORCE ALARM ACTIVE • LEFT';
|
||||
} else {
|
||||
text.textContent = 'CRITICAL FORCE ALARM ACTIVE • RIGHT';
|
||||
}
|
||||
banner.classList.remove('hidden');
|
||||
} else {
|
||||
if (!leftCritical && !rightCritical && !imbalanceCritical) {
|
||||
banner.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (leftCritical && rightCritical) {
|
||||
parts.push('FORCE LEFT + RIGHT');
|
||||
} else if (leftCritical) {
|
||||
parts.push('FORCE LEFT');
|
||||
} else if (rightCritical) {
|
||||
parts.push('FORCE RIGHT');
|
||||
}
|
||||
|
||||
if (imbalanceCritical) {
|
||||
parts.push('IMBALANCE');
|
||||
}
|
||||
|
||||
text.textContent = 'CRITICAL ALARM ACTIVE • ' + parts.join(' • ');
|
||||
banner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function redrawGauges() {
|
||||
|
|
@ -1322,6 +1550,7 @@ const uiHTML = `<!DOCTYPE html>
|
|||
const d = await res.json();
|
||||
latestData = d;
|
||||
|
||||
const connected = !!d.connected;
|
||||
const leftPercent = Number(d.sila_l) || 0;
|
||||
const rightPercent = Number(d.sila_r) || 0;
|
||||
const leftKN = Number(d.sila_l_kn) || 0;
|
||||
|
|
@ -1331,7 +1560,7 @@ const uiHTML = `<!DOCTYPE html>
|
|||
const imbalance = Number(d.imbalance_percent) || 0;
|
||||
const bias = Number(d.bias_percent) || 0;
|
||||
|
||||
setStatusConnected(!!d.connected);
|
||||
setStatusConnected(connected);
|
||||
|
||||
document.querySelector('#digital-l .percent').textContent = leftPercent.toFixed(1);
|
||||
document.querySelector('#digital-l .kn').textContent = leftKN.toFixed(1) + ' ' + UNIT_FORCE;
|
||||
|
|
@ -1348,11 +1577,13 @@ const uiHTML = `<!DOCTYPE html>
|
|||
|
||||
applyChannelState('l', leftPercent);
|
||||
applyChannelState('r', rightPercent);
|
||||
updateAlarmBanner(leftPercent, rightPercent);
|
||||
updateSummaryBar(connected, leftPercent, rightPercent, imbalance);
|
||||
updateAlarmBanner(leftPercent, rightPercent, imbalance, connected);
|
||||
redrawGauges();
|
||||
} catch (err) {
|
||||
console.warn('Live fetch error:', err);
|
||||
setStatusConnected(false);
|
||||
updateSummaryBar(false, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue