diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 95ec3cc..d068363 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -837,6 +837,82 @@ ], "isBackground": false, "problemMatcher": [] + }, + { + "label": "wago-sim: pytest vertical", + "type": "shell", + "command": ".venv\\Scripts\\python.exe", + "args": [ + "-m", + "pytest", + "tests", + "-q", + "--tb=short" + ], + "isBackground": false, + "problemMatcher": [] + }, + { + "label": "wago-sim: serve vertical", + "type": "shell", + "command": ".venv\\Scripts\\python.exe", + "args": [ + "-m", + "wago_sim.cli", + "serve", + "--node", + "config/pump_node.yaml", + "--host", + "127.0.0.1", + "--port", + "5020", + "--web-port", + "8088" + ], + "isBackground": true, + "problemMatcher": [] + }, + { + "label": "kill ports 3", + "type": "shell", + "command": ".venv\\Scripts\\python.exe", + "args": [ + "-c", + "import subprocess; subprocess.run(['powershell','-NoProfile','-Command','Get-NetTCPConnection -LocalPort 5020,8088 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $_ -Force }'], capture_output=True); print('killed')" + ], + "isBackground": false, + "problemMatcher": [] + }, + { + "label": "wago-sim: serve vertical2", + "type": "shell", + "command": ".venv\\Scripts\\python.exe", + "args": [ + "-m", + "wago_sim.cli", + "serve", + "--node", + "config/pump_node.yaml", + "--host", + "127.0.0.1", + "--port", + "5020", + "--web-port", + "8088" + ], + "isBackground": true, + "problemMatcher": [] + }, + { + "label": "stop vertical serve", + "type": "shell", + "command": ".venv\\Scripts\\python.exe", + "args": [ + "-c", + "import subprocess; subprocess.run(['powershell','-NoProfile','-Command','Get-NetTCPConnection -LocalPort 5020,8088 -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique | ForEach-Object { Stop-Process -Id $_ -Force }'], capture_output=True); print('stopped')" + ], + "isBackground": false, + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/README.md b/README.md index eb29c54..520f53b 100644 --- a/README.md +++ b/README.md @@ -347,8 +347,8 @@ requirement ID for each item in parentheses. Signed support (2026-10-04): range bounds and simulated values may be negative (-65534–65535; stored as 16-bit two's complement), for signed plant quantities. DI/DO channels render - in two rows (0–7 upper, 8–15 lower), mirroring two-deck WAGO - modules. + in vertical columns of eight (0–7 left, 8–15 next) on thin, + tall module bezels mirroring real DIN-rail housings. - [ ] M6: scenario run view + alias-ownership rejection (FR-10.2.6, FR-10.2.9) — deferred. - [x] Tests: 46 web-UI tests (`tests/test_webui.py`) via httpx diff --git a/docs/spec/software-spec.md b/docs/spec/software-spec.md index f72559a..cba7f85 100644 --- a/docs/spec/software-spec.md +++ b/docs/spec/software-spec.md @@ -897,9 +897,11 @@ Terminal monitors (§9) suffice for single engineers, but FAT benches are the channel indicators carry only the 0-based channel number: below each DI/DO LED, and on analog modules outside the indicator stack, above the overflow and below the underflow LED (FR-10.2.14) so the - limit LEDs stay adjacent to the bar. DI/DO channels lay out in **two - rows** — channels 0–7 on the upper row, 8–15 on the lower row — - mirroring the physical two-deck layout of 16-channel WAGO modules. + limit LEDs stay adjacent to the bar. DI/DO channels lay out in + **vertical columns of eight** — channels 0–7 in the left column, + 8–15 in the next — and modules are drawn as **thin, tall bezels** + mirroring the proportions of real WAGO DIN-rail housings (narrow + width, vertically stacked LEDs). - FR-10.2.3: Values shall update live (WebSocket push or short-polling), including changes made by the PLC/master over Modbus, by scenarios, by the console monitor, or by other UI clients — one shared source of truth. diff --git a/src/wago_sim/webui/static/app.js b/src/wago_sim/webui/static/app.js index 40b06ed..5d51431 100644 --- a/src/wago_sim/webui/static/app.js +++ b/src/wago_sim/webui/static/app.js @@ -271,9 +271,16 @@ function renderRail() { const el = document.createElement("div"); el.className = "module"; el.dataset.index = mod.index; - // width: 8 channels per row (two-deck layout), min 110px - const perRow = Math.min(mod.channels.length, 8); - el.style.width = `${Math.max(110, 30 * perRow)}px`; + const isDigital = mod.channels[0] && + (mod.channels[0].kind === "di" || mod.channels[0].kind === "do"); + // thin, tall bezels: digital modules get one 26px column per + // group of eight; analog modules keep their indicator-stack width + const digitalCount = mod.channels.filter( + (c) => c.kind === "di" || c.kind === "do").length; + const columns = Math.max(1, Math.ceil(digitalCount / 8)); + el.style.width = isDigital + ? `${Math.max(46, 26 * columns)}px` + : `${Math.max(90, 30 * mod.channels.length)}px`; const kind = mod.channels[0] ? mod.channels[0].kind : "di"; el.classList.add(`mod-${kind.startsWith("d") ? kind : "analog"}`); @@ -283,14 +290,15 @@ function renderRail() { const designation = firstCode.slice(0, firstCode.indexOf(".")); let html = `