first commit
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# ALS Alert Screen
|
||||
|
||||
Jednostronicowa aplikacja HTML/JS wyświetlająca alarmy maszyn na podstawie danych z WebSocket. Przeznaczona do montażu na monitorze halowym.
|
||||
|
||||
---
|
||||
|
||||
## Działanie
|
||||
|
||||
Strona nasłuchuje danych z WebSocket dla każdej skonfigurowanej maszyny. W zależności od statusów maszyn wyświetla jeden z dwóch widoków:
|
||||
|
||||
| Sytuacja | Widok |
|
||||
|----------|-------|
|
||||
| Brak maszyny w alarmie | Iframe z `http://diluals31/terminal/ift/5/PL/index` |
|
||||
| Co najmniej jedna maszyna w alarmie | Ekran alarmowy z kartami maszyn |
|
||||
|
||||
Alarm jest wykrywany gdy pole `status` w danych maszyny zawiera słowo **`alarm`** (bez rozróżnienia wielkości liter), np. `"Alarm - awaria"`, `"ALARM"`.
|
||||
|
||||
---
|
||||
|
||||
## Ekran alarmowy
|
||||
|
||||
- Karty maszyn wypełniają cały dostępny ekran w układzie **siatki N×M** (możliwie kwadratowej).
|
||||
- Numer maszyny jest wyświetlany bardzo dużą czcionką, widoczną z daleka — rozmiar skaluje się automatycznie zarówno do wymiarów ekranu (`vw`/`vh`), jak i szerokości karty (`cqw`), więc tekst nigdy nie jest ucięty.
|
||||
- Pod numerem wyświetlany jest pełny tekst statusu.
|
||||
- Pasek u góry pokazuje liczbę maszyn w alarmie i miga.
|
||||
|
||||
### Układ siatki w zależności od liczby alarmów
|
||||
|
||||
| Alarmy | Kolumny | Wiersze |
|
||||
|--------|---------|---------|
|
||||
| 1 | 1 | 1 |
|
||||
| 2 | 2 | 1 |
|
||||
| 3 | 2 | 2 |
|
||||
| 4 | 2 | 2 |
|
||||
| 5 | 3 | 2 |
|
||||
| 6 | 3 | 2 |
|
||||
| 9 | 3 | 3 |
|
||||
|
||||
---
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
Wszystkie parametry konfiguracyjne znajdują się na początku sekcji `<script>` w pliku `als_alert_screen.html`.
|
||||
|
||||
### Lista maszyn
|
||||
|
||||
```js
|
||||
const MACHINES = [
|
||||
"1.64", "1.76", "2.60", ...
|
||||
];
|
||||
```
|
||||
|
||||
Każdy wpis to numer maszyny używany w adresie WebSocket. Aby dodać lub usunąć maszynę — edytuj tę tablicę.
|
||||
|
||||
### Adres WebSocket
|
||||
|
||||
```js
|
||||
const WS_BASE = "ws://luport.local/ws/als-data/";
|
||||
```
|
||||
|
||||
Dla każdej maszyny otwierane jest połączenie pod adresem `WS_BASE + id + "/"`,
|
||||
np. `ws://luport.local/ws/als-data/2.60/`.
|
||||
|
||||
### Format wiadomości WebSocket
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "machine_update",
|
||||
"machine": "2.60",
|
||||
"data": {
|
||||
"machine": "2.60",
|
||||
"status": "Automatyczny",
|
||||
"mold": 6447,
|
||||
"variant": [3],
|
||||
"cavities": 2,
|
||||
"order": "202605350 0010",
|
||||
"qty_planned": 8400,
|
||||
"qty_produced_ok": 2132,
|
||||
"qty_produced_nok": 10,
|
||||
"cycle_time_planned": 58.0,
|
||||
"cycle_time_average": 57.4,
|
||||
"progress": 25
|
||||
},
|
||||
"timestamp": "2026-05-28T12:00:32.898808"
|
||||
}
|
||||
```
|
||||
|
||||
Strona obsługuje tylko wiadomości z `"type": "machine_update"`. Pozostałe są ignorowane.
|
||||
|
||||
### Czas ponownego połączenia
|
||||
|
||||
```js
|
||||
const RECONNECT_MS = 5000;
|
||||
```
|
||||
|
||||
Przy zerwaniu połączenia WS strona automatycznie próbuje ponownie po 5 sekundach.
|
||||
|
||||
---
|
||||
|
||||
## Tryb debug
|
||||
|
||||
Służy do podglądu ekranu alarmowego bez rzeczywistych danych WS.
|
||||
|
||||
### Przez URL
|
||||
|
||||
Otwórz stronę z parametrem `?debug`:
|
||||
|
||||
```
|
||||
als_alert_screen.html?debug
|
||||
```
|
||||
|
||||
Ładuje przykładowe dane dla maszyn `2.60`, `3.22`, `4.09` i natychmiast pokazuje ekran alarmowy. Połączenia WS **nie są** nawiązywane. Pasek statusu wyświetla `[DEBUG] widok alarmowy`.
|
||||
|
||||
### Przez konsolę przeglądarki
|
||||
|
||||
W każdej chwili można przełączyć widok bez przeładowania strony:
|
||||
|
||||
```js
|
||||
// włącz widok alarmowy
|
||||
DEBUG_ALARM = true; updateDisplay();
|
||||
|
||||
// wróć do normalnego widoku
|
||||
DEBUG_ALARM = false; updateDisplay();
|
||||
```
|
||||
|
||||
### Dane przykładowe
|
||||
|
||||
Maszyny i dane używane w trybie debug są zdefiniowane w zmiennych `DEBUG_MACHINES` i `DEBUG_DATA` — można je swobodnie edytować.
|
||||
|
||||
---
|
||||
|
||||
## Pasek statusu
|
||||
|
||||
W prawym dolnym rogu ekranu widoczny jest mały pasek z liczbą aktywnych połączeń WebSocket:
|
||||
|
||||
```
|
||||
WS: 32 / 35
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Wymagania
|
||||
|
||||
- Przeglądarka z obsługą WebSocket, CSS Container Queries (`cqw`) i `min()` w CSS — spełnia każda nowoczesna przeglądarka (Chrome 105+, Firefox 110+, Edge 105+).
|
||||
- Dostęp sieciowy do `luport.local` (WebSocket) oraz `diluals31` (iframe).
|
||||
@@ -0,0 +1,330 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ALS Alert Screen</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── NORMAL VIEW ──────────────────────────────── */
|
||||
#normal-view {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── ALARM VIEW ──────────────────────────────── */
|
||||
#alarm-view {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #0d0000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#alarm-view.active { display: flex; }
|
||||
|
||||
.alarm-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 18px 24px;
|
||||
background: #1e0000;
|
||||
border-bottom: 3px solid #ff0000;
|
||||
animation: headerBlink 1s step-start infinite;
|
||||
}
|
||||
@keyframes headerBlink {
|
||||
0%, 100% { background: #1e0000; }
|
||||
50% { background: #3a0000; }
|
||||
}
|
||||
|
||||
.alarm-icon { font-size: clamp(28px, 4vw, 56px); }
|
||||
|
||||
.alarm-title {
|
||||
font-size: clamp(22px, 3.5vw, 52px);
|
||||
font-weight: 900;
|
||||
color: #ff2222;
|
||||
letter-spacing: .15em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.alarm-count {
|
||||
font-size: clamp(18px, 2.5vw, 38px);
|
||||
font-weight: 700;
|
||||
color: #ff8888;
|
||||
background: #3a0000;
|
||||
border: 2px solid #ff3333;
|
||||
border-radius: 8px;
|
||||
padding: 4px 18px;
|
||||
}
|
||||
|
||||
/* cards area */
|
||||
#machines-grid {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
/* rows, columns & --cols/--rows set dynamically in JS */
|
||||
gap: 3px;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.machine-card {
|
||||
background: #1a0000;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
overflow: hidden;
|
||||
container-type: inline-size;
|
||||
animation: cardGlow 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes cardGlow {
|
||||
0%, 100% { box-shadow: 0 0 12px rgba(255,0,0,.25); }
|
||||
50% { box-shadow: 0 0 30px rgba(255,0,0,.70); }
|
||||
}
|
||||
|
||||
/* ── big machine number – skala przez --cols / --rows + cqw ── */
|
||||
.card-machine-id {
|
||||
font-size: min(calc(55vw / var(--cols, 1)), calc(55vh / var(--rows, 1)), 35cqw);
|
||||
font-weight: 900;
|
||||
color: #ff2222;
|
||||
line-height: 1;
|
||||
letter-spacing: .04em;
|
||||
text-align: center;
|
||||
text-shadow: 0 0 24px rgba(255,0,0,.6);
|
||||
}
|
||||
|
||||
.card-status {
|
||||
font-size: min(calc(7vw / var(--cols, 1)), calc(7vh / var(--rows, 1)), 5cqw);
|
||||
font-weight: 700;
|
||||
color: #ff9999;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── detail grid ── */
|
||||
.card-info {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px 14px;
|
||||
border-top: 1px solid #3a0000;
|
||||
padding-top: 8px;
|
||||
}
|
||||
.info-item { display: flex; flex-direction: column; }
|
||||
.info-label {
|
||||
font-size: clamp(9px, .8vw, 12px);
|
||||
color: #664444;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
.info-value {
|
||||
font-size: clamp(11px, 1.1vw, 16px);
|
||||
color: #ddcccc;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.progress-row {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 12px;
|
||||
background: #3a0000;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #990000, #ff4444);
|
||||
border-radius: 6px;
|
||||
transition: width .8s ease;
|
||||
}
|
||||
.progress-label {
|
||||
text-align: right;
|
||||
font-size: clamp(9px, .8vw, 11px);
|
||||
color: #775555;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* ── STATUS BAR ──────────────────────────────── */
|
||||
#status-bar {
|
||||
position: fixed;
|
||||
bottom: 6px;
|
||||
right: 10px;
|
||||
font-size: 11px;
|
||||
color: #444;
|
||||
background: rgba(0,0,0,.6);
|
||||
padding: 3px 10px;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<iframe id="normal-view" src="http://diluals31/terminal/ift/5/PL/index" allowfullscreen></iframe>
|
||||
|
||||
<div id="alarm-view">
|
||||
<div class="alarm-header">
|
||||
<span class="alarm-icon">⚠</span>
|
||||
<span class="alarm-title">Alarm</span>
|
||||
<span class="alarm-count" id="alarm-count">0</span>
|
||||
<span class="alarm-icon">⚠</span>
|
||||
</div>
|
||||
<div id="machines-grid"></div>
|
||||
</div>
|
||||
|
||||
<div id="status-bar">WS: 0 / 0</div>
|
||||
|
||||
<script>
|
||||
const MACHINES = [
|
||||
"1.64","1.76","2.60","2.63","2.67","2.72","2.74",
|
||||
"3.01","3.04","3.08","3.22","3.26","3.51","3.55","3.56",
|
||||
"3.59","3.61","3.66","3.69","3.70","3.71","3.73","3.75",
|
||||
"4.05","4.09","4.14","4.52","4.53","4.57","4.58","4.62",
|
||||
"4.65","4.68","5.29","5.54"
|
||||
];
|
||||
|
||||
const WS_BASE = "ws://luport.local/ws/als-data/";
|
||||
const RECONNECT_MS = 5000;
|
||||
|
||||
// ── Debug mode ────────────────────────────────────────────────────────────
|
||||
// Otwórz stronę z ?debug aby wymusić widok alarmowy z przykładowymi danymi.
|
||||
// Możesz też wpisać w konsoli: DEBUG_ALARM = true; updateDisplay();
|
||||
let DEBUG_ALARM = new URLSearchParams(location.search).has("debug");
|
||||
|
||||
const DEBUG_MACHINES = ["2.60", "3.22", "4.09"];
|
||||
const DEBUG_DATA = {
|
||||
"2.60": { machine:"2.60", status:"Alarm - brak materiału" },
|
||||
"3.22": { machine:"3.22", status:"Alarm - awaria" },
|
||||
"4.09": { machine:"4.09", status:"Alarm - przekroczony cykl" },
|
||||
};
|
||||
|
||||
const machineData = {}; // id → last data payload
|
||||
const wsStatus = {}; // id → 'connected' | 'error' | 'disconnected'
|
||||
const wsMap = {}; // id → WebSocket instance
|
||||
|
||||
// ── WebSocket management ──────────────────────────────────────────────────
|
||||
|
||||
function connect(id) {
|
||||
if (wsMap[id]) {
|
||||
try { wsMap[id].close(); } catch (_) {}
|
||||
}
|
||||
|
||||
const ws = new WebSocket(WS_BASE + id + "/");
|
||||
wsMap[id] = ws;
|
||||
wsStatus[id] = "connecting";
|
||||
|
||||
ws.onopen = () => {
|
||||
wsStatus[id] = "connected";
|
||||
refreshStatus();
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data);
|
||||
if (msg.type === "machine_update" && msg.data) {
|
||||
machineData[id] = { ...msg.data, _ts: msg.timestamp };
|
||||
updateDisplay();
|
||||
}
|
||||
} catch (e) { /* ignore malformed frames */ }
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
wsStatus[id] = "error";
|
||||
refreshStatus();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
wsStatus[id] = "disconnected";
|
||||
refreshStatus();
|
||||
setTimeout(() => connect(id), RECONNECT_MS);
|
||||
};
|
||||
}
|
||||
|
||||
// ── Display logic ─────────────────────────────────────────────────────────
|
||||
|
||||
function isAlarm(data) {
|
||||
if (DEBUG_ALARM) return true; // w trybie debug wszystkie wstrzyknięte maszyny to alarmy
|
||||
return data && typeof data.status === "string" &&
|
||||
data.status.toLowerCase().includes("alarm");
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
const alarms = Object.entries(machineData)
|
||||
.filter(([, d]) => isAlarm(d))
|
||||
.sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
|
||||
const normalView = document.getElementById("normal-view");
|
||||
const alarmView = document.getElementById("alarm-view");
|
||||
const countEl = document.getElementById("alarm-count");
|
||||
|
||||
if (alarms.length > 0) {
|
||||
normalView.style.display = "none";
|
||||
alarmView.classList.add("active");
|
||||
countEl.textContent = alarms.length === 1
|
||||
? "1 maszyna"
|
||||
: alarms.length + " maszyny";
|
||||
renderCards(alarms);
|
||||
} else {
|
||||
normalView.style.display = "block";
|
||||
alarmView.classList.remove("active");
|
||||
}
|
||||
}
|
||||
|
||||
function renderCards(alarms) {
|
||||
const grid = document.getElementById("machines-grid");
|
||||
grid.innerHTML = "";
|
||||
|
||||
const n = alarms.length;
|
||||
const cols = Math.ceil(Math.sqrt(n));
|
||||
const rows = Math.ceil(n / cols);
|
||||
grid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
|
||||
grid.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
|
||||
grid.style.setProperty("--cols", cols);
|
||||
grid.style.setProperty("--rows", rows);
|
||||
|
||||
alarms.forEach(([id, d]) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "machine-card";
|
||||
card.innerHTML = `
|
||||
<div class="card-machine-id">${d.machine ?? id}</div>
|
||||
<div class="card-status">${d.status ?? "ALARM"}</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
const connected = Object.values(wsStatus).filter(s => s === "connected").length;
|
||||
document.getElementById("status-bar").textContent =
|
||||
`WS: ${connected} / ${MACHINES.length}`;
|
||||
}
|
||||
|
||||
// ── Bootstrap ─────────────────────────────────────────────────────────────
|
||||
if (DEBUG_ALARM) {
|
||||
DEBUG_MACHINES.forEach(id => { machineData[id] = DEBUG_DATA[id]; });
|
||||
document.getElementById("status-bar").textContent = "[DEBUG] widok alarmowy";
|
||||
} else {
|
||||
MACHINES.forEach(connect);
|
||||
}
|
||||
updateDisplay();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user