From 0b1c894190c12fe798e9cde84cdb7e97ce95e485 Mon Sep 17 00:00:00 2001 From: Dmitry Gammel Date: Tue, 18 Aug 2026 03:06:41 +0500 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=B1=D0=BE=D1=80=20=D0=BF=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0=20=E2=80=94=20=D0=B2=D1=8B=D0=BF=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D1=8E=D1=89=D0=B8=D0=B9=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA?= =?UTF-8?q?,=20=D0=BF=D1=80=D0=BE=D0=B2=D0=BE=D0=B4=20=D1=80=D0=B8=D1=81?= =?UTF-8?q?=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=BE=D1=82=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=BE=D0=B7=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIN_CATALOG: 16 доп. GPIO с возможностями (аналоговый вход/цифровой вход/цифровой выход) и настоящими координатами на плате — те же, что заняты sensor-1/fan-1/light-1/pump-1, исключены. Список в форме фильтруется под тип добавляемого устройства (вход/выход, аналоговый/ дискретный) и не предлагает уже занятые пины. Устройство с выбранным пином рисуется проводом прямо от этого пина на схеме (левый/правый край платы, высота = позиция пина), а не от общей пунктирной шины EXT — та осталась только как запасной вариант для устройств без указанного пина. --- internal/httpserver/web/index.html | 152 ++++++++++++++++++++++++----- 1 file changed, 125 insertions(+), 27 deletions(-) diff --git a/internal/httpserver/web/index.html b/internal/httpserver/web/index.html index 4f9cccf..20b7c28 100644 --- a/internal/httpserver/web/index.html +++ b/internal/httpserver/web/index.html @@ -152,7 +152,7 @@
подключение…
- + @@ -175,14 +175,18 @@ - - + + - + + + + + GPIO4 GPIO16 @@ -190,6 +194,8 @@ GPIO18 EXT + + @@ -253,6 +259,34 @@ const SIGNAL_PRESETS = { const CORE_LABELS = { sensor: 'Датчик', fan: 'Вентилятор', light: 'Свет', pump: 'Помпа' }; +// Assignable GPIO pins for extra devices — deliberately not the full ESP32 +// pinout, just enough to be realistic: which pins can only be read +// (34/35/36/39 are input-only on a real ESP32, no output driver), which +// support analog (ADC) input, and which are plain digital GPIO. Absolute +// board coordinates = the ESP32 rect's own translate(255,20) + local x/y, +// matching the four core pins' wire-start points (255 for the left edge, +// 445 for the right edge). +const PIN_CATALOG = [ + { pin: 'GPIO32', x: 255, y: 40, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO33', x: 255, y: 65, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO25', x: 255, y: 90, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO26', x: 255, y: 115, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO27', x: 255, y: 140, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO13', x: 255, y: 195, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO14', x: 255, y: 220, analogIn: true, digitalIn: true, digitalOut: true }, + { pin: 'GPIO5', x: 255, y: 245, analogIn: false, digitalIn: true, digitalOut: true }, + { pin: 'GPIO19', x: 255, y: 270, analogIn: false, digitalIn: true, digitalOut: true }, + { pin: 'GPIO21', x: 445, y: 40, analogIn: false, digitalIn: true, digitalOut: true }, + { pin: 'GPIO22', x: 445, y: 65, analogIn: false, digitalIn: true, digitalOut: true }, + { pin: 'GPIO23', x: 445, y: 90, analogIn: false, digitalIn: true, digitalOut: true }, + { pin: 'GPIO34', x: 445, y: 115, analogIn: true, digitalIn: true, digitalOut: false }, + { pin: 'GPIO35', x: 445, y: 170, analogIn: true, digitalIn: true, digitalOut: false }, + { pin: 'GPIO36', x: 445, y: 210, analogIn: true, digitalIn: true, digitalOut: false }, + { pin: 'GPIO39', x: 445, y: 250, analogIn: true, digitalIn: true, digitalOut: false }, +]; + +function pinEntry(name) { return PIN_CATALOG.find((p) => p.pin === name); } + let latest = {}; // slot -> last known Device let cardSlots = new Set(); // slots that currently have a rendered card @@ -347,7 +381,7 @@ function addDeviceCardHTML() {
- +
@@ -368,10 +402,10 @@ function addDeviceCardHTML() { - - + + -

Создаёт отдельный MQTT-девайс (свой external_id) — так же, как завели бы новое устройство на платформе, а не довесок к существующему.

+

Создаёт отдельный MQTT-девайс (свой external_id) — так же, как завели бы новое устройство на платформе, а не довесок к существующему. Список пинов сужается под выбранный тип сигнала и не предлагает уже занятые.

`; } @@ -379,6 +413,36 @@ function onNewDeviceKindChange() { const kind = document.getElementById('new-device-kind').value; document.getElementById('new-device-sensor-fields').style.display = kind === 'sensor' ? 'block' : 'none'; document.getElementById('new-device-actuator-fields').style.display = kind === 'actuator' ? 'block' : 'none'; + refreshPinOptions(); +} + +// Pins already claimed by any current device — the four core devices don't +// use PIN_CATALOG entries at all (their GPIO4/16/17/18 are fixed and never +// offered here), so only extras' .pin values need excluding. +function usedPins() { + return new Set(Object.values(latest).map((d) => d.pin).filter(Boolean)); +} + +function compatiblePins(kind, discrete) { + const used = usedPins(); + return PIN_CATALOG.filter((p) => { + if (used.has(p.pin)) return false; + if (kind === 'actuator') return p.digitalOut; + return discrete ? p.digitalIn : p.analogIn; + }); +} + +function refreshPinOptions() { + const select = document.getElementById('new-device-pin'); + if (!select) return; // add-device card not mounted yet + const kind = document.getElementById('new-device-kind').value; + const discrete = document.getElementById('new-device-discrete').checked; + const compatible = compatiblePins(kind, discrete); + + const prev = select.value; + select.innerHTML = '' + + compatible.map((p) => ``).join(''); + if (compatible.some((p) => p.pin === prev)) select.value = prev; } async function addDevice() { @@ -519,7 +583,10 @@ function render(snapshot) { document.getElementById('bulb-body').setAttribute('fill', latest.light?.power ? '#e8b339' : '#233348'); document.getElementById('pump-ring').classList.toggle('on', !!latest.pump?.power); - renderExtensionDevices(snapshot.devices.filter((d) => !d.core)); + const extras = snapshot.devices.filter((d) => !d.core); + renderPinnedDevices(extras.filter((d) => d.pin && pinEntry(d.pin))); + renderExtensionDevices(extras.filter((d) => !d.pin || !pinEntry(d.pin))); + refreshPinOptions(); } function insertCard(d) { @@ -539,6 +606,29 @@ function flashCard(slot) { // Draws every non-core device as its own icon hanging off the "EXT" bus // below the fixed board — visually distinct (dashed wire) from the four // GPIO-wired peripherals, since these aren't on a real fixed pin. +// Shared icon+label markup for one extra device at a given (x,y) — used by +// both the pinned layout (real GPIO position) and the unpinned fallback +// (generic EXT bus). +function deviceIconSVG(d, x, y, sub) { + const title = escapeHTML(deviceTitle(d)); + let svg = ''; + if (d.kind === 'sensor') { + const name = Object.keys(d.readings || {})[0]; + const value = name !== undefined ? d.readings[name] : 0; + svg += ``; + svg += `${escapeHTML(value)}`; + } else { + const on = !!d.power; + svg += ``; + svg += `${on ? 'ON' : 'OFF'}`; + } + svg += `${title}`; + svg += `${escapeHTML(sub)}`; + return svg; +} + +// Devices with no pin assigned hang off a generic dashed "EXT" bus below +// the board — there's no real position to draw them at. function renderExtensionDevices(extras) { const g = document.getElementById('extension-devices'); const trunk = document.getElementById('ext-trunk'); @@ -561,26 +651,33 @@ function renderExtensionDevices(extras) { const x = startX + spacing * i; const y = busY + 55; svg += ``; - if (d.pin) { - svg += `${escapeHTML(d.pin)}`; - } + const sub = `${escapeHTML(d.external_id)} · ${d.kind === 'sensor' ? 'input' : 'output'}`; + svg += deviceIconSVG(d, x, y, sub); + }); - const title = escapeHTML(deviceTitle(d)); - const pinPrefix = d.pin ? `${escapeHTML(d.pin)} · ` : ''; - const sub = `${pinPrefix}${escapeHTML(d.external_id)} · ${d.kind === 'sensor' ? 'input' : 'output'}`; + g.innerHTML = svg; +} - if (d.kind === 'sensor') { - const name = Object.keys(d.readings || {})[0]; - const value = name !== undefined ? d.readings[name] : 0; - svg += ``; - svg += `${escapeHTML(value)}`; - } else { - const on = !!d.power; - svg += ``; - svg += `${on ? 'ON' : 'OFF'}`; - } - svg += `${title}`; - svg += `${sub}`; +// Devices with a real assigned pin get a solid wire straight out from that +// pin's actual position on the board — left-side pins grow an icon to the +// left of the board, right-side pins to the right, both at the pin's own +// height, so the wire reads as "this exact pin goes to this device". +function renderPinnedDevices(pinned) { + const g = document.getElementById('pinned-devices'); + let svg = ''; + + pinned.forEach((d) => { + const entry = pinEntry(d.pin); + if (!entry) return; // pin no longer in the catalog (shouldn't happen) — skip rather than crash + const onLeft = entry.x === 255; + const iconX = onLeft ? -40 : 730; + const y = entry.y; + const wireColor = d.kind === 'sensor' ? 'var(--wire-sensor)' : 'var(--wire-fan)'; + + svg += ``; + svg += `${escapeHTML(entry.pin)}`; + const sub = `${escapeHTML(entry.pin)} · ${d.kind === 'sensor' ? 'input' : 'output'}`; + svg += deviceIconSVG(d, iconX, y, sub); }); g.innerHTML = svg; @@ -615,6 +712,7 @@ function connectEvents() { } document.getElementById('cards').innerHTML = addDeviceCardHTML(); +refreshPinOptions(); connectEvents();