diff --git a/john103C6T6NewVer/MCU_PINS.md b/john103C6T6NewVer/MCU_PINS.md
new file mode 100644
index 0000000..1f567fc
--- /dev/null
+++ b/john103C6T6NewVer/MCU_PINS.md
@@ -0,0 +1,78 @@
+# Пины МК
+
+Документ составлен по прошивке:
+
+- `..\new rev\john103C8T6\john103C6T6.ioc`
+- `..\new rev\john103C8T6\Core\Inc\main.h`
+- `..\new rev\john103C8T6\Core\Src\gpio.c`
+- `..\new rev\john103C8T6\Core\Src\usart.c`
+- `..\new rev\john103C8T6\Core\Src\can.c`
+- `..\new rev\john103C8T6\Core\Src\i2c.c`
+- `..\new rev\john103C8T6\Core\Src\spi.c`
+- `..\new rev\john103C8T6\Core\Inc\ow_port.h`
+
+МК: `STM32F103C8Tx`, корпус `LQFP48`.
+
+## Основные интерфейсы
+
+| Пин МК | Функция | Настройка | Назначение в проекте |
+|---|---|---|---|
+| `PB6` | `USART1_TX` | AF push-pull, remap USART1 | Modbus RTU slave TX |
+| `PB7` | `USART1_RX` | input, no pull, remap USART1 | Modbus RTU slave RX |
+| `PA2` | `USART2_TX` | AF push-pull | Modbus RTU master TX |
+| `PA3` | `USART2_RX` | input, no pull | Modbus RTU master RX |
+| `PA15` | `One_wire` | GPIO, динамически input/output | Шина 1-Wire для DS18B20 |
+| `PA10` | `Relay_dc5v` | GPIO output push-pull | Реле 5 В, управляется из Modbus coil `20` |
+| `PC13` | GPIO output | push-pull | Светодиод/индикация, используется в `led_blink()` |
+| `PA0` | GPIO input | no pull | Вход, назначение в коде не найдено |
+| `PA11` | `CAN_RX` | input | CAN RX |
+| `PA12` | `CAN_TX` | AF push-pull | CAN TX |
+| `PB8` | `I2C1_SCL` | AF open-drain, remap I2C1 | I2C1 SCL |
+| `PB9` | `I2C1_SDA` | AF open-drain, remap I2C1 | I2C1 SDA |
+| `PB3` | `SPI1_SCK` | AF push-pull, remap SPI1 | SPI1 SCK |
+| `PB4` | `SPI1_MISO` | input, no pull, remap SPI1 | SPI1 MISO |
+| `PB5` | `SPI1_MOSI` | AF push-pull, remap SPI1 | SPI1 MOSI |
+| `PA13` | `SWDIO` | Serial Wire | Отладка |
+| `PA14` | `SWCLK` | Serial Wire | Отладка |
+| `PD0` | `OSC_IN` | HSE oscillator | Внешний кварц |
+| `PD1` | `OSC_OUT` | HSE oscillator | Внешний кварц |
+
+## GPIO output, сконфигурированные в `MX_GPIO_Init`
+
+| Пин МК | Начальное состояние | Комментарий |
+|---|---:|---|
+| `PC13` | `RESET` | Индикация, мигает при старте через `led_blink(GPIOC, 13, ...)` |
+| `PA1` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA4` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA5` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA6` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA7` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA8` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA9` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PA10` | `RESET` | `Relay_dc5v`, в цикле main зависит от `MB_DATA.Coils.coils[1].state_val_bit.state_val_05` |
+| `PA15` | `RESET` | 1-Wire, затем драйвер меняет режим пина input/output |
+| `PB0` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB1` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB2` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB10` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB11` | `RESET` | Есть macro `GPIOB11_valve`, но прямой записи в `PB11` в текущем коде не найдено |
+| `PB12` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB13` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB14` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+| `PB15` | `RESET` | Выход сконфигурирован, активного использования в коде не найдено |
+
+## Периферия без внешнего пина
+
+| Периферия | Настройка | Назначение |
+|---|---|---|
+| `TIM1` | internal clock, period `65535`, prescaler `0` | Тайминги 1-Wire, `OW_TIM` |
+| `TIM2` | internal clock, period `65535`, prescaler `7199` | Таймер Modbus slave |
+| `TIM4` | internal clock, period `65535`, prescaler `7199` | Таймер Modbus master |
+| `ADC1` | `ADC_CHANNEL_VREFINT` | Внутренний Vref, внешних ADC-пинов нет |
+| `RTC` | LSI | Часы, доступны через Modbus-регистры |
+
+## Замечания
+
+- В `.ioc` пины `PA2/PA3` отмечены как GPIO output, но в актуальном `usart.c` они инициализируются как `USART2_TX/RX`. Для документации использована фактическая инициализация из исходников.
+- Функция `handle_valves()` содержит запись в `GPIOC` pin `14`, но `PC14` не инициализирован в `MX_GPIO_Init()` и сама функция в основном цикле закомментирована.
+- Для RS-485 direction control (`DE/RE`) отдельный GPIO в `modbus_config.h` не задан: `RS_EnableReceive()` и `RS_EnableTransmit()` закомментированы.
diff --git a/john103C6T6NewVer/MODBUS_MAP.md b/john103C6T6NewVer/MODBUS_MAP.md
new file mode 100644
index 0000000..5ae4fdc
--- /dev/null
+++ b/john103C6T6NewVer/MODBUS_MAP.md
@@ -0,0 +1,201 @@
+# Modbus Map
+
+Документ составлен по прошивке:
+
+- `..\new rev\john103C8T6\Modbus\modbus_config.h`
+- `..\new rev\john103C8T6\Modbus\modbus_data.h`
+- `..\new rev\john103C8T6\Modbus\modbus_data.c`
+- `..\new rev\john103C8T6\Core\Src\main.c`
+- `..\new rev\john103C8T6\Core\Inc\PROJ_setup.h`
+
+Адреса ниже указаны в 0-based формате, как они используются в прошивке. В некоторых Modbus-терминалах эти же адреса отображаются как `30001`, `40001`, `00001` и т.п. В таком терминале к адресу обычно надо прибавить `1` и выбрать нужный тип таблицы.
+
+## Общие параметры
+
+| Параметр | Значение |
+|---|---|
+| Протокол | Modbus RTU |
+| Slave ID | `3` |
+| Slave UART | `USART1`, `PB6 TX`, `PB7 RX` |
+| Slave UART настройки | `115200`, `8N1` |
+| Slave таймер | `TIM2` |
+| Master UART | `USART2`, `PA2 TX`, `PA3 RX` |
+| Master UART настройки | `115200`, `8N1` |
+| Master таймер | `TIM4` |
+| `MAX_SENSE` | `32` |
+| Включенные таблицы | Coils, Holding Registers, Input Registers |
+
+## Input Registers, function `0x04`
+
+Базовый массив: `MB_DATA.InRegs`, адресный диапазон прошивки `0..1999`.
+
+| Адрес | Кол-во | Имя | Тип | Описание |
+|---:|---:|---|---|---|
+| `0` | `32` | `sens_Temp[0..31]` | `uint16_t` | Температура DS18B20, в коде записывается как `temperature * 10`. Рекомендуется читать как `int16_t / 10.0`, если возможны отрицательные температуры |
+| `32` | `968` | `reserve` | `uint16_t[]` | Резерв до адреса `999` |
+| `1000` | `128` | `ID.DevAddr[32][8]` | raw bytes as registers | ROM-коды DS18B20: 32 датчика по 8 байт. Для датчика `N`: базовый регистр `1000 + N * 4` |
+| `1128` | `72` | `reserve1` | `uint16_t[]` | Резерв до адреса `1199` |
+| `1200` | `1` | `num_Tsens` | `uint16_t` | Количество найденных датчиков DS18B20 |
+| `1201` | `1` | `rtc.hours` | `uint16_t` | Текущие часы RTC |
+| `1202` | `1` | `rtc.minutes` | `uint16_t` | Текущие минуты RTC |
+| `1203` | `1` | `rtc.seconds` | `uint16_t` | Текущие секунды RTC |
+| `1204` | `1` | `rtc.date` | `uint16_t` | День месяца |
+| `1205` | `1` | `rtc.month` | `uint16_t` | Месяц |
+| `1206` | `1` | `rtc.year` | `uint16_t` | Год в формате RTC проекта |
+| `1207` | `1` | `rtc.weekday` | `uint16_t` | День недели |
+| `1208` | `1` | `rtc.apply` | `uint16_t` | Для input-регистров обычно `0` |
+| `1209` | `1` | `rtc.status` | `uint16_t` | Статус RTC |
+| `1210` | `790` | reserved | `uint16_t[]` | Адреса доступны в общем диапазоне, прикладного поля нет |
+
+## Holding Registers, functions `0x03`, `0x06`, `0x10`
+
+Базовый массив: `MB_DATA.HoldRegs`, адресный диапазон прошивки `0..1999`.
+
+| Адрес | Кол-во | Имя | Тип | Описание |
+|---:|---:|---|---|---|
+| `0` | `32` | `set_Temp[0..31]` | `uint16_t` | Уставки температуры для датчиков. В текущем коде используются как градусы без умножения на 10 |
+| `32` | `68` | `reserve` | `uint16_t[]` | Резерв до адреса `99` |
+| `100` | `32` | `set_hyst[0..31]` | `uint16_t` | Гистерезис температуры для датчиков. В текущем коде используется как градусы |
+| `132` | `68` | `reserve1` | `uint16_t[]` | Резерв до адреса `199` |
+| `200` | `1` | `rtc.hours` | `uint16_t` | Часы для установки RTC |
+| `201` | `1` | `rtc.minutes` | `uint16_t` | Минуты для установки RTC |
+| `202` | `1` | `rtc.seconds` | `uint16_t` | Секунды для установки RTC |
+| `203` | `1` | `rtc.date` | `uint16_t` | День месяца |
+| `204` | `1` | `rtc.month` | `uint16_t` | Месяц |
+| `205` | `1` | `rtc.year` | `uint16_t` | Год в формате RTC проекта |
+| `206` | `1` | `rtc.weekday` | `uint16_t` | День недели |
+| `207` | `1` | `rtc.apply` | `uint16_t` | Записать `1`, чтобы применить время RTC |
+| `208` | `1` | `rtc.status` | `uint16_t` | Статус установки RTC |
+| `209` | `1791` | reserved | `uint16_t[]` | Адреса доступны в общем диапазоне, прикладного поля нет |
+
+RTC status:
+
+| Значение | Имя | Описание |
+|---:|---|---|
+| `0` | `MB_RTC_STATUS_IDLE` | Нет операции |
+| `1` | `MB_RTC_STATUS_SET_OK` | Время установлено |
+| `2` | `MB_RTC_STATUS_VALUE_ERROR` | Ошибка значения времени/даты |
+| `3` | `MB_RTC_STATUS_HAL_ERROR` | Ошибка HAL RTC |
+
+## Coils, functions `0x01`, `0x05`, `0x0F`
+
+Базовый массив: `MB_DATA.Coils`, адресный диапазон прошивки `0..999`.
+
+| Coil address | Кол-во | Имя | Описание |
+|---:|---:|---|---|
+| `0` | `48` | `coils[0..2]` | Общие управляющие биты `state_val_01..state_val_16` в трех 16-битных словах |
+| `20` | `1` | `coils[1].state_val_05` | Управляет `PA10 / Relay_dc5v` в основном цикле |
+| `48` | `80` | `reserve_coils` | Резерв до coil `127` |
+| `128` | `32` | `status_tSens[0..1]` | Статусы подключения DS18B20: `Temp1_isConnected..Temp32_isConnected` |
+| `160` | `96` | `reserve_status_tSens` | Резерв до coil `255` |
+| `256` | `16` | `relay_struct_on` | Расчетные биты включения реле по датчикам `Temp1..Temp16` |
+| `272` | `16` | `reserve_relay_struct_on` | Резерв |
+| `288` | `16` | `relay_struct_off` | Расчетные биты выключения реле по датчикам `Temp1..Temp16` |
+| `304` | `80` | reserved | Резерв до coil `383` |
+| `384` | `1` | `init_param` | При записи `1` прошивка применяет `set_Temp[]` и `set_hyst[]`, затем сбрасывает бит |
+| `385` | `1` | `init_Tsens` | При записи `1` прошивка повторно ищет DS18B20, затем сбрасывает бит |
+| `386` | `1` | `Save_Param_to_Flash` | Поле объявлено, активного использования в текущем `main.c` не найдено |
+| `387` | `13` | reserved2 | Резервные биты |
+| `400` | `600` | reserved | Адреса доступны в общем диапазоне, прикладного поля нет |
+
+## Привязка температур и реле
+
+В `value_control()` прошивка сравнивает `sens[i].temperature` с уставкой и гистерезисом:
+
+- если `temperature < set_temp - hyst`, выставляется `relay_struct_off` для датчика `i`;
+- если `temperature > set_temp + hyst`, выставляется `relay_struct_on` для датчика `i`;
+- если `temperature == set_temp`, оба бита для датчика сбрасываются.
+
+Эти расчетные биты лежат в coils `256..271` и `288..303`. Прямая аппаратная привязка этих расчетных битов к GPIO-выходам в текущем `main.c` не найдена. Прямое управление GPIO найдено только для `PA10 / Relay_dc5v` через coil `20`.
+
+
+## GUI binding to the real STM project
+
+Source project checked: `..\new rev\john103C8T6`.
+
+The PC GUI bridge is bound to the actual STM Modbus layout from:
+
+- `Modbus\modbus_config.h`
+- `Modbus\modbus_data.h`
+- `Core\Src\main.c`
+- `Core\Inc\ds18b20_driver.h`
+- `Core\Inc\PROJ_setup.h`
+
+Active STM settings:
+
+| Item | Value |
+|---|---:|
+| Default slave ID | `3` |
+| `MAX_SENSE` in STM | `32` |
+| GUI channels used | first `16` |
+| STM protocol on COM | Modbus RTU |
+| Optional GUI network mode | Modbus TCP gateway/device with the same register map |
+
+GUI runtime map:
+
+| GUI value | STM source | Modbus function | 0-based address |
+|---|---|---:|---:|
+| Current temperature channel `N` | `MB_DATA.InRegs.sens_Temp[N]` | `0x04` | `0 + N` |
+| DS18B20 ROM ID channel `N` | `MB_DATA.InRegs.ID.DevAddr[N][8]` | `0x04` | `1000 + N * 4` |
+| Sensor connected channel `N` | `MB_DATA.Coils.status_tSens` | `0x01` | `128 + N` |
+| Setpoint channel `N` | `MB_DATA.HoldRegs.set_Temp[N]`, value = `degC * 10` | `0x03` / `0x06` | `0 + N` |
+| Apply setpoints | `MB_DATA.Coils.init_param` | `0x05` | `384` |
+| Open command/state channel `N` | `MB_DATA.Coils.relay_struct_on` | `0x01` / `0x05` | `256 + N` |
+| Close command/state channel `N` | `MB_DATA.Coils.relay_struct_off` | `0x01` / `0x05` | `288 + N` |
+
+Notes:
+
+- GUI channel `1` uses index `N = 0`; GUI channel `16` uses index `N = 15`.
+- Setpoints are written as tenths of a degree: GUI `28.5°C` -> Modbus holding value `285`; readback `285` -> GUI `28.5°C`.
+- DS18B20 IDs are stored by STM as `uint8_t DevAddr[32][8]` and exposed through 16-bit input registers. The bridge restores each register in little-endian byte order to display the ROM ID correctly.
+- The current STM project does not expose analog valve position or opening angle registers. GUI position/angle are therefore derived from binary relay state: open = `100% / 90°`, close = `0% / 0°`.
+- `value_control()` in `Core\Src\main.c` calculates `relay_struct_on/off` from temperature, setpoint, and hysteresis. Direct manual coil writes from GUI can be overwritten by that firmware logic unless STM firmware adds a manual override register/coil.
+
+
+## STM room/channel structure added for GUI
+
+Added to Keil STM project `..\new rev\john103C8T6`.
+
+### Input registers: room status block
+
+Base address: `400`. One room/channel uses `18` registers. Channel `N` uses base `400 + N * 18`, where GUI channel 1 is `N = 0`.
+
+| Offset | Field | Scale / meaning |
+|---:|---|---|
+| 0 | `channel` | 1-based channel number |
+| 1 | `location` | numeric location code |
+| 2..5 | `ds18b20_id[4]` | 8-byte DS18B20 ROM ID, little-endian bytes per register |
+| 6 | `temperature_x10` | current temperature, degC * 10 |
+| 7 | `setpoint_x10` | setpoint, degC * 10 |
+| 8 | `hysteresis_x10` | hysteresis, degC * 10 |
+| 9 | `valve_position_pct` | valve opening percent, 0..100 |
+| 10 | `valve_angle_deg` | opening angle in degrees |
+| 11 | `valve_angle_max_deg` | max opening angle, default 90 |
+| 12 | `is_connected` | DS18B20 connected flag |
+| 13 | `valve_open` | open relay/command state |
+| 14 | `valve_close` | close relay/command state |
+| 15 | `mode` | 0 auto, 1 manual |
+| 16 | `command_state` | 0 stop, 1 open, 2 close |
+| 17 | `reserved` | reserved |
+
+### Holding registers: room control block
+
+Base address: `300`. One room/channel uses `8` registers. Channel `N` uses base `300 + N * 8`.
+
+| Offset | Field | Scale / meaning |
+|---:|---|---|
+| 0 | `setpoint_x10` | writable setpoint, degC * 10 |
+| 1 | `hysteresis_x10` | writable hysteresis, degC * 10 |
+| 2 | `valve_position_pct` | writable manual opening percent, 0..100 |
+| 3 | `valve_angle_max_deg` | max opening angle, default 90 |
+| 4 | `mode` | 0 auto, 1 manual |
+| 5 | `command` | 0 stop, 1 open, 2 close |
+| 6 | `location` | numeric location code |
+| 7 | `apply` | write 1 to apply room setpoint/hysteresis |
+
+Compatibility:
+
+- Legacy `set_Temp[0..31]` at holding `0..31` is kept.
+- Legacy `set_hyst[0..31]` at holding `100..131` is kept.
+- Legacy coils `256..271` and `288..303` are kept.
+- The Python bridge writes both the legacy map and the new room control block.
diff --git a/john103C6T6NewVer/README.md b/john103C6T6NewVer/README.md
new file mode 100644
index 0000000..f8b729b
--- /dev/null
+++ b/john103C6T6NewVer/README.md
@@ -0,0 +1,105 @@
+# Web GUI для датчиков температуры и клапанов
+
+Веб-интерфейс для:
+- мониторинга температур,
+- установки уставок,
+- ручного и автоматического управления клапанами,
+- управления позицией клапана в `%`.
+
+## Быстрый старт
+
+1. Запустите демо backend:
+ ```bash
+ python mock_server.py
+ ```
+2. Откройте страницу:
+ - `http://127.0.0.1:8080/index.html`
+3. В поле `API endpoint` оставьте пустым для демо или укажите `http://127.0.0.1:8080`.
+
+Если API не указан, интерфейс переходит в офлайн-режим (демо) и хранит состояние в `localStorage`.
+
+## Привязка к вашему проекту
+
+GUI читает и пишет данные через `API_PATHS` в `app.js`:
+
+1. Датчики: `/api/sensors`, `/sensors`, `/api/data`, `/state`
+2. Клапаны: `/api/valves`, `/valves`, `/api/data`, `/state`
+3. Запись: сначала `PUT /api/*/:id`, затем `PUT /api/*` (без `:id`)
+
+Если ваш контракт другой, правьте:
+- массив `API_PATHS` в `app.js`
+- функции `normalizeSensor`, `normalizeValve` в `app.js`
+
+## Работа с COM-портом (MCU)
+
+Для чтения с порта COM запущен `serial_bridge.py`:
+
+```bash
+python serial_bridge.py --serial-port COM3 --baudrate 115200
+```
+
+По умолчанию сервис доступен на `http://127.0.0.1:8080`.
+
+Поддерживаемые входящие форматы с порта:
+
+1) JSON-пакет:
+```json
+{"sensors":[{"id":"zone_1","value":24.5,"setpoint":28.0}], "valves":[{"id":"valve_1","mode":"auto","position":32,"targetTemp":28}]}
+```
+
+2) Текстовый формат:
+```text
+T1=24.5;T1_SETPOINT=28.0;V1_MODE=auto;V1_POS=32;V1_TGT=28
+```
+
+Исходящие команды от GUI (`PUT`) также отправляются в порт как JSON:
+```json
+{"type":"sensor","id":"zone_1","setpoint":28.0}
+{"type":"valve","id":"valve_1","mode":"manual","position":50}
+```
+
+Если нужен другой протокол от/к MCU (Modbus/байт-структура), пришлите пример кадров — подгоню парсер в `serial_bridge.py`.
+
+В GUI:
+- поле `COM порт` показывает список `/api/serial/ports`;
+- кнопка `Обновить порты` — принудительно перечитывает список COM;
+- кнопка `Подключить` / `Отключить` — вызывает `/api/serial/connect` и `/api/serial/disconnect` в `serial_bridge.py`.
+
+## Форматы API
+
+### GET
+- `GET /api/sensors` → массив:
+ ```json
+ [
+ { "id": "zone_1", "name": "Термопара 1", "value": 24.6, "setpoint": 28, "unit": "°C", "zone": "1" }
+ ]
+ ```
+- `GET /api/valves` → массив:
+ ```json
+ [
+ { "id": "valve_1", "name": "Клапан 1", "zone": "1", "mode": "auto", "position": 40, "targetTemp": 28, "isOpen": true }
+ ]
+ ```
+
+### PUT
+- `PUT /api/sensors/{id}`:
+ ```json
+ { "setpoint": 29.5 }
+ ```
+- `PUT /api/valves/{id}`:
+ - ручной:
+ ```json
+ { "mode": "manual", "position": 50 }
+ ```
+ - авто:
+ ```json
+ { "mode": "auto", "targetTemp": 28.0 }
+ ```
+ - или отдельные поля (`mode`, `position`, `targetTemp`).
+
+## Файлы
+- `index.html` — разметка страницы
+- `styles.css` — стили
+- `app.js` — логика интерфейса
+- `mock_server.py` — демонстрационный backend
+- `serial_bridge.py` — backend для COM-порта
diff --git a/john103C6T6NewVer/app.js b/john103C6T6NewVer/app.js
new file mode 100644
index 0000000..d679000
--- /dev/null
+++ b/john103C6T6NewVer/app.js
@@ -0,0 +1,1640 @@
+const SENSOR_COUNT = 16;
+const VALVE_COUNT = 32;
+const DEFAULT_OPEN_DEGREES_MAX = 90;
+const CHANNEL_LOCATIONS = [
+ "DUO прав",
+ "DUO лев",
+ "TRIO",
+ "SOLO",
+ "ОСНОВА 7",
+ "ОСНОВА 6",
+ "ОСНОВА 5",
+ "ОСНОВА 4",
+ "ОСНОВА 3",
+ "ОСНОВА 2",
+ "ОСНОВА 1",
+];
+
+function defaultChannelLocation(index) {
+ return CHANNEL_LOCATIONS[index % CHANNEL_LOCATIONS.length];
+}
+
+function makeDefaultSensors() {
+ return Array.from({ length: SENSOR_COUNT }, (_, index) => {
+ const number = index + 1;
+ return {
+ id: `zone_${number}`,
+ name: `Датчик ${number}`,
+ value: 0,
+ setpoint: 28,
+ unit: "°C",
+ zone: String(number),
+ location: defaultChannelLocation(index),
+ ds18b20Id: `28-00-00-00-00-00-00-${number.toString(16).toUpperCase().padStart(2, "0")}`,
+ };
+ });
+}
+
+function makeDefaultValves() {
+ return Array.from({ length: VALVE_COUNT }, (_, index) => {
+ const number = index + 1;
+ const zone = (index % SENSOR_COUNT) + 1;
+ return {
+ id: `valve_${number}`,
+ name: `Клапан ${number}`,
+ zone: String(zone),
+ mode: "auto",
+ position: 0,
+ targetTemp: 28,
+ isOpen: false,
+ openDegrees: 0,
+ openDegreesMax: DEFAULT_OPEN_DEGREES_MAX,
+ };
+ });
+}
+
+const defaultSensors = makeDefaultSensors();
+const defaultValves = makeDefaultValves();
+
+const API_PATHS = {
+ sensorsRead: ["/api/sensors", "/sensors", "/api/data", "/state"],
+ valvesRead: ["/api/valves", "/valves", "/api/data", "/state"],
+ sensorWrite: [
+ (id) => `/api/sensors/${encodeURIComponent(id)}`,
+ (id) => `/sensors/${encodeURIComponent(id)}`,
+ () => "/api/sensors",
+ () => "/sensors",
+ ],
+ valveWrite: [
+ (id) => `/api/valves/${encodeURIComponent(id)}`,
+ (id) => `/valves/${encodeURIComponent(id)}`,
+ () => "/api/valves",
+ () => "/valves",
+ ],
+ valveCalibrate: [
+ (id) => `/api/valves/${encodeURIComponent(id)}/calibrate`,
+ (id) => `/valves/${encodeURIComponent(id)}/calibrate`,
+ ],
+ valvesCalibrateAll: ["/api/valves/calibrate-all", "/api/calibration/all", "/calibration/all"],
+};
+
+const SERIAL_API_PATHS = {
+ ports: [
+ "/api/serial/ports",
+ "/api/ports",
+ "/ports",
+ "/serial/ports",
+ "/status/ports",
+ ],
+ status: [
+ "/api/serial/status",
+ "/api/state",
+ "/state",
+ "/serial/status",
+ ],
+ connect: [
+ "/api/serial/connect",
+ "/api/connect",
+ "/connect",
+ "/serial/connect",
+ ],
+ disconnect: [
+ "/api/serial/disconnect",
+ "/api/disconnect",
+ "/disconnect",
+ "/serial/disconnect",
+ ],
+};
+
+const state = {
+ sensors: [...defaultSensors],
+ valves: [...defaultValves],
+ apiBase: "",
+ timer: null,
+};
+
+let serialConnected = false;
+let selectedPort = "";
+
+const sensorsEl = document.getElementById("sensors");
+const valvesEl = document.getElementById("valves");
+const statusEl = document.getElementById("status");
+const globalStatus = document.getElementById("globalStatus");
+const apiInput = document.getElementById("apiBase");
+const refreshBtn = document.getElementById("refreshBtn");
+const saveApiBtn = document.getElementById("saveApiBtn");
+const refreshPortsBtn = document.getElementById("refreshPortsBtn");
+const connectPortBtn = document.getElementById("connectPortBtn");
+const comPortSelect = document.getElementById("comPortSelect");
+const modbusTransport = document.getElementById("modbusTransport");
+const tcpHost = document.getElementById("tcpHost");
+const tcpPort = document.getElementById("tcpPort");
+const modbusSlaveId = document.getElementById("modbusSlaveId");
+const serialStatus = document.getElementById("serialStatus");
+const calibrateAllBtn = document.getElementById("calibrateAllBtn");
+
+function clamp(value, min, max) {
+ return Math.max(min, Math.min(max, value));
+}
+
+function parseGuiNumber(value, fallback = 0) {
+ const parsed = Number(String(value ?? "").replace(",", "."));
+ return Number.isFinite(parsed) ? parsed : fallback;
+}
+
+function storageGet(name, fallback) {
+ const raw = localStorage.getItem(name);
+ if (!raw) return fallback;
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return fallback;
+ }
+}
+
+function storageSet(name, value) {
+ localStorage.setItem(name, JSON.stringify(value));
+}
+
+function getModbusTransport() {
+ return (modbusTransport?.value || storageGet("modbusTransport", "rtu") || "rtu").toLowerCase();
+}
+
+function applyModbusTransportView() {
+ const transport = getModbusTransport();
+ const isTcp = transport === "tcp";
+
+ document.querySelectorAll(".rtu-field").forEach((element) => {
+ element.classList.toggle("hidden", isTcp);
+ });
+ document.querySelectorAll(".tcp-field").forEach((element) => {
+ element.classList.toggle("hidden", !isTcp);
+ });
+
+ if (refreshPortsBtn) {
+ refreshPortsBtn.disabled = isTcp;
+ }
+ if (serialStatus && !serialConnected) {
+ serialStatus.textContent = isTcp ? "Modbus TCP: не подключен" : "COM: не подключен";
+ }
+}
+
+function initModbusTransportControls() {
+ if (modbusTransport) {
+ modbusTransport.value = storageGet("modbusTransport", "rtu");
+ modbusTransport.addEventListener("change", () => {
+ storageSet("modbusTransport", modbusTransport.value);
+ applyModbusTransportView();
+ });
+ }
+ if (tcpHost) {
+ tcpHost.value = storageGet("tcpHost", tcpHost.value || "192.168.0.10");
+ tcpHost.addEventListener("change", () => storageSet("tcpHost", tcpHost.value.trim()));
+ }
+ if (tcpPort) {
+ tcpPort.value = storageGet("tcpPort", tcpPort.value || "502");
+ tcpPort.addEventListener("change", () => storageSet("tcpPort", tcpPort.value || "502"));
+ }
+ if (modbusSlaveId) {
+ modbusSlaveId.value = storageGet("modbusSlaveId", modbusSlaveId.value || "3");
+ modbusSlaveId.addEventListener("change", () => storageSet("modbusSlaveId", modbusSlaveId.value || "3"));
+ }
+ applyModbusTransportView();
+}
+
+function normalizeApiUrl(value) {
+ const trimmed = (value || "").trim();
+ if (trimmed && !/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
+ return normalizeApiUrl(`http://${trimmed}`);
+ }
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
+ try {
+ const parsed = new URL(trimmed);
+ return `${parsed.protocol}//${parsed.host}`;
+ } catch {
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
+ }
+ }
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
+}
+
+function ensureApiBase() {
+ if (state.apiBase) {
+ return state.apiBase;
+ }
+ if (window.location?.protocol?.startsWith("http") && window.location.host) {
+ state.apiBase = `${window.location.protocol}//${window.location.host}`;
+ } else {
+ state.apiBase = "http://127.0.0.1:8080";
+ }
+ if (apiInput) {
+ apiInput.value = state.apiBase;
+ }
+ storageSet("apiBase", state.apiBase);
+ return state.apiBase;
+}
+
+function endpoint(path) {
+ if (!state.apiBase) {
+ throw new Error("demo");
+ }
+ return `${state.apiBase}${path}`;
+}
+
+async function parseResponseBody(response) {
+ const body = await response.text();
+ if (!body) {
+ return {};
+ }
+ try {
+ return JSON.parse(body);
+ } catch {
+ return body;
+ }
+}
+
+async function apiGet(path) {
+ const response = await fetch(endpoint(path), {
+ method: "GET",
+ headers: { accept: "application/json" },
+ });
+ if (!response.ok) {
+ const body = await response.text();
+ throw new Error(`HTTP ${response.status}: ${body}`);
+ }
+ return parseResponseBody(response);
+}
+
+async function apiPut(path, payload) {
+ const response = await fetch(endpoint(path), {
+ method: "PUT",
+ headers: { "content-type": "application/json", accept: "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!response.ok) {
+ const body = await response.text();
+ throw new Error(`HTTP ${response.status}: ${body}`);
+ }
+ return parseResponseBody(response);
+}
+
+async function apiPost(path, payload) {
+ const response = await fetch(endpoint(path), {
+ method: "POST",
+ headers: { "content-type": "application/json", accept: "application/json" },
+ body: JSON.stringify(payload),
+ });
+ if (!response.ok) {
+ const body = await response.text();
+ throw new Error(`HTTP ${response.status}: ${body}`);
+ }
+ return parseResponseBody(response);
+}
+
+function extractPorts(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (Array.isArray(payload?.ports)) return payload.ports;
+ return [];
+}
+
+function extractSelectedPort(payload) {
+ if (typeof payload?.selected === "string") return payload.selected;
+ if (typeof payload?.port === "string") return payload.port;
+ return "";
+}
+
+async function apiGetFallback(paths) {
+ let lastError;
+ for (const path of paths) {
+ try {
+ return await apiGet(path);
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError || new Error("No available endpoint");
+}
+
+async function apiPostFallback(paths, payload) {
+ let lastError;
+ for (const path of paths) {
+ try {
+ return await apiPost(path, payload);
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError || new Error("No available endpoint");
+}
+
+function degreesFromPosition(position, maxDegrees = DEFAULT_OPEN_DEGREES_MAX) {
+ const maxValue = Number(maxDegrees) > 0 ? Number(maxDegrees) : DEFAULT_OPEN_DEGREES_MAX;
+ return Math.round(clamp(position, 0, 100) * maxValue / 100);
+}
+
+function positionFromDegrees(degrees, maxDegrees = DEFAULT_OPEN_DEGREES_MAX) {
+ const maxValue = Number(maxDegrees) > 0 ? Number(maxDegrees) : DEFAULT_OPEN_DEGREES_MAX;
+ return Math.round(clamp(degrees, 0, maxValue) * 100 / maxValue);
+}
+
+async function fetchComPorts() {
+ if (!state.apiBase) {
+ return { ports: [], selected: "" };
+ }
+ const payload = await apiGetFallback(SERIAL_API_PATHS.ports);
+ return {
+ ports: extractPorts(payload),
+ selected: extractSelectedPort(payload),
+ };
+}
+
+function applyPortOptions(ports, preferredPort = "") {
+ if (!comPortSelect) return;
+ const current = comPortSelect.value;
+ comPortSelect.innerHTML = "";
+ for (const item of ports) {
+ const entry = typeof item === "string" ? { device: item, description: "" } : item;
+ const option = document.createElement("option");
+ const value = entry.device || entry.port || "";
+ if (!value) continue;
+ option.value = value;
+ option.textContent = `${entry.description ? `${entry.description} (${value})` : value}`;
+ option.dataset.hwid = entry.hwid || "";
+ comPortSelect.appendChild(option);
+ }
+ const targetPort = preferredPort || current;
+ if (ports.find((item) => (typeof item === "string" ? item : item.device) === targetPort)) {
+ comPortSelect.value = targetPort;
+ } else if (current) {
+ comPortSelect.value = "";
+ }
+}
+
+async function refreshPortsLegacy(silent = false) {
+ const payload = await fetchComPorts();
+ const ports = payload?.ports || [];
+ applyPortOptions(ports, payload?.selected || "");
+ if (!silent) {
+ updateStatus(ports.length ? "Доступные порты обновлены" : "Порты не найдены", ports.length ? "ok" : "warn");
+ }
+}
+
+async function connectSelectedPortLegacyOld() {
+ if (!state.apiBase) {
+ updateStatus("Укажите API endpoint, чтобы работать с COM-портами", "warn");
+ return;
+ }
+ const value = comPortSelect.value;
+ if (!value) {
+ updateStatus("Выберите COM порт", "warn");
+ return;
+ }
+ selectedPort = value;
+ try {
+ const payload = { port: value, baud: 115200, baudrate: 115200, parity: "N", stopbits: 1, bytesize: 8, timeout: 0.3 };
+ await apiPostFallback(SERIAL_API_PATHS.connect, payload);
+ serialConnected = true;
+ serialStatus.textContent = `COM: подключён (${value})`;
+ serialStatus.className = "status status-ok";
+ updateStatus(`COM ${value} подключён`, "ok");
+ } catch (error) {
+ serialConnected = false;
+ serialStatus.textContent = "COM: ошибка подключения";
+ serialStatus.className = "status status-error";
+ updateStatus(`Ошибка подключения COM: ${error.message}`, "error");
+ }
+}
+
+async function disconnectPortLegacy() {
+ if (!state.apiBase) return;
+ try {
+ await apiPostFallback(SERIAL_API_PATHS.disconnect, {});
+ } catch {
+ // ignore
+ } finally {
+ serialConnected = false;
+ serialStatus.textContent = "COM: не подключён";
+ serialStatus.className = "status status-warn";
+ selectedPort = "";
+ }
+}
+
+function normalizeSensor(raw = {}) {
+ const rawId = raw.id ?? raw.sensorId ?? raw.key ?? raw.code;
+ const zone = String(raw.zone ?? raw.channel ?? raw.sensorZone ?? "").trim() || String(rawId ?? "").replace(/\D+/g, "");
+ const fallbackZone = zone ? `zone_${zone}` : "zone_1";
+ const id = rawId || fallbackZone;
+ return {
+ id: String(id),
+ name: String(raw.name ?? raw.label ?? `Термопара ${zone || 1}`),
+ zone: String(zone || "1"),
+ value: Number(raw.value ?? raw.current ?? raw.temperature ?? raw.temp ?? 0),
+ setpoint: Number(raw.setpoint ?? raw.target ?? raw.targetTemp ?? raw.tSet ?? 0),
+ unit: raw.unit ?? raw.units ?? "°C",
+ };
+}
+
+function normalizeValve(raw = {}) {
+ const rawId = raw.id ?? raw.valveId ?? raw.key ?? raw.code;
+ const zone = String(raw.zone ?? raw.channel ?? raw.controlZone ?? "").trim() || String(rawId ?? "").replace(/\D+/g, "");
+ const id = rawId || `valve_${zone || "1"}`;
+ const mode = String(raw.mode ?? raw.workMode ?? "auto").toLowerCase() === "manual" ? "manual" : "auto";
+ const maxOpenDegrees = Number(raw.openDegreesMax ?? raw.maxOpenDegrees ?? raw.degMax ?? DEFAULT_OPEN_DEGREES_MAX);
+ const maxDegrees = Number.isFinite(maxOpenDegrees) && maxOpenDegrees > 0 ? maxOpenDegrees : DEFAULT_OPEN_DEGREES_MAX;
+ let position = Number(raw.position ?? raw.pos ?? raw.percent ?? raw.value);
+ let openDegrees = Number(raw.openDegrees ?? raw.degree ?? raw.posDeg ?? raw.opening ?? raw.openAngle ?? raw.angle);
+
+ if (Number.isNaN(position)) {
+ if (Number.isNaN(openDegrees)) {
+ position = 0;
+ openDegrees = 0;
+ } else {
+ position = positionFromDegrees(openDegrees, maxDegrees);
+ }
+ } else if (Number.isNaN(openDegrees)) {
+ openDegrees = degreesFromPosition(position, maxDegrees);
+ } else {
+ position = positionFromDegrees(openDegrees, maxDegrees);
+ }
+ return {
+ id: String(id),
+ name: String(raw.name ?? raw.label ?? `Клапан ${zone || 1}`),
+ zone: String(zone || "1"),
+ mode,
+ position: clamp(position, 0, 100),
+ openDegrees: clamp(Math.round(openDegrees), 0, maxDegrees),
+ openDegreesMax: maxDegrees,
+ targetTemp: Number(raw.targetTemp ?? raw.targetTemperature ?? raw.setpoint ?? raw.tSet ?? 0),
+ isOpen: Boolean(raw.isOpen ?? raw.open ?? position > 0),
+ };
+}
+
+function extractCollection(payload, key) {
+ if (!payload) return null;
+ if (Array.isArray(payload)) return payload;
+ if (Array.isArray(payload[key])) return payload[key];
+ if (Array.isArray(payload.data?.[key])) return payload.data[key];
+ if (Array.isArray(payload.result?.[key])) return payload.result[key];
+ if (Array.isArray(payload.state?.[key])) return payload.state[key];
+ if (Array.isArray(payload.data?.[`temperature_${key}`])) return payload.data[`temperature_${key}`];
+ return null;
+}
+
+async function fetchCollection(paths, key, normalizer) {
+ let lastError;
+ for (const path of paths) {
+ try {
+ const payload = await apiGet(path);
+ const collection = extractCollection(payload, key);
+ if (!collection || !Array.isArray(collection)) continue;
+ return collection.map(normalizer);
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError || new Error(`No available endpoint for ${key}`);
+}
+
+async function sendWithFallback(paths, id, payload) {
+ let lastError;
+ for (const resolvePath of paths) {
+ try {
+ const route = typeof resolvePath === "function" ? resolvePath(id) : resolvePath;
+ return await apiPut(route, payload);
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError || new Error("No available write endpoint");
+}
+
+async function postWithFallback(paths, id, payload) {
+ let lastError;
+ for (const resolvePath of paths) {
+ try {
+ const route = typeof resolvePath === "function" ? resolvePath(id) : resolvePath;
+ return await apiPost(route, payload);
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError || new Error("No available write endpoint");
+}
+
+function writePayloadBase(id, type = "sensor") {
+ const sensor = findSensorById(id);
+ const valve = findValveById(id);
+ const entity = type === "valve" ? valve : sensor;
+ return {
+ id,
+ ...(entity ? { zone: entity.zone, name: entity.name } : {}),
+ };
+}
+
+function mergeDefaults(stored, defaults, normalizer = (item) => item) {
+ const source = Array.isArray(stored) ? stored : [];
+ const byId = new Map(source.map((item) => [String(item.id), normalizer(item)]));
+ return defaults.map((item) => ({
+ ...item,
+ ...(byId.get(String(item.id)) || {}),
+ }));
+}
+
+function buildDemos() {
+ state.sensors = mergeDefaults(storageGet("sensorState", []), defaultSensors, normalizeSensor);
+ state.valves = mergeDefaults(storageGet("valveState", []), defaultValves, normalizeValve);
+ storageSet("sensorState", state.sensors);
+ storageSet("valveState", state.valves);
+}
+
+function updateStatus(message, type = "ok") {
+ globalStatus.textContent = message;
+ globalStatus.className = `status-${type}`;
+}
+
+function setConnectButtonText() {
+ if (!connectPortBtn) {
+ return;
+ }
+ connectPortBtn.textContent = serialConnected ? "Отключить" : "Подключить";
+}
+
+function setSerialBusyState(isBusy, message) {
+ if (refreshPortsBtn) {
+ refreshPortsBtn.disabled = isBusy;
+ }
+ if (connectPortBtn) {
+ connectPortBtn.disabled = isBusy;
+ connectPortBtn.textContent = isBusy ? "..." : (serialConnected ? "Отключить" : "Подключить");
+ }
+ if (calibrateAllBtn) {
+ calibrateAllBtn.disabled = isBusy || !serialConnected;
+ }
+ if (message) {
+ serialStatus.textContent = message;
+ }
+}
+
+function applySerialStateFromPayload(payload = {}) {
+ const connected = Boolean(payload?.connected);
+ const transport = String(payload?.transport || getModbusTransport()).toLowerCase();
+ const isTcp = transport === "tcp";
+ const tcpAddress = payload?.address || (payload?.host || payload?.ip ? `${payload?.host || payload?.ip}:${payload?.tcpPort || payload?.tcp_port || 502}` : "");
+ const port = isTcp ? (tcpAddress || payload?.port || "") : (payload?.port || payload?.selected || payload?.com || "");
+ serialConnected = connected;
+ selectedPort = connected && port ? port : "";
+ if (comPortSelect && selectedPort) {
+ comPortSelect.value = selectedPort;
+ }
+ if (serialStatus) {
+ if (connected && selectedPort) {
+ serialStatus.textContent = isTcp ? `Modbus TCP: connected (${selectedPort})` : `COM: connected (${selectedPort})`;
+ serialStatus.className = "status status-ok";
+ } else {
+ serialStatus.textContent = isTcp ? "Modbus TCP: disconnected" : "COM: disconnected";
+ serialStatus.className = "status status-warn";
+ }
+ }
+ if (calibrateAllBtn) {
+ calibrateAllBtn.disabled = !serialConnected;
+ }
+ setConnectButtonText();
+ storageSet("comPort", selectedPort || "");
+}
+
+async function loadState() {
+ if (!state.apiBase) {
+ buildDemos();
+ simulateSensorPhysics();
+ statusEl.textContent = "Режим: демо (без API)";
+ statusEl.className = "status status-warn";
+ globalStatus.textContent = "Демо: все изменения сохраняются в браузере";
+ return;
+ }
+ const [sensors, valves] = await Promise.all([
+ fetchCollection(API_PATHS.sensorsRead, "sensors", normalizeSensor),
+ fetchCollection(API_PATHS.valvesRead, "valves", normalizeValve),
+ ]);
+ state.sensors = sensors;
+ state.valves = valves;
+ statusEl.textContent = "Подключено к API";
+ statusEl.className = "status status-ok";
+ globalStatus.textContent = `API подключен: ${state.apiBase}`;
+}
+
+function findSensorByZone(zone) {
+ return state.sensors.find((s) => String(s.zone) === String(zone));
+}
+
+function findValveByZone(zone) {
+ return state.valves.find((v) => String(v.zone) === String(zone));
+}
+
+function findSensorById(id) {
+ return state.sensors.find((s) => s.id === id);
+}
+
+function findValveById(id) {
+ return state.valves.find((v) => v.id === id);
+}
+
+function renderSensors() {
+ const sensorsRoot = document.getElementById("sensors");
+ if (!sensorsRoot) return;
+ sensorsRoot.innerHTML = "";
+ const sensorPanel = sensorsRoot.closest(".panel");
+ if (sensorPanel) {
+ sensorPanel.hidden = false;
+ const title = sensorPanel.querySelector("h2");
+ if (title) title.textContent = "Каналы обработки датчиков";
+ }
+}
+
+function renderValves() {
+ const separateValvesRoot = document.getElementById("valves");
+ if (separateValvesRoot) {
+ separateValvesRoot.innerHTML = "";
+ const separateValvesPanel = separateValvesRoot.closest(".panel");
+ if (separateValvesPanel) separateValvesPanel.hidden = true;
+ }
+
+ const valvesRoot = document.getElementById("sensors");
+ if (!valvesRoot) return;
+ valvesRoot.innerHTML = "";
+ const valvePanel = valvesRoot.closest(".panel");
+ if (valvePanel) {
+ valvePanel.hidden = false;
+ const title = valvePanel.querySelector("h2");
+ if (title) title.textContent = "Каналы обработки датчиков";
+ }
+
+ const sensorLocations = storageGet("sensorLocations", {});
+ state.sensors.forEach((sensor, index) => {
+ const channelNumber = index + 1;
+ const openValve = state.valves[index * 2] || {};
+ const closeValve = state.valves[index * 2 + 1] || {};
+ const valve = openValve.id ? openValve : closeValve;
+ const card = document.createElement("article");
+ card.className = "item compact-item valve-item channel-card full-channel-card";
+ card.dataset.id = valve.id || `valve_${channelNumber * 2 - 1}`;
+
+ const rawTemp = Number(sensor.value);
+ const temp = Number.isFinite(rawTemp) ? rawTemp : 0;
+ const tempText = Number.isFinite(rawTemp) ? temp.toFixed(1) : "--";
+ let setpoint = Number(valve.targetTemp ?? sensor.setpoint ?? 28);
+ const positionRaw = Number(valve.position ?? 0);
+ const position = Number.isFinite(positionRaw) ? Math.max(0, Math.min(100, Math.round(positionRaw))) : 0;
+ const maxDegreesRaw = Number(valve.openDegreesMax ?? DEFAULT_OPEN_DEGREES_MAX);
+ const maxDegrees = Number.isFinite(maxDegreesRaw) && maxDegreesRaw > 0 ? maxDegreesRaw : DEFAULT_OPEN_DEGREES_MAX;
+ const angleRaw = Number(valve.openDegrees ?? ((position / 100) * maxDegrees));
+ const openDegrees = Number.isFinite(angleRaw) ? Math.max(0, Math.min(maxDegrees, Math.round(angleRaw))) : 0;
+ const openActive = Boolean(openValve.isOpen || position > 0 || openDegrees > 0);
+ const closeActive = Boolean(closeValve.isOpen || position <= 0);
+ const connected = Boolean(valve.connected ?? valve.isConnected ?? sensor.connected ?? state.connected);
+ const tempFill = Math.max(0, Math.min(100, ((temp + 5) / 55) * 100));
+ const mode = valve.mode === "manual" ? "manual" : "auto";
+ const modeText = mode === "manual" ? "ручной" : "авто";
+ const sensorName = sensor.name || `Датчик ${channelNumber}`;
+ const sensorId = sensor.id || `zone_${channelNumber}`;
+ const setpointDrafts = storageGet("setpointDrafts", {});
+ const draftValue = setpointDrafts[sensorId] ?? setpointDrafts[valve.id];
+ if (draftValue !== undefined) {
+ setpoint = parseGuiNumber(draftValue, setpoint);
+ }
+ const ds18b20Id = sensor.ds18b20Id || sensor.romId || sensor.rom || sensor.address || "--";
+ const location = sensorLocations[sensorId] || sensor.location || defaultChannelLocation(index);
+ const locationOptions = CHANNEL_LOCATIONS.map((name) => (
+ ``
+ )).join("");
+ const openValveId = openValve.id || `valve_${channelNumber * 2 - 1}`;
+ const closeValveId = closeValve.id || `valve_${channelNumber * 2}`;
+ const openHex = (Number(String(openValveId).replace(/\D/g, "")) || channelNumber * 2 - 1).toString(16).toUpperCase().padStart(2, "0");
+ const closeHex = (Number(String(closeValveId).replace(/\D/g, "")) || channelNumber * 2).toString(16).toUpperCase().padStart(2, "0");
+ const setpointText = Number.isFinite(setpoint) ? setpoint.toFixed(1) : "--";
+ const delta = Number.isFinite(rawTemp) && Number.isFinite(setpoint) ? temp - setpoint : NaN;
+ const deltaText = Number.isFinite(delta) ? `${delta > 0 ? "+" : ""}${delta.toFixed(1)}°C` : "--";
+ const deltaClass = !Number.isFinite(delta) || Math.abs(delta) <= 0.5 ? "ok" : delta > 0 ? "hot" : "cold";
+ const stateText = openActive ? "открытие" : closeActive ? "закрытие" : "стоп";
+
+ card.innerHTML = `
+
+
+ Канал ${channelNumber}
+ ${sensorName} · ${location} · зона ${sensor.zone || channelNumber}
+
+
связь
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ угол открытия
+ ${openDegrees}°
+ максимум ${Math.round(maxDegrees)}°
+
+
+
+
+
температура${tempText}°C
+
уставка${setpointText}°C
+
отклонение${deltaText}
+
расположение${location}
+
ID DS18B20${ds18b20Id}
+
режим${modeText}
+
угол/max${openDegrees}° / ${Math.round(maxDegrees)}°
+
команда${stateText}
+
связь${connected ? "есть" : "нет"}
+
канал${channelNumber}
+
+
+
+
+
+ клапан откр ${channelNumber}
+ клапан закр ${channelNumber}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ valvesRoot.appendChild(card);
+ });
+}
+
+function render() {
+ renderSensors();
+ renderValves();
+}
+
+async function applySetpoint(sensorId) {
+ const sensor = findSensorById(sensorId);
+ const input = document.querySelector(`.setpoint[data-id="${sensorId}"]`);
+ const value = Number(input.value);
+ if (Number.isNaN(value)) {
+ updateStatus("Некорректное значение уставки", "warn");
+ return;
+ }
+
+ if (!state.apiBase) {
+ sensor.setpoint = value;
+ storageSet("sensorState", state.sensors);
+ render();
+ updateStatus(`Демо: уставка ${sensor.name} = ${value.toFixed(1)} °C`, "ok");
+ return;
+ }
+
+ const payload = {
+ ...writePayloadBase(sensorId, "sensor"),
+ setpoint: value,
+ id: sensorId,
+ };
+
+ try {
+ await sendWithFallback(API_PATHS.sensorWrite, sensorId, payload);
+ await loadState();
+ render();
+ updateStatus(`Уставка ${sensor.name} обновлена`, "ok");
+ } catch (error) {
+ updateStatus(`Ошибка уставки ${sensor.name}: ${error.message}`, "error");
+ }
+}
+
+async function applyValveMode(valveId, mode) {
+ const valve = findValveById(valveId);
+ if (!valve) return;
+ valve.mode = mode;
+
+ if (!state.apiBase) {
+ storageSet("valveState", state.valves);
+ render();
+ updateStatus(`Демо: ${valve.name} → ${mode}`, "ok");
+ return;
+ }
+
+ const payload = {
+ ...writePayloadBase(valveId, "valve"),
+ mode,
+ id: valveId,
+ };
+
+ try {
+ await sendWithFallback(API_PATHS.valveWrite, valveId, payload);
+ await loadState();
+ render();
+ updateStatus(`Режим ${valve.name}: ${mode}`, "ok");
+ } catch (error) {
+ updateStatus(`Ошибка режима ${valve.name}: ${error.message}`, "error");
+ await loadState();
+ render();
+ }
+}
+
+async function applyValveTarget(valveId) {
+ const valve = findValveById(valveId);
+ const input = document.querySelector(`.targetTemp[data-id="${valveId}"]`);
+ const value = Number(input.value);
+ if (Number.isNaN(value)) {
+ updateStatus("Некорректная целевая температура", "warn");
+ return;
+ }
+
+ if (!state.apiBase) {
+ valve.targetTemp = value;
+ storageSet("valveState", state.valves);
+ updateStatus(`Демо: цель ${valve.name} = ${value.toFixed(1)} °C`, "ok");
+ return;
+ }
+
+ const payload = {
+ ...writePayloadBase(valveId, "valve"),
+ targetTemp: value,
+ id: valveId,
+ };
+
+ try {
+ await sendWithFallback(API_PATHS.valveWrite, valveId, payload);
+ await loadState();
+ render();
+ updateStatus(`Целевая температура ${valve.name} обновлена`, "ok");
+ } catch (error) {
+ updateStatus(`Ошибка цели ${valve.name}: ${error.message}`, "error");
+ }
+}
+
+async function applyValvePosition(valveId) {
+ const valve = findValveById(valveId);
+ const input = document.querySelector(`.position[data-id="${valveId}"]`);
+ const value = Number(input.value);
+ if (Number.isNaN(value)) {
+ updateStatus("Некорректная позиция клапана", "warn");
+ return;
+ }
+ valve.position = clamp(Math.round(value), 0, 100);
+ valve.openDegrees = degreesFromPosition(valve.position, valve.openDegreesMax);
+ valve.isOpen = valve.position > 0;
+
+ if (!state.apiBase) {
+ storageSet("valveState", state.valves);
+ render();
+ updateStatus(`Демо: ручная позиция ${valve.name} = ${value}%`, "ok");
+ return;
+ }
+
+ const payload = {
+ ...writePayloadBase(valveId, "valve"),
+ mode: "manual",
+ position: valve.position,
+ openDegrees: valve.openDegrees,
+ openDegreesMax: valve.openDegreesMax,
+ id: valveId,
+ };
+
+ try {
+ await sendWithFallback(API_PATHS.valveWrite, valveId, payload);
+ await loadState();
+ render();
+ updateStatus(`Позиция ${valve.name} обновлена`, "ok");
+ } catch (error) {
+ updateStatus(`Ошибка позиции ${valve.name}: ${error.message}`, "error");
+ }
+}
+
+async function calibrateValve(valveId) {
+ const valve = findValveById(valveId);
+ if (!valve) return;
+
+ if (!state.apiBase) {
+ valve.position = 0;
+ valve.openDegrees = 0;
+ valve.isOpen = false;
+ storageSet("valveState", state.valves);
+ render();
+ updateStatus(`Демо: калибровка ${valve.name} выполнена`, "ok");
+ return;
+ }
+
+ try {
+ const response = await postWithFallback(API_PATHS.valveCalibrate, valveId, {});
+ const updated = response?.valve || response;
+ if (updated && updated.id) {
+ const index = state.valves.findIndex((item) => item.id === valveId);
+ if (index !== -1) {
+ state.valves[index] = normalizeValve(updated);
+ } else {
+ await loadState();
+ }
+ } else {
+ await loadState();
+ }
+ render();
+ updateStatus(`Калибровка ${valve.name} выполнена`, "ok");
+ } catch (error) {
+ updateStatus(`Ошибка калибровки ${valve.name}: ${error.message}`, "error");
+ }
+}
+
+async function calibrateAllValves(silent = false) {
+ if (!state.apiBase) {
+ state.valves = state.valves.map((valve) => ({
+ ...valve,
+ position: 0,
+ openDegrees: 0,
+ isOpen: false,
+ }));
+ render();
+ if (!silent) {
+ updateStatus("Демо: калибровка всех клапанов выполнена", "ok");
+ }
+ return;
+ }
+
+ try {
+ const response = await apiPostFallback(API_PATHS.valvesCalibrateAll, {});
+ const payload = Array.isArray(response?.valves) ? response.valves : null;
+ if (payload) {
+ state.valves = payload.map(normalizeValve);
+ } else {
+ await loadState();
+ }
+ render();
+ if (!silent) {
+ updateStatus("Калибровка всех клапанов выполнена", "ok");
+ }
+ } catch (error) {
+ if (!silent) {
+ updateStatus(`Ошибка калибровки всех клапанов: ${error.message}`, "error");
+ }
+ }
+}
+
+function simulateSensorPhysics() {
+ if (state.timer) clearInterval(state.timer);
+ state.timer = setInterval(() => {
+ for (const sensor of state.sensors) {
+ const valve = findValveByZone(sensor.zone);
+ const v = valve || {};
+ let target;
+ if (v.mode === "manual") {
+ target = 20 + (clamp(v.position ?? 0, 0, 100) / 100) * 70;
+ } else {
+ target = v.targetTemp ?? sensor.setpoint ?? 30;
+ }
+ const drift = target - sensor.value;
+ const noise = (Math.random() - 0.5) * 0.2;
+ sensor.value = clamp(sensor.value + drift * 0.08 + noise, -40, 150);
+ sensor.value = Number(sensor.value.toFixed(2));
+ }
+ for (const valve of state.valves) {
+ const maxOpenDegrees = Number(valve.openDegreesMax) > 0 ? Number(valve.openDegreesMax) : DEFAULT_OPEN_DEGREES_MAX;
+ valve.openDegrees = Math.round(degreesFromPosition(valve.position ?? 0, maxOpenDegrees));
+ valve.isOpen = (valve.position ?? 0) > 0;
+ }
+ storageSet("sensorState", state.sensors);
+ storageSet("valveState", state.valves);
+ render();
+ }, 2500);
+}
+
+function attachEvents() {
+ refreshBtn.addEventListener("click", () => {
+ const url = normalizeApiUrl(apiInput.value);
+ state.apiBase = url;
+ storageSet("apiBase", state.apiBase);
+ refreshAll(true);
+ refreshPorts(false);
+ });
+
+ refreshPortsBtn.addEventListener("click", () => {
+ refreshPorts();
+ });
+
+ connectPortBtn.addEventListener("click", async () => {
+ if (serialConnected) {
+ await disconnectPort();
+ setConnectButtonText();
+ return;
+ }
+ await connectSelectedPort();
+ setConnectButtonText();
+ });
+
+ if (calibrateAllBtn) {
+ calibrateAllBtn.addEventListener("click", () => {
+ calibrateAllValves(false);
+ });
+ }
+
+ saveApiBtn.addEventListener("click", () => {
+ const url = normalizeApiUrl(apiInput.value);
+ state.apiBase = url;
+ storageSet("apiBase", url);
+ if (!url) {
+ updateStatus("API отключён; переход в демо", "warn");
+ statusEl.textContent = "Режим: демо (без API)";
+ statusEl.className = "status status-warn";
+ } else {
+ updateStatus(`Сохранён API: ${state.apiBase}`, "ok");
+ statusEl.textContent = "Сохранён адрес API";
+ statusEl.className = "status status-ok";
+ }
+ render();
+ refreshPorts(false);
+ if (!url) {
+ serialStatus.textContent = "COM: не подключён";
+ serialStatus.className = "status status-warn";
+ connectPortBtn.textContent = "Подключить";
+ selectedPort = "";
+ serialConnected = false;
+ }
+ });
+
+ sensorsEl.addEventListener("click", (event) => {
+ const target = event.target;
+ if (target.classList.contains("applySetpoint")) {
+ applySetpoint(target.dataset.id);
+ }
+ });
+
+ valvesEl.addEventListener("click", (event) => {
+ const target = event.target;
+ if (target.classList.contains("modeBtn")) {
+ const button = target.closest(".toggle");
+ const valveId = button.dataset.id;
+ const mode = target.dataset.mode;
+ const card = button.closest(".item");
+ if (!card) return;
+ const autoBlock = card.querySelector(".valveAuto");
+ const manualBlock = card.querySelector(".valveManual");
+ const applyManualBtn = card.querySelector(".applyManual");
+
+ button.querySelectorAll(".modeBtn").forEach((b) => b.classList.remove("active"));
+ target.classList.add("active");
+ if (mode === "auto") {
+ autoBlock.classList.remove("hidden");
+ manualBlock.classList.add("hidden");
+ applyManualBtn.classList.add("hidden");
+ } else {
+ autoBlock.classList.add("hidden");
+ manualBlock.classList.remove("hidden");
+ applyManualBtn.classList.remove("hidden");
+ }
+ applyValveMode(valveId, mode);
+ }
+ if (target.classList.contains("applyTarget")) {
+ applyValveTarget(target.dataset.id);
+ }
+ if (target.classList.contains("applyManual")) {
+ applyValvePosition(target.dataset.id);
+ }
+ if (target.classList.contains("calibrateValve")) {
+ calibrateValve(target.dataset.id);
+ }
+ });
+
+ valvesEl.addEventListener("input", (event) => {
+ const target = event.target;
+ if (target.classList.contains("position")) {
+ const label = target.closest("label");
+ const span = label ? label.querySelector("strong") : null;
+ if (span) span.textContent = `${target.value}%`;
+ }
+ });
+}
+
+async function refreshAll(silent = false) {
+ try {
+ await loadState();
+ render();
+ if (!silent) updateStatus("Данные обновлены", "ok");
+ } catch (error) {
+ if (state.apiBase) {
+ statusEl.textContent = `Ошибка API: ${error.message}`;
+ statusEl.className = "status status-error";
+ globalStatus.textContent = "Не удалось получить данные с API. Откат в демо";
+ globalStatus.className = "status-warn";
+ await refreshPorts();
+ render();
+ } else {
+ updateStatus(`Демо инициализирован: ${error.message}`, "warn");
+ }
+ }
+}
+
+async function connectSelectedPortLegacy() {
+ if (!state.apiBase) {
+ updateStatus("Укажите API endpoint для работы с COM-портами", "warn");
+ return;
+ }
+ const value = comPortSelect.value;
+ if (!value) {
+ updateStatus("Выберите COM порт", "warn");
+ return;
+ }
+ const payload = { port: value, baud: 115200, baudrate: 115200, parity: "N", stopbits: 1, bytesize: 8, timeout: 0.3 };
+ try {
+ await apiPostFallback(SERIAL_API_PATHS.connect, payload);
+ selectedPort = value;
+ serialConnected = true;
+ storageSet("comPort", value);
+ serialStatus.textContent = `COM: подключен (${value})`;
+ serialStatus.className = "status status-ok";
+ connectPortBtn.textContent = "Отключить";
+ updateStatus(`COM ${value} подключен`, "ok");
+ } catch (error) {
+ serialConnected = false;
+ serialStatus.textContent = "COM: ошибка подключения";
+ serialStatus.className = "status status-error";
+ connectPortBtn.textContent = "Подключить";
+ storageSet("comPort", "");
+ selectedPort = "";
+ updateStatus(`Ошибка подключения COM: ${error.message}`, "error");
+ }
+}
+
+async function disconnectPortLegacy() {
+ if (!state.apiBase) return;
+ try {
+ await apiPostFallback(SERIAL_API_PATHS.disconnect, {});
+ } catch {
+ // ignore
+ } finally {
+ serialConnected = false;
+ selectedPort = "";
+ storageSet("comPort", "");
+ serialStatus.textContent = "COM: не подключен";
+ serialStatus.className = "status status-warn";
+ connectPortBtn.textContent = "Подключить";
+ }
+}
+
+async function restoreSerialUiLegacy() {
+ if (!state.apiBase) return;
+ try {
+ const status = await apiGetFallback(SERIAL_API_PATHS.status);
+ serialConnected = Boolean(status?.connected);
+ const port = status?.port || status?.selected;
+ if (serialConnected && port) {
+ selectedPort = port;
+ comPortSelect.value = selectedPort;
+ serialStatus.textContent = `COM: подключен (${selectedPort})`;
+ serialStatus.className = "status status-ok";
+ connectPortBtn.textContent = "Отключить";
+ storageSet("comPort", selectedPort);
+ } else {
+ selectedPort = "";
+ storageSet("comPort", "");
+ serialStatus.textContent = "COM: не подключен";
+ serialStatus.className = "status status-warn";
+ connectPortBtn.textContent = "Подключить";
+ }
+ } catch {
+ // no-op
+ }
+}
+
+async function refreshPorts(silent = false) {
+ if (!state.apiBase) {
+ ensureApiBase();
+ }
+ if (!state.apiBase) {
+ if (!silent) {
+ updateStatus("Set API base URL first", "warn");
+ }
+ return;
+ }
+ setSerialBusyState(true, "Scanning COM ports...");
+ try {
+ const payload = await fetchComPorts();
+ const ports = Array.isArray(payload?.ports) ? payload.ports : [];
+ const preferredPort = payload?.selected || selectedPort || storageGet("comPort", "");
+ applyPortOptions(ports, preferredPort);
+ if (!silent) {
+ updateStatus(
+ ports.length ? `Found COM ports: ${ports.length}` : "No COM ports found",
+ ports.length ? "ok" : "warn"
+ );
+ }
+ } catch (error) {
+ applyPortOptions([], "");
+ updateStatus(`Port scan error: ${error.message}`, "error");
+ } finally {
+ setSerialBusyState(false);
+ }
+}
+
+async function connectSelectedPort() {
+ if (!state.apiBase) {
+ updateStatus("Set API base URL first", "warn");
+ return;
+ }
+ const requestedPort = comPortSelect.value;
+ if (!requestedPort) {
+ updateStatus("Select COM port", "warn");
+ return;
+ }
+ setSerialBusyState(true, `Connecting ${requestedPort}...`);
+ try {
+ const payload = {
+ port: requestedPort,
+ baud: 115200,
+ baudrate: 115200,
+ parity: "N",
+ stopBits: 1,
+ stopbits: 1,
+ byteSize: 8,
+ bytesize: 8,
+ timeout: 0.3,
+ };
+ const status = await apiPostFallback(SERIAL_API_PATHS.connect, payload);
+ applySerialStateFromPayload({
+ connected: true,
+ port: status?.port || requestedPort,
+ selected: status?.selected || requestedPort,
+ });
+ await calibrateAllValves(true);
+ updateStatus(`COM ${requestedPort} connected`, "ok");
+ await refreshPorts(true);
+ } catch (error) {
+ applySerialStateFromPayload({ connected: false });
+ updateStatus(`COM connect error: ${error.message}`, "error");
+ } finally {
+ setSerialBusyState(false);
+ }
+}
+
+async function connectRtuPort() {
+ if (!state.apiBase) {
+ updateStatus("Укажи API endpoint для Modbus RTU", "warn");
+ return;
+ }
+
+ const value = comPortSelect?.value || selectedPort;
+ if (!value) {
+ updateStatus("Выберите COM порт", "warn");
+ return;
+ }
+
+ const unitId = Number(modbusSlaveId?.value || 3);
+ storageSet("modbusTransport", "rtu");
+ storageSet("comPort", value);
+ storageSet("modbusSlaveId", String(unitId));
+ setSerialBusyState(true, `Modbus RTU: подключение ${value}, slave ${unitId}...`);
+ try {
+ const status = await apiPostFallback(SERIAL_API_PATHS.connect, {
+ transport: "rtu",
+ mode: "rtu",
+ port: value,
+ baud: 115200,
+ baudrate: 115200,
+ parity: "N",
+ stopbits: 1,
+ bytesize: 8,
+ timeout: 0.8,
+ unitId,
+ slaveId: unitId,
+ });
+ applySerialStateFromPayload({
+ ...(status || {}),
+ connected: Boolean(status?.connected ?? status?.ok),
+ transport: "rtu",
+ port: value,
+ });
+ updateStatus(`Modbus RTU connected: ${value}, slave ${unitId}`, "ok");
+ } catch (error) {
+ applySerialStateFromPayload({ connected: false, transport: "rtu" });
+ updateStatus(`Modbus RTU error: ${error.message}`, "error");
+ } finally {
+ setSerialBusyState(false);
+ applyModbusTransportView();
+ }
+}
+
+async function connectTcpPort() {
+ if (!state.apiBase) {
+ updateStatus("Укажи API endpoint для Modbus TCP", "warn");
+ return;
+ }
+
+ const host = (tcpHost?.value || "").trim();
+ const port = Number(tcpPort?.value || 502);
+ const unitId = Number(modbusSlaveId?.value || 3);
+ if (!host) {
+ updateStatus("Укажи IP адрес Modbus TCP", "warn");
+ return;
+ }
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
+ updateStatus("Некорректный TCP порт", "warn");
+ return;
+ }
+
+ storageSet("modbusTransport", "tcp");
+ storageSet("tcpHost", host);
+ storageSet("tcpPort", String(port));
+ storageSet("modbusSlaveId", String(unitId));
+ setSerialBusyState(true, `Modbus TCP: подключение ${host}:${port}, slave ${unitId}...`);
+ try {
+ const status = await apiPostFallback(SERIAL_API_PATHS.connect, {
+ transport: "tcp",
+ mode: "tcp",
+ host,
+ ip: host,
+ tcpPort: port,
+ tcp_port: port,
+ port,
+ unitId,
+ slaveId: unitId,
+ timeout: 0.8,
+ });
+ applySerialStateFromPayload({
+ ...(status || {}),
+ connected: Boolean(status?.connected ?? status?.ok),
+ transport: "tcp",
+ host,
+ tcpPort: port,
+ address: `${host}:${port}`,
+ });
+ updateStatus(`Modbus TCP connected: ${host}:${port}, slave ${unitId}`, "ok");
+ } catch (error) {
+ applySerialStateFromPayload({ connected: false, transport: "tcp" });
+ updateStatus(`Modbus TCP error: ${error.message}`, "error");
+ } finally {
+ setSerialBusyState(false);
+ applyModbusTransportView();
+ }
+}
+
+if (connectPortBtn) {
+ connectPortBtn.addEventListener("click", async (event) => {
+ event.preventDefault();
+ event.stopImmediatePropagation();
+
+ if (serialConnected) {
+ await disconnectPort();
+ setConnectButtonText();
+ applyModbusTransportView();
+ return;
+ }
+
+ if (getModbusTransport() === "tcp") {
+ await connectTcpPort();
+ } else {
+ await connectRtuPort();
+ }
+ setConnectButtonText();
+ }, true);
+}
+
+async function disconnectPort() {
+ if (!state.apiBase) {
+ applySerialStateFromPayload({ connected: false });
+ return;
+ }
+ setSerialBusyState(true, "Disconnecting...");
+ try {
+ const status = await apiPostFallback(SERIAL_API_PATHS.disconnect, {});
+ applySerialStateFromPayload(status || { connected: false });
+ } catch {
+ // ignore
+ } finally {
+ applySerialStateFromPayload({ connected: false });
+ setSerialBusyState(false);
+ }
+}
+
+async function restoreSerialUi() {
+ if (!state.apiBase) {
+ return;
+ }
+ try {
+ const status = await apiGetFallback(SERIAL_API_PATHS.status);
+ applySerialStateFromPayload(status);
+ if (status?.connected) {
+ await calibrateAllValves(true);
+ }
+ } catch {
+ applySerialStateFromPayload({ connected: false });
+ }
+}
+
+
+
+// Channel location selector
+document.addEventListener("change", (event) => {
+ const select = event.target.closest(".sensorLocation");
+ if (!select) return;
+
+ const sensorId = select.dataset.id;
+ const sensor = state.sensors.find((item) => item.id === sensorId);
+ if (sensor) {
+ sensor.location = select.value;
+ storageSet("sensorState", state.sensors);
+ }
+
+ const locations = storageGet("sensorLocations", {});
+ locations[sensorId] = select.value;
+ storageSet("sensorLocations", locations);
+
+ render();
+});
+
+// Controls inside sensor processing channel cards
+document.addEventListener("input", (event) => {
+ const targetInput = event.target.closest(".full-channel-card .targetTemp");
+ if (targetInput) {
+ const card = targetInput.closest(".full-channel-card");
+ const valveId = card?.dataset.id;
+ const targetTemp = parseGuiNumber(targetInput.value, 0);
+ const valve = state.valves.find((item) => item.id === valveId);
+ if (valve) {
+ valve.targetTemp = targetTemp;
+ }
+
+ const valveNumber = Number(String(valveId || "").replace(/\D/g, "")) || 1;
+ const sensorIndex = Math.max(0, Math.floor((valveNumber - 1) / 2));
+ const sensor = state.sensors[sensorIndex];
+ if (sensor) {
+ sensor.setpoint = targetTemp;
+ }
+
+ const setpointDrafts = storageGet("setpointDrafts", {});
+ if (sensor?.id) {
+ setpointDrafts[sensor.id] = targetInput.value;
+ }
+ if (valveId) {
+ setpointDrafts[valveId] = targetInput.value;
+ }
+ storageSet("setpointDrafts", setpointDrafts);
+ storageSet("sensorState", state.sensors);
+ storageSet("valveState", state.valves);
+ return;
+ }
+
+ const positionInput = event.target.closest(".full-channel-card .position");
+ if (!positionInput) return;
+ const label = positionInput.closest("label");
+ const value = Number(positionInput.value || 0);
+ const strong = label?.querySelector("strong");
+ if (strong) strong.textContent = `${value}%`;
+});
+
+document.addEventListener("click", async (event) => {
+ const button = event.target.closest(".full-channel-card button");
+ if (!button) return;
+
+ const card = button.closest(".full-channel-card");
+ const valveId = card?.dataset.id;
+ if (!card || !valveId) return;
+
+ if (button.classList.contains("modeBtn")) {
+ const mode = button.dataset.mode === "manual" ? "manual" : "auto";
+ card.querySelectorAll(".modeBtn").forEach((item) => item.classList.remove("active"));
+ button.classList.add("active");
+ const valve = state.valves.find((item) => item.id === valveId);
+ if (valve) valve.mode = mode;
+ storageSet("valveState", state.valves);
+ await sendWithFallback(API_PATHS.valveWrite, valveId, {
+ ...writePayloadBase(valveId, "valve"),
+ id: valveId,
+ mode,
+ });
+ return;
+ }
+
+ if (button.classList.contains("applyTarget")) {
+ const targetInput = card.querySelector(".targetTemp");
+ const targetTemp = parseGuiNumber(targetInput?.value, 0);
+ const valve = state.valves.find((item) => item.id === valveId);
+ if (valve) valve.targetTemp = targetTemp;
+ const valveNumber = Number(String(valveId || "").replace(/\D/g, "")) || 1;
+ const sensorIndex = Math.max(0, Math.floor((valveNumber - 1) / 2));
+ const sensor = state.sensors[sensorIndex];
+ if (sensor) sensor.setpoint = targetTemp;
+ const setpointDrafts = storageGet("setpointDrafts", {});
+ if (sensor?.id) {
+ setpointDrafts[sensor.id] = targetInput?.value ?? String(targetTemp);
+ }
+ setpointDrafts[valveId] = targetInput?.value ?? String(targetTemp);
+ storageSet("setpointDrafts", setpointDrafts);
+ storageSet("sensorState", state.sensors);
+ storageSet("valveState", state.valves);
+ await sendWithFallback(API_PATHS.valveWrite, valveId, {
+ ...writePayloadBase(valveId, "valve"),
+ id: valveId,
+ targetTemp,
+ });
+ await loadState();
+ return;
+ }
+
+ if (button.classList.contains("applyManual")) {
+ const positionInput = card.querySelector(".position");
+ const position = Number(positionInput?.value || 0);
+ const valve = state.valves.find((item) => item.id === valveId);
+ if (valve) {
+ valve.position = position;
+ valve.isOpen = position > 0;
+ }
+ storageSet("valveState", state.valves);
+ await sendWithFallback(API_PATHS.valveWrite, valveId, {
+ ...writePayloadBase(valveId, "valve"),
+ id: valveId,
+ position,
+ });
+ await loadState();
+ return;
+ }
+
+ if (button.classList.contains("calibrateValve")) {
+ await postWithFallback(API_PATHS.valveCalibrate, valveId, {
+ ...writePayloadBase(valveId, "valve"),
+ id: valveId,
+ });
+ await loadState();
+ }
+});
+
+// Quick manual open/close buttons for channel cards
+document.addEventListener("click", (event) => {
+ const quickButton = event.target.closest(".quickPosition");
+ if (!quickButton) return;
+
+ const card = quickButton.closest(".valve-item");
+ if (!card) return;
+
+ const manualButton = card.querySelector(".valveManual");
+ if (manualButton && !manualButton.classList.contains("active")) {
+ manualButton.click();
+ }
+
+ const positionInput = card.querySelector(".position");
+ if (positionInput) {
+ positionInput.value = quickButton.dataset.position || "0";
+ positionInput.dispatchEvent(new Event("input", { bubbles: true }));
+ }
+
+ const applyButton = card.querySelector(".applyManual");
+ if (applyButton) applyButton.click();
+});
+window.addEventListener("load", async () => {
+ apiInput.value = storageGet("apiBase", "");
+ state.apiBase = normalizeApiUrl(apiInput.value);
+ initModbusTransportControls();
+ ensureApiBase();
+ selectedPort = storageGet("comPort", "");
+ attachEvents();
+ await refreshAll(true);
+ if (getModbusTransport() !== "tcp") {
+ await refreshPorts(true);
+ }
+ render();
+ if (selectedPort) {
+ comPortSelect.value = selectedPort;
+ }
+ await restoreSerialUi();
+ setInterval(() => {
+ if (state.apiBase) {
+ loadState().then(render).catch(() => {});
+ }
+ }, 3000);
+});
diff --git a/john103C6T6NewVer/index.html b/john103C6T6NewVer/index.html
new file mode 100644
index 0000000..5b28f35
--- /dev/null
+++ b/john103C6T6NewVer/index.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+ Тепло и клапаны — управление
+
+
+
+
+
+
+
+
+
+
+ Датчики температуры
+
+
+
+
+ Клапаны
+
+
+
+
+
+
+
+
+
+
diff --git a/john103C6T6NewVer/mock_server.py b/john103C6T6NewVer/mock_server.py
new file mode 100644
index 0000000..23de48b
--- /dev/null
+++ b/john103C6T6NewVer/mock_server.py
@@ -0,0 +1,314 @@
+import json
+import random
+import threading
+import time
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from pathlib import Path
+from urllib.parse import parse_qs, urlparse
+
+
+SENSOR_COUNT = 16
+VALVE_COUNT = 32
+DEFAULT_OPEN_DEGREES_MAX = 90
+
+
+def make_default_sensors():
+ return [
+ {
+ "id": f"zone_{number}",
+ "name": f"Датчик {number}",
+ "value": 24.0 + (number % 4),
+ "setpoint": 28.0,
+ "unit": "°C",
+ "zone": str(number),
+ "ds18b20Id": f"28-00-00-00-00-00-00-{number:02X}",
+ }
+ for number in range(1, SENSOR_COUNT + 1)
+ ]
+
+
+def make_default_valves():
+ valves = []
+ for index in range(VALVE_COUNT):
+ number = index + 1
+ zone = (index % SENSOR_COUNT) + 1
+ valves.append(
+ {
+ "id": f"valve_{number}",
+ "name": f"Клапан {number}",
+ "zone": str(zone),
+ "mode": "auto",
+ "position": 0,
+ "targetTemp": 28,
+ "isOpen": False,
+ "openDegrees": 0,
+ "openDegreesMax": DEFAULT_OPEN_DEGREES_MAX,
+ }
+ )
+ return valves
+
+
+SENSORS = make_default_sensors()
+VALVES = make_default_valves()
+SERIAL_STATE = {"connected": False, "transport": "rtu", "port": None, "unitId": 3}
+
+state_lock = threading.Lock()
+
+
+def clamp(value, min_value, max_value):
+ return max(min_value, min(max_value, value))
+
+
+def open_degrees_max(valve):
+ return max(1, int(valve.get("openDegreesMax", DEFAULT_OPEN_DEGREES_MAX)))
+
+
+def position_to_degrees(position, valve):
+ max_degrees = open_degrees_max(valve)
+ return clamp(round(position * max_degrees / 100), 0, max_degrees)
+
+
+def apply_open_counter(valve):
+ valve["openDegrees"] = position_to_degrees(valve.get("position", 0), valve)
+
+
+def sensor_by_zone(zone):
+ for sensor in SENSORS:
+ if str(sensor["zone"]) == str(zone):
+ return sensor
+ return None
+
+
+def valve_by_id(valve_id):
+ for valve in VALVES:
+ if valve["id"] == valve_id:
+ return valve
+ return None
+
+
+def calibrate_valve(valve):
+ valve["position"] = 0
+ valve["openDegrees"] = 0
+ valve["isOpen"] = False
+
+
+def calibrate_all_valves():
+ for valve in VALVES:
+ calibrate_valve(valve)
+
+
+def update_physics_loop():
+ while True:
+ with state_lock:
+ for sensor in SENSORS:
+ valve = valve_by_id(f"valve_{sensor['zone']}")
+ if not valve:
+ continue
+ if valve["mode"] == "manual":
+ target = 20 + clamp(valve["position"], 0, 100) * 0.7
+ else:
+ target = float(valve.get("targetTemp", sensor["setpoint"]))
+
+ drift = (target - sensor["value"]) * 0.07
+ noise = (random.random() - 0.5) * 0.2
+ sensor["value"] = clamp(sensor["value"] + drift + noise, -40, 150)
+ sensor["value"] = round(sensor["value"], 2)
+
+ for valve in VALVES:
+ apply_open_counter(valve)
+ valve["isOpen"] = valve.get("position", 0) > 0
+ time.sleep(2.0)
+
+
+class Handler(SimpleHTTPRequestHandler):
+
+ def end_headers(self):
+ self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ self.send_header("Pragma", "no-cache")
+ self.send_header("Expires", "0")
+ super().end_headers()
+ def _send_json(self, payload, code=200):
+ body = json.dumps(payload).encode("utf-8")
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_OPTIONS(self):
+ self.send_response(204)
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.end_headers()
+
+ def do_POST(self):
+ parsed = urlparse(self.path)
+ parts = [seg for seg in parsed.path.split("/") if seg]
+
+ if parsed.path in ("/api/serial/connect", "/api/connect"):
+ length = int(self.headers.get("Content-Length", "0") or "0")
+ body = self.rfile.read(length)
+ try:
+ payload = json.loads(body.decode("utf-8", errors="ignore")) if body else {}
+ except Exception:
+ payload = {}
+ transport = str(payload.get("transport", payload.get("mode", "rtu"))).lower()
+ try:
+ unit_id = int(payload.get("unitId", payload.get("slaveId", payload.get("slave", SERIAL_STATE.get("unitId", 3)))))
+ except Exception:
+ unit_id = 3
+ unit_id = max(1, min(247, unit_id))
+ if transport in ("tcp", "modbus_tcp", "modbus-tcp"):
+ host = str(payload.get("host") or payload.get("ip") or "127.0.0.1").strip()
+ tcp_port = int(payload.get("tcpPort", payload.get("tcp_port", payload.get("port", 502))))
+ SERIAL_STATE.update({
+ "connected": True,
+ "transport": "tcp",
+ "unitId": unit_id,
+ "host": host,
+ "tcpPort": tcp_port,
+ "address": f"{host}:{tcp_port}",
+ "port": f"{host}:{tcp_port}",
+ })
+ else:
+ port = str(payload.get("port") or "COM1").strip()
+ SERIAL_STATE.update({"connected": True, "transport": "rtu", "port": port, "unitId": unit_id})
+ self._send_json({"ok": True, **SERIAL_STATE})
+ return
+
+ if parsed.path in ("/api/serial/disconnect", "/api/disconnect"):
+ transport = SERIAL_STATE.get("transport", "rtu")
+ SERIAL_STATE.update({"connected": False, "transport": transport, "port": None})
+ self._send_json({"ok": True, **SERIAL_STATE})
+ return
+
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "valves" and parts[2] == "calibrate-all":
+ with state_lock:
+ calibrate_all_valves()
+ self._send_json({"ok": True, "valves": [dict(item) for item in VALVES], "message": "calibration started"})
+ return
+
+ if len(parts) == 4 and parts[0] == "api" and parts[1] == "valves" and parts[3] == "calibrate":
+ valve_id = parts[2]
+ with state_lock:
+ valve = valve_by_id(valve_id)
+ if not valve:
+ self.send_response(404)
+ self.end_headers()
+ return
+ calibrate_valve(valve)
+ self._send_json({"ok": True, "valve": dict(valve), "message": "calibration started"})
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+ def do_GET(self):
+ parsed = urlparse(self.path)
+ if parsed.path in ("/api/serial/status", "/api/state"):
+ self._send_json({"ok": True, **SERIAL_STATE})
+ return
+ if parsed.path in ("/api/serial/ports", "/api/ports"):
+ self._send_json({"ok": True, "ports": [], "selected": SERIAL_STATE.get("port")})
+ return
+ if parsed.path == "/api/sensors":
+ self._send_json(self._snapshot_sensors())
+ return
+ if parsed.path == "/api/valves":
+ self._send_json(self._snapshot_valves())
+ return
+
+ # Serve static files (index.html, app.js, styles.css)
+ file = Path(parsed.path.lstrip("/"))
+ if parsed.path == "/":
+ file = Path("index.html")
+ if file.exists():
+ return super().do_GET()
+
+ self.send_response(404)
+ self.send_header("Content-Type", "text/plain; charset=utf-8")
+ self.end_headers()
+ self.wfile.write(b"not found")
+
+ def do_PUT(self):
+ parsed = urlparse(self.path)
+ segments = [seg for seg in parsed.path.split("/") if seg]
+ if len(segments) == 2 and segments[0] == "api":
+ self.send_response(400)
+ self.end_headers()
+ return
+ if len(segments) != 3 or segments[0] != "api":
+ self.send_response(404)
+ self.end_headers()
+ return
+
+ _, resource, resource_id = segments
+ length = int(self.headers.get("Content-Length", "0"))
+ body = self.rfile.read(length)
+ try:
+ payload = json.loads(body.decode("utf-8")) if body else {}
+ except Exception:
+ payload = {}
+
+ with state_lock:
+ if resource == "sensors":
+ sensor = next((s for s in SENSORS if s["id"] == resource_id), None)
+ if not sensor:
+ self.send_response(404)
+ self.end_headers()
+ return
+ if "setpoint" in payload:
+ sensor["setpoint"] = float(payload["setpoint"])
+ self._send_json(sensor)
+ return
+ if resource == "valves":
+ valve = valve_by_id(resource_id)
+ if not valve:
+ self.send_response(404)
+ self.end_headers()
+ return
+ max_degrees = open_degrees_max(valve)
+ if "openDegreesMax" in payload:
+ valve["openDegreesMax"] = max(1, int(payload["openDegreesMax"]))
+ max_degrees = open_degrees_max(valve)
+ valve["openDegrees"] = position_to_degrees(valve["position"], valve)
+ if "openDegrees" in payload:
+ valve["openDegrees"] = clamp(int(payload["openDegrees"]), 0, open_degrees_max(valve))
+ valve["position"] = clamp(round(valve["openDegrees"] * 100 / max_degrees), 0, 100)
+ elif "position" in payload:
+ valve["position"] = clamp(int(payload["position"]), 0, 100)
+ valve["openDegrees"] = position_to_degrees(valve["position"], valve)
+ if "mode" in payload:
+ valve["mode"] = payload["mode"]
+ if "targetTemp" in payload:
+ valve["targetTemp"] = float(payload["targetTemp"])
+ valve["isOpen"] = valve["position"] > 0
+ self._send_json(valve)
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+ def _snapshot_sensors(self):
+ with state_lock:
+ return [dict(item) for item in SENSORS]
+
+ def _snapshot_valves(self):
+ with state_lock:
+ return [dict(item) for item in VALVES]
+
+
+def main():
+ calibrate_all_valves()
+ threading.Thread(target=update_physics_loop, daemon=True).start()
+ server = HTTPServer(("0.0.0.0", 8080), Handler)
+ print("Server started: http://127.0.0.1:8080")
+ server.serve_forever()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/john103C6T6NewVer/serial_bridge.py b/john103C6T6NewVer/serial_bridge.py
new file mode 100644
index 0000000..ca625ba
--- /dev/null
+++ b/john103C6T6NewVer/serial_bridge.py
@@ -0,0 +1,1136 @@
+import argparse
+import json
+import os
+import re
+import socket
+import threading
+import time
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+from pathlib import Path
+from typing import Optional
+from urllib.parse import urlparse
+
+import serial
+from serial.tools import list_ports
+
+
+SENSOR_COUNT = 16
+VALVE_COUNT = 32
+DEFAULT_OPEN_DEGREES_MAX = 90
+MODBUS_UNIT_ID = 3
+MODBUS_TEMP_INPUT_BASE = 0
+MODBUS_DS18B20_ID_INPUT_BASE = 1000
+MODBUS_ROOM_INPUT_BASE = 400
+MODBUS_ROOM_INPUT_REGS = 18
+MODBUS_SETPOINT_HOLDING_BASE = 0
+MODBUS_TEMPERATURE_SCALE = 10.0
+MODBUS_ROOM_HOLDING_BASE = 300
+MODBUS_ROOM_HOLDING_REGS = 8
+MODBUS_ROOM_HOLDING_SETPOINT_X10 = 0
+MODBUS_ROOM_HOLDING_HYST_X10 = 1
+MODBUS_ROOM_HOLDING_POSITION_PCT = 2
+MODBUS_ROOM_HOLDING_ANGLE_MAX = 3
+MODBUS_ROOM_HOLDING_MODE = 4
+MODBUS_ROOM_HOLDING_COMMAND = 5
+MODBUS_ROOM_HOLDING_LOCATION = 6
+MODBUS_ROOM_HOLDING_APPLY = 7
+MODBUS_SENSOR_STATUS_COIL_BASE = 128
+MODBUS_VALVE_OPEN_COIL_BASE = 256
+MODBUS_VALVE_CLOSE_COIL_BASE = 288
+MODBUS_APPLY_PARAMS_COIL = 384
+MODBUS_POLL_INTERVAL = 1.0
+ROOM_MODE_AUTO = 0
+ROOM_MODE_MANUAL = 1
+ROOM_COMMAND_STOP = 0
+ROOM_COMMAND_OPEN = 1
+ROOM_COMMAND_CLOSE = 2
+
+
+def modbus_crc16(data: bytes) -> int:
+ crc = 0xFFFF
+ for byte in data:
+ crc ^= byte
+ for _ in range(8):
+ if crc & 1:
+ crc = (crc >> 1) ^ 0xA001
+ else:
+ crc >>= 1
+ return crc & 0xFFFF
+
+
+def to_signed16(value: int) -> int:
+ value = int(value) & 0xFFFF
+ return value - 0x10000 if value & 0x8000 else value
+
+
+def numeric_suffix(value: object, fallback: int = 1) -> int:
+ match = re.search(r"\d+", str(value or ""))
+ return int(match.group(0)) if match else fallback
+
+
+def ds18b20_id_from_registers(registers: list[int]) -> str:
+ if len(registers) < 4:
+ return ""
+ data = bytearray()
+ for value in registers[:4]:
+ data.extend((int(value) & 0xFFFF).to_bytes(2, "little"))
+ if not any(data):
+ return ""
+ return "-".join(f"{byte:02X}" for byte in data)
+
+
+def make_default_sensors():
+ return [
+ {
+ "id": f"zone_{number}",
+ "name": f"Датчик {number}",
+ "value": 0.0,
+ "setpoint": 28.0,
+ "unit": "°C",
+ "zone": str(number),
+ "ds18b20Id": f"28-00-00-00-00-00-00-{number:02X}",
+ }
+ for number in range(1, SENSOR_COUNT + 1)
+ ]
+
+
+def make_default_valves():
+ valves = []
+ for index in range(VALVE_COUNT):
+ number = index + 1
+ zone = (index % SENSOR_COUNT) + 1
+ valves.append(
+ {
+ "id": f"valve_{number}",
+ "name": f"Клапан {number}",
+ "zone": str(zone),
+ "mode": "auto",
+ "position": 0,
+ "targetTemp": 28,
+ "isOpen": False,
+ "openDegrees": 0,
+ "openDegreesMax": DEFAULT_OPEN_DEGREES_MAX,
+ }
+ )
+ return valves
+
+
+DEFAULT_SENSORS = make_default_sensors()
+DEFAULT_VALVES = make_default_valves()
+
+
+def clamp(value, min_value, max_value):
+ try:
+ v = float(value)
+ except Exception:
+ return min_value
+ if v < min_value:
+ return min_value
+ if v > max_value:
+ return max_value
+ return v
+
+
+def discover_serial_ports() -> list[dict[str, str]]:
+ ports: dict[str, dict[str, str]] = {}
+ try:
+ available_ports = list_ports.comports(include_links=True)
+ except TypeError:
+ available_ports = list_ports.comports()
+ except Exception:
+ available_ports = []
+
+ for item in available_ports:
+ add_serial_port(ports, item.device, item.description, item.hwid)
+
+ if os.name == "nt":
+ try:
+ import winreg
+
+ with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DEVICEMAP\SERIALCOMM") as key:
+ index = 0
+ while True:
+ try:
+ value_name, device, _ = winreg.EnumValue(key, index)
+ except OSError:
+ break
+ if str(device).upper() not in ports:
+ add_serial_port(ports, device, "Windows serial port", f"registry:{value_name}")
+ index += 1
+ except (ImportError, OSError):
+ pass
+
+ return sorted(ports.values(), key=port_sort_key)
+
+
+class BridgeState:
+ def __init__(self):
+ self.sensors = [dict(item) for item in DEFAULT_SENSORS]
+ self.valves = [dict(item) for item in DEFAULT_VALVES]
+
+
+state = BridgeState()
+PREFERRED_PORT_TOKENS = (
+ "st-link",
+ "stm",
+ "stmicro",
+ "usb",
+ "vcp",
+ "virtual com",
+ "usb serial",
+)
+
+
+def add_serial_port(ports: dict[str, dict[str, str]], device: object, description: object = "", hwid: object = "") -> None:
+ device_text = str(device).strip()
+ if not device_text:
+ return
+ key = device_text.upper()
+ ports[key] = {
+ "device": device_text,
+ "description": str(description or "").strip(),
+ "hwid": str(hwid or "").strip(),
+ }
+
+
+def port_text(port: dict[str, str]) -> str:
+ return " ".join([port.get("device", ""), port.get("description", ""), port.get("hwid", "")]).lower()
+
+
+def port_com_number(port: dict[str, str]) -> int:
+ device = port.get("device", "").upper()
+ if device.startswith("COM") and device[3:].isdigit():
+ return int(device[3:])
+ return 0
+
+
+def port_sort_key(port: dict[str, str]) -> tuple[int, int, str]:
+ text = port_text(port)
+ rank = 0 if any(token in text for token in PREFERRED_PORT_TOKENS) else 1
+ com_num = port_com_number(port)
+ if 1 <= com_num <= 4:
+ rank = 2
+ return (rank, com_num, port.get("device", "").upper())
+
+
+def preferred_serial_port(ports: list[dict[str, str]]) -> str | None:
+ if not ports:
+ return None
+ return sorted(ports, key=port_sort_key)[0]["device"]
+
+
+state_lock = threading.Lock()
+
+
+def valve_open_degrees_max(valve: dict) -> int:
+ return int(clamp(valve.get("openDegreesMax", DEFAULT_OPEN_DEGREES_MAX), 1, 10000))
+
+
+def position_from_degrees(degrees: int, max_degrees: int) -> int:
+ if max_degrees <= 0:
+ return 0
+ return int(round(clamp(degrees, 0, max_degrees) * 100 / max_degrees))
+
+
+def degrees_from_position(position: int, max_degrees: int) -> int:
+ return int(round(clamp(position, 0, 100) * max_degrees / 100))
+
+
+class SerialBridge:
+ def __init__(self, port: str, baudrate: int, parity: str, stopbits: float, bytesize: int, timeout: float):
+ self.port = port
+ self.baudrate = baudrate
+ self.parity = parity
+ self.stopbits = stopbits
+ self.bytesize = bytesize
+ self.timeout = timeout
+ self.transport = "rtu"
+ self.tcp_host = ""
+ self.tcp_port = 502
+ self.tcp_socket = None
+ self.tcp_file = None
+ self.unit_id = MODBUS_UNIT_ID
+ self.transaction_id = 0
+ self.request_lock = threading.Lock()
+ self.serial = None
+ self.running = False
+ self.thread = None
+
+ def configure_rtu(self, port: str, baudrate: int, parity: str, stopbits: float, bytesize: int, timeout: float):
+ self.transport = "rtu"
+ self.port = port
+ self.baudrate = baudrate
+ self.parity = parity
+ self.stopbits = stopbits
+ self.bytesize = bytesize
+ self.timeout = timeout
+
+ def configure_tcp(self, host: str, tcp_port: int = 502, timeout: float = 0.8):
+ self.transport = "tcp"
+ self.tcp_host = host
+ self.tcp_port = int(tcp_port)
+ self.timeout = timeout
+ self.port = f"{self.tcp_host}:{self.tcp_port}"
+
+ def connect(self):
+ if self.running and self.thread and self.thread.is_alive():
+ self.disconnect()
+ if self.transport == "tcp":
+ self.tcp_socket = socket.create_connection((self.tcp_host, self.tcp_port), timeout=self.timeout)
+ self.tcp_socket.settimeout(self.timeout)
+ self.tcp_file = self.tcp_socket.makefile("rb")
+ else:
+ self.serial = serial.Serial(
+ self.port,
+ self.baudrate,
+ parity=self.parity,
+ stopbits=self.stopbits,
+ bytesize=self.bytesize,
+ timeout=self.timeout,
+ )
+ self.running = True
+ self.thread = threading.Thread(target=self.modbus_poll_loop, daemon=True)
+ self.thread.start()
+
+ def disconnect(self):
+ self.running = False
+ if self.thread and self.thread.is_alive():
+ try:
+ self.thread.join(timeout=0.5)
+ except Exception:
+ pass
+ if self.serial and self.serial.is_open:
+ self.serial.close()
+ if self.tcp_file:
+ try:
+ self.tcp_file.close()
+ except Exception:
+ pass
+ self.tcp_file = None
+ if self.tcp_socket:
+ try:
+ self.tcp_socket.close()
+ except Exception:
+ pass
+ self.tcp_socket = None
+
+ def is_connected(self):
+ if self.transport == "tcp":
+ return self.tcp_socket is not None
+ return bool(self.serial and self.serial.is_open)
+
+ def status(self):
+ connected = self.is_connected()
+ if self.transport == "tcp":
+ return {
+ "ok": True,
+ "connected": connected,
+ "transport": "tcp",
+ "unitId": self.unit_id,
+ "host": self.tcp_host,
+ "tcpPort": self.tcp_port,
+ "address": f"{self.tcp_host}:{self.tcp_port}" if self.tcp_host else "",
+ "port": f"{self.tcp_host}:{self.tcp_port}" if connected and self.tcp_host else None,
+ }
+ return {
+ "ok": True,
+ "connected": connected,
+ "transport": "rtu",
+ "unitId": self.unit_id,
+ "port": self.port if connected else None,
+ "baudrate": self.baudrate,
+ }
+
+ def send(self, payload: dict):
+ if not self.is_connected():
+ return
+ typ = str(payload.get("type", "")).lower()
+ if typ == "sensor":
+ self.write_sensor_payload(payload)
+ return
+ if typ == "valve":
+ self.write_valve_payload(payload)
+ return
+
+ def _recv_exact_tcp(self, size: int) -> bytes:
+ data = bytearray()
+ while len(data) < size:
+ chunk = self.tcp_socket.recv(size - len(data))
+ if not chunk:
+ raise ConnectionError("Modbus TCP connection closed")
+ data.extend(chunk)
+ return bytes(data)
+
+ def _read_exact_serial(self, size: int) -> bytes:
+ data = bytearray()
+ deadline = time.monotonic() + max(float(self.timeout or 0.3), 0.3)
+ while len(data) < size and time.monotonic() < deadline:
+ chunk = self.serial.read(size - len(data))
+ if chunk:
+ data.extend(chunk)
+ else:
+ time.sleep(0.005)
+ if len(data) != size:
+ raise TimeoutError("Modbus RTU response timeout")
+ return bytes(data)
+
+ def modbus_request(self, function: int, payload: bytes) -> bytes:
+ pdu = bytes([function]) + payload
+ with self.request_lock:
+ if self.transport == "tcp":
+ self.transaction_id = (self.transaction_id + 1) & 0xFFFF
+ mbap = (
+ self.transaction_id.to_bytes(2, "big")
+ + b"\x00\x00"
+ + (len(pdu) + 1).to_bytes(2, "big")
+ + bytes([self.unit_id])
+ )
+ self.tcp_socket.sendall(mbap + pdu)
+ header = self._recv_exact_tcp(7)
+ length = int.from_bytes(header[4:6], "big")
+ response_pdu = self._recv_exact_tcp(max(0, length - 1))
+ else:
+ frame = bytes([self.unit_id]) + pdu
+ crc = modbus_crc16(frame)
+ request = frame + crc.to_bytes(2, "little")
+ try:
+ self.serial.reset_input_buffer()
+ except Exception:
+ pass
+ self.serial.write(request)
+ header = self._read_exact_serial(3)
+ if header[0] != self.unit_id:
+ raise ValueError("Unexpected Modbus RTU unit id")
+ if header[1] & 0x80:
+ tail = self._read_exact_serial(2)
+ frame = header + tail
+ if modbus_crc16(frame[:-2]) != int.from_bytes(frame[-2:], "little"):
+ raise ValueError("Bad Modbus RTU CRC")
+ raise RuntimeError(f"Modbus exception {header[2]}")
+ if header[1] in (0x01, 0x02, 0x03, 0x04):
+ tail = self._read_exact_serial(header[2] + 2)
+ else:
+ tail = self._read_exact_serial(5)
+ frame = header + tail
+ if modbus_crc16(frame[:-2]) != int.from_bytes(frame[-2:], "little"):
+ raise ValueError("Bad Modbus RTU CRC")
+ response_pdu = frame[1:-2]
+
+ if not response_pdu:
+ raise TimeoutError("Empty Modbus response")
+ if response_pdu[0] & 0x80:
+ code = response_pdu[1] if len(response_pdu) > 1 else 0
+ raise RuntimeError(f"Modbus exception {code}")
+ if response_pdu[0] != function:
+ raise ValueError("Unexpected Modbus function")
+ return response_pdu
+
+ def read_registers(self, function: int, address: int, count: int) -> list[int]:
+ response = self.modbus_request(function, address.to_bytes(2, "big") + count.to_bytes(2, "big"))
+ byte_count = response[1]
+ data = response[2:2 + byte_count]
+ return [int.from_bytes(data[index:index + 2], "big") for index in range(0, len(data), 2)]
+
+ def read_input_registers(self, address: int, count: int) -> list[int]:
+ return self.read_registers(0x04, address, count)
+
+ def read_holding_registers(self, address: int, count: int) -> list[int]:
+ return self.read_registers(0x03, address, count)
+
+ def read_coils(self, address: int, count: int) -> list[bool]:
+ response = self.modbus_request(0x01, address.to_bytes(2, "big") + count.to_bytes(2, "big"))
+ data = response[2:2 + response[1]]
+ bits: list[bool] = []
+ for byte in data:
+ for bit in range(8):
+ bits.append(bool(byte & (1 << bit)))
+ if len(bits) >= count:
+ return bits
+ return bits
+
+ def write_register(self, address: int, value: int):
+ self.modbus_request(0x06, address.to_bytes(2, "big") + (int(value) & 0xFFFF).to_bytes(2, "big"))
+
+ def write_coil(self, address: int, value: bool):
+ raw_value = 0xFF00 if value else 0x0000
+ self.modbus_request(0x05, address.to_bytes(2, "big") + raw_value.to_bytes(2, "big"))
+
+ def write_sensor_payload(self, payload: dict):
+ sensor_number = numeric_suffix(payload.get("id"), 1)
+ channel = max(0, min(SENSOR_COUNT - 1, sensor_number - 1))
+ if "setpoint" in payload:
+ setpoint_x10 = round(float(payload["setpoint"]) * MODBUS_TEMPERATURE_SCALE)
+ self.write_register(MODBUS_SETPOINT_HOLDING_BASE + channel, setpoint_x10)
+ self.write_register(MODBUS_ROOM_HOLDING_BASE + channel * MODBUS_ROOM_HOLDING_REGS + MODBUS_ROOM_HOLDING_SETPOINT_X10, setpoint_x10)
+ self.write_register(MODBUS_ROOM_HOLDING_BASE + channel * MODBUS_ROOM_HOLDING_REGS + MODBUS_ROOM_HOLDING_APPLY, 1)
+ self.write_coil(MODBUS_APPLY_PARAMS_COIL, True)
+
+ def write_valve_payload(self, payload: dict):
+ valve_number = numeric_suffix(payload.get("id"), 1)
+ channel = max(0, min(SENSOR_COUNT - 1, (valve_number - 1) // 2))
+ room_base = MODBUS_ROOM_HOLDING_BASE + channel * MODBUS_ROOM_HOLDING_REGS
+ if "targetTemp" in payload:
+ setpoint_x10 = round(float(payload["targetTemp"]) * MODBUS_TEMPERATURE_SCALE)
+ self.write_register(MODBUS_SETPOINT_HOLDING_BASE + channel, setpoint_x10)
+ self.write_register(room_base + MODBUS_ROOM_HOLDING_SETPOINT_X10, setpoint_x10)
+ self.write_register(room_base + MODBUS_ROOM_HOLDING_APPLY, 1)
+ self.write_coil(MODBUS_APPLY_PARAMS_COIL, True)
+ if "mode" in payload:
+ mode = ROOM_MODE_MANUAL if str(payload.get("mode")).lower() == "manual" else ROOM_MODE_AUTO
+ self.write_register(room_base + MODBUS_ROOM_HOLDING_MODE, mode)
+ if "position" not in payload and "isOpen" not in payload:
+ return
+ if "position" in payload:
+ position_pct = int(clamp(payload.get("position") or 0, 0, 100))
+ should_open = position_pct > 0
+ else:
+ should_open = bool(payload.get("isOpen"))
+ position_pct = 100 if should_open else 0
+ self.write_register(room_base + MODBUS_ROOM_HOLDING_POSITION_PCT, position_pct)
+ self.write_register(room_base + MODBUS_ROOM_HOLDING_COMMAND, ROOM_COMMAND_OPEN if should_open else ROOM_COMMAND_CLOSE)
+ self.write_coil(MODBUS_VALVE_OPEN_COIL_BASE + channel, should_open)
+ self.write_coil(MODBUS_VALVE_CLOSE_COIL_BASE + channel, not should_open)
+
+ def modbus_poll_loop(self):
+ while self.running:
+ try:
+ self.poll_modbus_state()
+ except Exception:
+ time.sleep(0.2)
+ time.sleep(MODBUS_POLL_INTERVAL)
+
+ def apply_room_registers(self, room_regs: list[int]):
+ with state_lock:
+ for index in range(SENSOR_COUNT):
+ start = index * MODBUS_ROOM_INPUT_REGS
+ room = room_regs[start:start + MODBUS_ROOM_INPUT_REGS]
+ if len(room) < MODBUS_ROOM_INPUT_REGS:
+ continue
+
+ sensor = state.sensors[index]
+ sensor["value"] = round(to_signed16(room[6]) / MODBUS_TEMPERATURE_SCALE, 1)
+ sensor["setpoint"] = round(to_signed16(room[7]) / MODBUS_TEMPERATURE_SCALE, 1)
+ sensor["ds18b20Id"] = ds18b20_id_from_registers(room[2:6])
+ sensor["connected"] = bool(room[12])
+ sensor["locationCode"] = int(room[1])
+
+ position = int(clamp(room[9], 0, 100))
+ open_degrees_max = int(room[11] or DEFAULT_OPEN_DEGREES_MAX)
+ open_degrees = int(clamp(room[10], 0, open_degrees_max))
+ mode = "manual" if int(room[15]) == ROOM_MODE_MANUAL else "auto"
+ open_state = bool(room[13])
+ close_state = bool(room[14])
+
+ open_index = index * 2
+ close_index = open_index + 1
+ if open_index < len(state.valves):
+ valve = state.valves[open_index]
+ valve["targetTemp"] = sensor["setpoint"]
+ valve["mode"] = mode
+ valve["position"] = position
+ valve["openDegrees"] = open_degrees
+ valve["openDegreesMax"] = open_degrees_max
+ valve["isOpen"] = open_state or position > 0
+ valve["commandState"] = int(room[16])
+
+ if close_index < len(state.valves):
+ valve = state.valves[close_index]
+ valve["targetTemp"] = sensor["setpoint"]
+ valve["mode"] = mode
+ valve["position"] = 100 if close_state else 0
+ valve["openDegrees"] = degrees_from_position(valve["position"], open_degrees_max)
+ valve["openDegreesMax"] = open_degrees_max
+ valve["isOpen"] = close_state
+ valve["commandState"] = int(room[16])
+
+ def poll_modbus_state(self):
+ if not self.is_connected():
+ return
+ try:
+ room_regs = self.read_input_registers(MODBUS_ROOM_INPUT_BASE, SENSOR_COUNT * MODBUS_ROOM_INPUT_REGS)
+ self.apply_room_registers(room_regs)
+ return
+ except Exception:
+ pass
+ temperatures = self.read_input_registers(MODBUS_TEMP_INPUT_BASE, SENSOR_COUNT)
+ sensor_ids = self.read_input_registers(MODBUS_DS18B20_ID_INPUT_BASE, SENSOR_COUNT * 4)
+ setpoints = self.read_holding_registers(MODBUS_SETPOINT_HOLDING_BASE, SENSOR_COUNT)
+ try:
+ sensor_connected = self.read_coils(MODBUS_SENSOR_STATUS_COIL_BASE, SENSOR_COUNT)
+ except Exception:
+ sensor_connected = [True] * SENSOR_COUNT
+ try:
+ valve_open = self.read_coils(MODBUS_VALVE_OPEN_COIL_BASE, SENSOR_COUNT)
+ except Exception:
+ valve_open = [False] * SENSOR_COUNT
+ try:
+ valve_close = self.read_coils(MODBUS_VALVE_CLOSE_COIL_BASE, SENSOR_COUNT)
+ except Exception:
+ valve_close = [False] * SENSOR_COUNT
+
+ with state_lock:
+ for index, sensor in enumerate(state.sensors[:SENSOR_COUNT]):
+ if index < len(temperatures):
+ sensor["value"] = round(to_signed16(temperatures[index]) / 10.0, 1)
+ id_start = index * 4
+ ds18b20_id = ds18b20_id_from_registers(sensor_ids[id_start:id_start + 4])
+ if ds18b20_id:
+ sensor["ds18b20Id"] = ds18b20_id
+ if index < len(setpoints):
+ sensor["setpoint"] = round(to_signed16(setpoints[index]) / MODBUS_TEMPERATURE_SCALE, 1)
+ if index < len(sensor_connected):
+ sensor["connected"] = bool(sensor_connected[index])
+
+ for index in range(SENSOR_COUNT):
+ open_index = index * 2
+ close_index = open_index + 1
+ open_state = bool(valve_open[index]) if index < len(valve_open) else False
+ close_state = bool(valve_close[index]) if index < len(valve_close) else False
+ position = 100 if open_state else 0 if close_state else int(state.valves[open_index].get("position", 0))
+
+ if open_index < len(state.valves):
+ valve = state.valves[open_index]
+ valve["isOpen"] = open_state
+ valve["position"] = position
+ valve["openDegrees"] = degrees_from_position(position, valve_open_degrees_max(valve))
+ if index < len(setpoints):
+ valve["targetTemp"] = round(to_signed16(setpoints[index]) / MODBUS_TEMPERATURE_SCALE, 1)
+
+ if close_index < len(state.valves):
+ valve = state.valves[close_index]
+ valve["isOpen"] = close_state
+ valve["position"] = 100 if close_state else 0
+ valve["openDegrees"] = degrees_from_position(valve["position"], valve_open_degrees_max(valve))
+ if index < len(setpoints):
+ valve["targetTemp"] = round(to_signed16(setpoints[index]) / MODBUS_TEMPERATURE_SCALE, 1)
+
+ def read_loop(self):
+ while self.running:
+ try:
+ raw = self.tcp_file.readline() if self.transport == "tcp" else self.serial.readline()
+ if not raw:
+ continue
+ try:
+ line = raw.decode("utf-8", errors="ignore").strip()
+ except Exception:
+ line = ""
+ if not line:
+ continue
+ updated = parse_input_line(line)
+ if not updated:
+ continue
+ with state_lock:
+ apply_updates(updated)
+ except Exception:
+ time.sleep(0.05)
+
+
+def zone_to_id(zone: Optional[str], kind: str):
+ if zone is None:
+ return None
+ idx = str(zone).strip().replace("zone_", "")
+ if kind == "sensor":
+ return f"zone_{idx}"
+ return f"valve_{idx}"
+
+
+def parse_input_line(line: str):
+ # JSON:
+ # {"sensors":[...], "valves":[...]} or {"s1": {"value": 24.6}, ...}
+ try:
+ payload = json.loads(line)
+ except Exception:
+ payload = None
+
+ if isinstance(payload, dict):
+ if isinstance(payload.get("sensors"), list) or isinstance(payload.get("valves"), list):
+ return {
+ "sensors": payload.get("sensors") if isinstance(payload.get("sensors"), list) else None,
+ "valves": payload.get("valves") if isinstance(payload.get("valves"), list) else None,
+ }
+ if any(isinstance(k, str) and (k.startswith("T") or k.startswith("S")) for k in payload.keys()):
+ return {"kv": payload}
+
+ if isinstance(payload, list):
+ # allow list with {"type":"sensor",...}
+ return {"items": payload}
+
+ # key=value format:
+ # T1=24.7;T2=25.1;V1_MODE=auto;V1_POS=45;...
+ pairs = [p.strip() for p in line.split(";") if "=" in p]
+ updates = {"kv": {}}
+ for pair in pairs:
+ key, value = pair.split("=", 1)
+ updates["kv"][key.strip().upper()] = value.strip()
+
+ if not updates["kv"]:
+ return None
+ return updates
+
+
+def apply_updates(payload):
+ if not payload:
+ return
+
+ if "sensors" in payload and isinstance(payload["sensors"], list):
+ for item in payload["sensors"]:
+ sid = str(item.get("id") or item.get("sensorId") or item.get("code") or "").strip()
+ sensor = next((s for s in state.sensors if s["id"] == sid), None)
+ if not sensor:
+ continue
+ if "value" in item:
+ sensor["value"] = clamp(item["value"], -1000, 1000)
+ if "setpoint" in item:
+ sensor["setpoint"] = clamp(item["setpoint"], -1000, 1000)
+ if "unit" in item:
+ sensor["unit"] = item["unit"]
+
+ if "valves" in payload and isinstance(payload["valves"], list):
+ for item in payload["valves"]:
+ vid = str(item.get("id") or item.get("valveId") or item.get("code") or "").strip()
+ valve = next((v for v in state.valves if v["id"] == vid), None)
+ if not valve:
+ continue
+ if "mode" in item:
+ mode = str(item["mode"]).lower()
+ valve["mode"] = "manual" if mode == "manual" else "auto"
+ if "position" in item:
+ valve["position"] = int(clamp(item["position"], 0, 100))
+ valve["openDegrees"] = degrees_from_position(valve["position"], valve_open_degrees_max(valve))
+ if "openDegreesMax" in item:
+ valve["openDegreesMax"] = int(clamp(item["openDegreesMax"], 1, 10000))
+ if "openDegrees" in item:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["openDegrees"] = int(clamp(item["openDegrees"], 0, max_degrees))
+ valve["position"] = position_from_degrees(valve["openDegrees"], max_degrees)
+ if "targetTemp" in item:
+ valve["targetTemp"] = clamp(item["targetTemp"], -1000, 1000)
+ if "isOpen" in item:
+ valve["isOpen"] = bool(item["isOpen"])
+
+ if "items" in payload and isinstance(payload["items"], list):
+ for item in payload["items"]:
+ if not isinstance(item, dict):
+ continue
+ typ = str(item.get("type", "")).lower()
+ if typ == "sensor":
+ sid = str(item.get("id") or item.get("sensorId") or item.get("zone") or "")
+ sensor = next((s for s in state.sensors if s["id"] == sid), None)
+ if sensor:
+ if "value" in item:
+ sensor["value"] = clamp(item["value"], -1000, 1000)
+ elif typ == "valve":
+ vid = str(item.get("id") or item.get("valveId") or item.get("zone") or "")
+ valve = next((v for v in state.valves if v["id"] == vid), None)
+ if valve:
+ if "mode" in item:
+ mode = str(item["mode"]).lower()
+ valve["mode"] = "manual" if mode == "manual" else "auto"
+ if "openDegreesMax" in item:
+ valve["openDegreesMax"] = int(clamp(item["openDegreesMax"], 1, 10000))
+ if "openDegrees" in item:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["openDegrees"] = int(clamp(item["openDegrees"], 0, max_degrees))
+ valve["position"] = position_from_degrees(valve["openDegrees"], max_degrees)
+ if "position" in item and "openDegrees" not in item:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["position"] = int(clamp(item["position"], 0, 100))
+ valve["openDegrees"] = degrees_from_position(valve["position"], max_degrees)
+
+ kv = payload.get("kv", {})
+ if not kv:
+ return
+
+ sensor_map = {sensor["id"].split("_")[1]: sensor for sensor in state.sensors if "_" in sensor["id"]}
+ valve_map = {valve["id"].split("_")[1]: valve for valve in state.valves if "_" in valve["id"]}
+
+ # Temperature value keys
+ # T1=24.5 | TEMP1=24.5 | Z1_TEMP=24.5
+ for key, raw_value in kv.items():
+ value = raw_value
+ key_upper = key.upper()
+ num = re.search(r"\d+", key_upper)
+ idx = num.group(0) if num else None
+
+ if key_upper.startswith("T") or key_upper.startswith("TEMP") or "TEMP" in key_upper:
+ zone = idx
+ if zone and zone in sensor_map:
+ try:
+ sensor_map[zone]["value"] = clamp(value, -1000, 1000)
+ except Exception:
+ pass
+ continue
+
+ if key_upper.startswith("S") and idx and "SETPOINT" in key_upper:
+ zone = idx
+ if zone in sensor_map:
+ try:
+ sensor_map[zone]["setpoint"] = clamp(value, -1000, 1000)
+ except Exception:
+ pass
+ continue
+
+ # valve keys: V1_MODE=auto, V1_POS=45, V1_TGT=28
+ if key_upper.startswith("V") and idx:
+ valve = valve_map.get(idx)
+ if not valve:
+ continue
+ if "MODE" in key_upper:
+ valve["mode"] = "manual" if "MANUAL" in key_upper and str(value).lower() == "manual" else str(value).lower()
+ if "POS" in key_upper:
+ try:
+ valve["position"] = int(clamp(value, 0, 100))
+ valve["openDegrees"] = degrees_from_position(valve["position"], valve_open_degrees_max(valve))
+ except Exception:
+ pass
+ if "TGT" in key_upper:
+ try:
+ valve["targetTemp"] = clamp(value, -1000, 1000)
+ except Exception:
+ pass
+ if "MAXDEG" in key_upper:
+ max_degrees = int(clamp(value, 1, 10000))
+ valve["openDegreesMax"] = max_degrees
+ max_degrees = valve_open_degrees_max(valve)
+ try:
+ valve["openDegrees"] = int(
+ clamp(valve.get("openDegrees", degrees_from_position(valve.get("position", 0), max_degrees)), 0, max_degrees)
+ )
+ valve["position"] = position_from_degrees(valve["openDegrees"], max_degrees)
+ except Exception:
+ pass
+ elif "DEG" in key_upper:
+ try:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["openDegrees"] = int(clamp(value, 0, max_degrees))
+ valve["position"] = position_from_degrees(valve["openDegrees"], max_degrees)
+ except Exception:
+ pass
+
+ for valve in state.valves:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["openDegrees"] = int(clamp(valve.get("openDegrees", degrees_from_position(valve.get("position", 0), max_degrees)), 0, max_degrees))
+ valve["isOpen"] = int(valve.get("position", 0)) > 0
+
+
+def update_sensor(id_, patch):
+ sid = str(id_)
+ with state_lock:
+ sensor = next((item for item in state.sensors if item["id"] == sid), None)
+ if not sensor:
+ return False
+ sensor.update(patch)
+ return True
+
+
+def update_valve(id_, patch):
+ vid = str(id_)
+ with state_lock:
+ valve = next((item for item in state.valves if item["id"] == vid), None)
+ if not valve:
+ return False
+ if "openDegreesMax" in patch:
+ valve["openDegreesMax"] = int(clamp(patch["openDegreesMax"], 1, 10000))
+
+ if "mode" in patch:
+ mode = str(patch["mode"]).lower()
+ valve["mode"] = "manual" if mode == "manual" else "auto"
+ if "targetTemp" in patch:
+ valve["targetTemp"] = clamp(patch["targetTemp"], -1000, 1000)
+ if "isOpen" in patch:
+ valve["isOpen"] = bool(patch["isOpen"])
+ if "name" in patch:
+ valve["name"] = patch["name"]
+
+ if "openDegrees" in patch:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["openDegrees"] = int(clamp(patch["openDegrees"], 0, max_degrees))
+ valve["position"] = position_from_degrees(valve["openDegrees"], max_degrees)
+ if "position" in patch:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["position"] = int(clamp(patch["position"], 0, 100))
+ valve["openDegrees"] = degrees_from_position(valve["position"], max_degrees)
+ if "openDegreesMax" in patch and "openDegrees" not in patch and "position" not in patch:
+ max_degrees = valve_open_degrees_max(valve)
+ valve["openDegrees"] = int(clamp(valve.get("openDegrees", 0), 0, max_degrees))
+ valve["isOpen"] = int(valve.get("position", 0)) > 0
+ return True
+
+
+def _calibrate_valve_locked(valve: dict) -> dict:
+ valve["position"] = 0
+ valve["openDegrees"] = 0
+ valve["isOpen"] = False
+ return dict(valve)
+
+
+def calibrate_valve(channel_id: str) -> dict | None:
+ with state_lock:
+ valve = next((item for item in state.valves if item["id"] == str(channel_id)), None)
+ if not valve:
+ return None
+ updated = _calibrate_valve_locked(valve)
+
+ bridge.send({
+ "type": "valve",
+ "id": str(channel_id),
+ "command": "calibrate",
+ "position": 0,
+ "openDegrees": 0,
+ "close": True,
+ })
+ return updated
+
+
+def calibrate_all_valves() -> list[dict]:
+ with state_lock:
+ calibrated = [_calibrate_valve_locked(valve) for valve in state.valves]
+
+ for item in calibrated:
+ bridge.send({
+ "type": "valve",
+ "id": item["id"],
+ "command": "calibrate",
+ "position": 0,
+ "openDegrees": 0,
+ "close": True,
+ })
+ return [dict(item) for item in calibrated]
+
+
+def snapshot_sensors():
+ with state_lock:
+ return [dict(item) for item in state.sensors]
+
+
+def snapshot_valves():
+ with state_lock:
+ return [dict(item) for item in state.valves]
+
+
+class Handler(SimpleHTTPRequestHandler):
+
+ def end_headers(self):
+ self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+ self.send_header("Pragma", "no-cache")
+ self.send_header("Expires", "0")
+ super().end_headers()
+ def _send_json(self, payload, code=200):
+ body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+ self.send_response(code)
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ def do_OPTIONS(self):
+ self.send_response(204)
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, PUT, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ self.end_headers()
+
+ def do_GET(self):
+ parsed = urlparse(self.path)
+ if parsed.path == "/api/sensors":
+ self._send_json(snapshot_sensors())
+ return
+ if parsed.path == "/api/valves":
+ self._send_json(snapshot_valves())
+ return
+ if parsed.path in ("/api/serial/ports", "/api/ports"):
+ ports = discover_serial_ports()
+ self._send_json(
+ {"ok": True, "ports": ports, "selected": bridge.port or preferred_serial_port(ports)}
+ )
+ return
+ if parsed.path in ("/api/serial/status", "/api/state"):
+ status = bridge.status()
+ if status.get("connected") and bridge.port:
+ status["selected"] = bridge.port
+ self._send_json(status)
+ return
+ if parsed.path == "/":
+ self.path = "/index.html"
+ return super().do_GET()
+
+ def do_POST(self):
+ parsed = urlparse(self.path)
+ parts = [seg for seg in parsed.path.split("/") if seg]
+ if len(parts) == 3 and parts[0] == "api" and parts[1] == "valves" and parts[2] == "calibrate-all":
+ status = {"ok": True, "valves": calibrate_all_valves(), "message": "calibration started"}
+ self._send_json(status)
+ return
+
+ if len(parts) == 4 and parts[0] == "api" and parts[1] == "valves" and parts[3] == "calibrate":
+ valve_id = parts[2]
+ updated = calibrate_valve(valve_id)
+ if not updated:
+ self.send_response(404)
+ self.end_headers()
+ return
+ self._send_json({"ok": True, "valve": updated, "message": "calibration started"})
+ return
+
+ if parsed.path in ("/api/serial/connect", "/api/connect"):
+ length = int(self.headers.get("Content-Length", "0") or "0")
+ body = self.rfile.read(length)
+ try:
+ payload = json.loads(body.decode("utf-8", errors="ignore")) if body else {}
+ except Exception:
+ payload = {}
+ transport = str(payload.get("transport", payload.get("mode", "rtu"))).lower()
+ timeout = float(payload.get("timeout", 0.8 if transport == "tcp" else 0.3))
+ try:
+ unit_id = int(payload.get("unitId", payload.get("slaveId", payload.get("slave", bridge.unit_id))))
+ except Exception:
+ unit_id = MODBUS_UNIT_ID
+ bridge.unit_id = int(clamp(unit_id, 1, 247))
+
+ if transport in ("tcp", "modbus_tcp", "modbus-tcp"):
+ host = str(payload.get("host") or payload.get("ip") or payload.get("address") or "").strip()
+ if not host:
+ self._send_json({"ok": False, "error": "TCP host/IP is required"}, 400)
+ return
+ tcp_port = int(payload.get("tcpPort", payload.get("tcp_port", payload.get("modbusPort", payload.get("port", 502)))))
+ bridge.configure_tcp(host, tcp_port, timeout)
+ else:
+ port = str(payload.get("port", "")).strip()
+ if not port:
+ self.send_response(400)
+ self.end_headers()
+ return
+ baud = payload.get("baudrate", payload.get("baud", 115200))
+ parity = payload.get("parity", "N")
+ stopbits = payload.get("stopbits", payload.get("stopBits", 1))
+ bytesize = payload.get("bytesize", payload.get("byteSize", 8))
+ bridge.configure_rtu(port, baud, parity, stopbits, bytesize, timeout)
+ try:
+ bridge.connect()
+ except Exception as exc:
+ self._send_json({"ok": False, "error": str(exc)}, 500)
+ return
+ calibrate_all_valves()
+ status = bridge.status()
+ status["ok"] = True
+ self._send_json(status)
+ return
+
+ if parsed.path in ("/api/serial/disconnect", "/api/disconnect"):
+ transport = bridge.transport
+ bridge.disconnect()
+ self._send_json({"ok": True, "connected": False, "transport": transport})
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+ def do_PUT(self):
+ parsed = urlparse(self.path)
+ parts = [seg for seg in parsed.path.split("/") if seg]
+ if len(parts) != 3 or parts[0] != "api":
+ self.send_response(404)
+ self.end_headers()
+ return
+ _, resource, raw_id = parts
+ item_id = raw_id
+ length = int(self.headers.get("Content-Length", "0") or "0")
+ body = self.rfile.read(length)
+ try:
+ payload = json.loads(body.decode("utf-8", errors="ignore")) if body else {}
+ except Exception:
+ payload = {}
+
+ if resource == "sensors":
+ patch = {}
+ if "setpoint" in payload:
+ patch["setpoint"] = float(payload["setpoint"])
+ if not patch:
+ patch = {k: v for k, v in payload.items() if k in ("setpoint", "value", "unit", "name")}
+ if not update_sensor(item_id, patch):
+ self.send_response(404)
+ self.end_headers()
+ return
+ if bridge.is_connected():
+ try:
+ bridge.send({"type": "sensor", "id": item_id, **patch})
+ except Exception as exc:
+ self._send_json({"ok": False, "error": str(exc)}, 500)
+ return
+ self._send_json(next(item for item in state.sensors if item["id"] == item_id))
+ return
+
+ if resource == "valves":
+ patch = {}
+ if "mode" in payload:
+ mode = str(payload["mode"]).lower()
+ patch["mode"] = "manual" if mode == "manual" else "auto"
+ if "position" in payload:
+ patch["position"] = int(clamp(payload["position"], 0, 100))
+ if "targetTemp" in payload:
+ patch["targetTemp"] = float(payload["targetTemp"])
+ if not patch:
+ patch = {k: v for k, v in payload.items() if k in ("mode", "position", "targetTemp", "isOpen", "name")}
+ if not update_valve(item_id, patch):
+ self.send_response(404)
+ self.end_headers()
+ return
+ if bridge.is_connected():
+ try:
+ bridge.send({"type": "valve", "id": item_id, **patch})
+ except Exception as exc:
+ self._send_json({"ok": False, "error": str(exc)}, 500)
+ return
+ self._send_json(next(item for item in state.valves if item["id"] == item_id))
+ return
+
+ self.send_response(404)
+ self.end_headers()
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description="Serial bridge for MCU data to Web GUI")
+ parser.add_argument("--host", default="0.0.0.0")
+ parser.add_argument("--port", type=int, default=8080)
+ parser.add_argument("--serial-port", default="")
+ parser.add_argument("--baudrate", type=int, default=115200)
+ parser.add_argument("--parity", default="N")
+ parser.add_argument("--stopbits", type=float, default=1)
+ parser.add_argument("--bytesize", type=int, default=8)
+ parser.add_argument("--timeout", type=float, default=0.3)
+ return parser.parse_args()
+
+
+if __name__ == "__main__":
+ args = parse_args()
+ bridge = SerialBridge(
+ args.serial_port,
+ baudrate=args.baudrate,
+ parity=args.parity,
+ stopbits=args.stopbits,
+ bytesize=args.bytesize,
+ timeout=args.timeout,
+ )
+
+ if args.serial_port:
+ try:
+ bridge.connect()
+ print(f"COM connected: {args.serial_port} @ {args.baudrate}")
+ calibrate_all_valves()
+ except Exception as exc:
+ print(f"Не удалось открыть {args.serial_port}: {exc}. Запуск только в памяти.")
+ else:
+ print("COM порт не указан. Сервер стартует в локальном режиме без чтения порта.")
+
+ server = HTTPServer((args.host, args.port), Handler)
+ print(f"Server started: http://{args.host}:{args.port}")
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+ finally:
+ bridge.disconnect()
+ server.server_close()
diff --git a/john103C6T6NewVer/server.err.log b/john103C6T6NewVer/server.err.log
new file mode 100644
index 0000000..cc2cd3b
--- /dev/null
+++ b/john103C6T6NewVer/server.err.log
@@ -0,0 +1,705 @@
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET /styles.css?v=20260625-ds18b20-id-1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET /app.js?v=20260625-setpoint-fix-1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET /api/serial/ports HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:31] "GET /api/serial/status HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:34] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:34] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:37] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:37] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:40] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:40] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:43] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:43] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:46] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:46] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:49] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:49] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:52] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:52] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:55] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:55] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:58] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:23:58] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:01] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:01] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:04] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:04] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:04] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:04] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:04] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:04] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:06] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:06] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:06] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:07] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:07] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:07] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:07] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:07] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:08] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:08] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:09] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:09] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:09] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:10] "PUT /api/valves/valve_1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:10] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:10] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:10] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:10] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:13] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:13] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:15] Request timed out: TimeoutError('Modbus RTU response timeout')
+127.0.0.1 - - [25/Jun/2026 16:24:16] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:18] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:20] Request timed out: TimeoutError('Modbus RTU response timeout')
+127.0.0.1 - - [25/Jun/2026 16:24:21] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:21] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:21] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:21] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "POST /connect HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "POST /serial/connect HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:22] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:25] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:25] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:28] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:28] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:31] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:31] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:34] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:34] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:37] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:37] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:39] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:39] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:39] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:39] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:40] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:41] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:43] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:46] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:46] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:24:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:07] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:07] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:08] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:25:10] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:25:10] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:11] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:25:12] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:13] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:25:17] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:17] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:44] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:44] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:47] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:47] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:25:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:14] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:14] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:19] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:19] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:22] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:22] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:25] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:25] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:28] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:28] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:31] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:31] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:34] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:34] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:35] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /styles.css?v=20260625-ds18b20-id-1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /app.js?v=20260625-setpoint-draft-1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] code 404, message File not found
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /favicon.ico HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /api/serial/ports HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:36] "GET /api/serial/status HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:37] Request timed out: TimeoutError('Modbus RTU response timeout')
+127.0.0.1 - - [25/Jun/2026 16:26:37] "POST /api/calibration/all HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:37] "POST /calibration/all HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:41] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:26:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:42] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:26:44] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:26:44] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:45] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:46] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:46] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:49] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:26:49] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:49] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:49] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:51] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:26:51] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:51] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:51] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:51] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:51] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:26:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:26:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:13] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:13] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:17] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:17] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:17] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:18] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:19] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:22] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:24] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:25] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /api/valves/valve_1 HTTP/1.1" 500 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves/valve_1 HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /api/valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:26] "PUT /valves HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 16:27:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:43] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:43] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:46] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:46] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:49] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:49] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:52] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:52] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:55] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:55] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:58] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:27:58] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:01] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:01] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:04] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:04] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:07] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:07] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:10] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:10] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:13] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:13] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:16] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:16] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:19] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:19] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:22] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:22] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:25] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:25] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:28] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:28] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:31] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:31] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:34] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:34] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:37] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:37] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:40] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:40] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:43] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:43] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:46] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:46] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:28:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:14] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:14] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:17] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:17] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:44] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:44] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:47] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:47] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:29:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:30:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:30:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:31:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:31:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:32:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:32:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:33:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:33:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:34:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:34:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:35:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:35:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:36:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:36:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:12] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:12] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:13] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:13] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:16] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:16] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:44] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:44] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:47] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:47] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:37:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:14] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:14] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:17] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:17] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:44] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:44] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:47] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:47] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:38:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:14] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:14] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:39:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:40:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:40:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:41:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:41:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:42:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:42:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:43:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:43:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:16] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:16] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:16] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:16] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:19] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:19] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:31] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:31] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:44] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:44] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:47] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:47] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:44:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:14] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:14] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:17] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:17] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:45:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:46:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:46:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:47:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:47:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:48:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:48:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:49:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:49:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:50:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:50:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:51:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:51:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:52:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:52:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:53:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:53:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:54:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:54:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:55:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:55:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:56:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:56:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:57:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:57:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:58:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:58:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:59:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 16:59:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:00:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:00:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:01:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:01:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:02:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:02:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:03:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:03:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:04:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:04:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:05:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:05:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:06:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:06:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:07:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:07:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:08:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:08:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:09:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:09:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:10:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:10:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:11:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:11:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:12:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:12:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:13:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:13:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:14:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:14:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:15:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:15:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:16:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:16:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:17:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:17:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:18:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:18:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:19:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:19:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:20:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:20:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:21:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:21:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:22:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:22:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:16] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:16] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET / HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /styles.css?v=20260625-ds18b20-id-1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /app.js?v=20260625-setpoint-draft-1 HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] code 404, message File not found
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /favicon.ico HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /api/serial/ports HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:18] "GET /api/serial/status HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:19] Request timed out: TimeoutError('Modbus RTU response timeout')
+127.0.0.1 - - [25/Jun/2026 17:23:19] "POST /api/calibration/all HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 17:23:19] "POST /calibration/all HTTP/1.1" 404 -
+127.0.0.1 - - [25/Jun/2026 17:23:22] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:22] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:25] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:25] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:28] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:28] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:31] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:31] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:34] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:34] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:38] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:38] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:41] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:41] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:44] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:44] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:47] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:47] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:50] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:50] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:53] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:53] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:56] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:56] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:59] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:23:59] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:02] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:02] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:05] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:05] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:08] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:08] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:11] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:11] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:14] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:14] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:17] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:17] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:20] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:20] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:23] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:23] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:26] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:26] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:29] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:29] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:32] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:32] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:35] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:24:35] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:25:03] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:25:03] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:25:04] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:25:04] "GET /api/valves HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:25:07] "GET /api/sensors HTTP/1.1" 200 -
+127.0.0.1 - - [25/Jun/2026 17:25:07] "GET /api/valves HTTP/1.1" 200 -
diff --git a/john103C6T6NewVer/server.out.log b/john103C6T6NewVer/server.out.log
new file mode 100644
index 0000000..e69de29
diff --git a/john103C6T6NewVer/server.pid b/john103C6T6NewVer/server.pid
new file mode 100644
index 0000000..d763ca2
--- /dev/null
+++ b/john103C6T6NewVer/server.pid
@@ -0,0 +1 @@
+23976
diff --git a/john103C6T6NewVer/server.port b/john103C6T6NewVer/server.port
new file mode 100644
index 0000000..524996e
--- /dev/null
+++ b/john103C6T6NewVer/server.port
@@ -0,0 +1 @@
+8090
diff --git a/john103C6T6NewVer/server.url b/john103C6T6NewVer/server.url
new file mode 100644
index 0000000..01f1635
--- /dev/null
+++ b/john103C6T6NewVer/server.url
@@ -0,0 +1 @@
+http://127.0.0.1:8090
diff --git a/john103C6T6NewVer/start_server.bat b/john103C6T6NewVer/start_server.bat
new file mode 100644
index 0000000..c23a0b2
--- /dev/null
+++ b/john103C6T6NewVer/start_server.bat
@@ -0,0 +1,78 @@
+@echo off
+setlocal EnableExtensions EnableDelayedExpansion
+cd /d "%~dp0"
+
+set "HOST=127.0.0.1"
+set "PID_FILE=%~dp0server.pid"
+set "URL_FILE=%~dp0server.url"
+
+if exist "%PID_FILE%" (
+ set /p OLD_PID=<"%PID_FILE%"
+ if defined OLD_PID (
+ tasklist /FI "PID eq %OLD_PID%" 2>NUL | find "%OLD_PID%" >NUL
+ if not errorlevel 1 (
+ echo Server is already running. PID: %OLD_PID%
+ if exist "%URL_FILE%" (
+ set /p URL=<"%URL_FILE%"
+ ) else (
+ set "URL=http://%HOST%:8080"
+ )
+ echo URL: !URL!
+ start "" "!URL!"
+ pause
+ exit /b 0
+ )
+ )
+ del "%PID_FILE%" >NUL 2>NUL
+)
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command ^
+ "$ErrorActionPreference='Stop';" ^
+ "$wd=(Resolve-Path -LiteralPath '%~dp0').Path;" ^
+ "$py=Get-Command py -ErrorAction SilentlyContinue;" ^
+ "$python=$null;" ^
+ "if($py){$python=(& $py.Source -3 -c 'import sys; print(sys.executable)' 2>$null | Select-Object -First 1)};" ^
+ "if(-not $python){$cmd=Get-Command python -ErrorAction SilentlyContinue; if($cmd){$python=$cmd.Source}};" ^
+ "if(-not $python -or -not (Test-Path -LiteralPath $python)){Write-Host 'Python was not found. Install Python or add it to PATH.'; exit 1};" ^
+ "$ports=@(8080,8090,8081,8000,5000,5500,18080,18081,28080,28081,49152,49153,49200);" ^
+ "$last='';" ^
+ "foreach($port in $ports){" ^
+ " Remove-Item -LiteralPath (Join-Path $wd 'server.out.log') -Force -ErrorAction SilentlyContinue;" ^
+ " Remove-Item -LiteralPath (Join-Path $wd 'server.err.log') -Force -ErrorAction SilentlyContinue;" ^
+ " $args=@('serial_bridge.py','--host','%HOST%','--port',[string]$port);" ^
+ " try{$p=Start-Process -FilePath $python -ArgumentList $args -WorkingDirectory $wd -WindowStyle Hidden -RedirectStandardOutput (Join-Path $wd 'server.out.log') -RedirectStandardError (Join-Path $wd 'server.err.log') -PassThru}catch{$last=$_.Exception.Message; continue};" ^
+ " Start-Sleep -Milliseconds 900;" ^
+ " $p.Refresh();" ^
+ " if(-not $p.HasExited){" ^
+ " $url='http://%HOST%:' + $port;" ^
+ " $p.Id | Set-Content -Encoding ASCII -LiteralPath (Join-Path $wd 'server.pid');" ^
+ " $port | Set-Content -Encoding ASCII -LiteralPath (Join-Path $wd 'server.port');" ^
+ " $url | Set-Content -Encoding ASCII -LiteralPath (Join-Path $wd 'server.url');" ^
+ " Write-Host ('Started server PID: ' + $p.Id);" ^
+ " Write-Host ('URL: ' + $url);" ^
+ " exit 0;" ^
+ " }" ^
+ " if(Test-Path -LiteralPath (Join-Path $wd 'server.err.log')){$last=Get-Content -LiteralPath (Join-Path $wd 'server.err.log') -Raw}" ^
+ "}" ^
+ "Write-Host 'Server exited immediately on all candidate ports. Check server.err.log.';" ^
+ "if($last){Write-Host $last};" ^
+ "exit 1;"
+
+if errorlevel 1 (
+ echo Failed to start server.
+ echo See server.err.log if it was created.
+ pause
+ exit /b 1
+)
+
+if exist "%URL_FILE%" (
+ set /p URL=<"%URL_FILE%"
+) else (
+ set "URL=http://%HOST%:8080"
+)
+
+echo Server started.
+echo URL: !URL!
+echo Logs: server.out.log and server.err.log
+start "" "!URL!"
+pause
diff --git a/john103C6T6NewVer/stop_server.bat b/john103C6T6NewVer/stop_server.bat
new file mode 100644
index 0000000..6d59f7a
--- /dev/null
+++ b/john103C6T6NewVer/stop_server.bat
@@ -0,0 +1,30 @@
+@echo off
+setlocal
+cd /d "%~dp0"
+
+set "PID_FILE=%~dp0server.pid"
+
+powershell -NoProfile -ExecutionPolicy Bypass -Command ^
+ "$ErrorActionPreference='Stop';" ^
+ "$wd=(Resolve-Path -LiteralPath '%~dp0').Path;" ^
+ "$pidPath=Join-Path $wd 'server.pid';" ^
+ "if(-not (Test-Path -LiteralPath $pidPath)){Write-Host 'server.pid not found. Server is not running or was not started by start_server.bat.'; exit 0};" ^
+ "$text=(Get-Content -LiteralPath $pidPath -Raw).Trim();" ^
+ "try{$id=[int]$text}catch{Write-Host 'Invalid PID file.'; Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue; exit 1};" ^
+ "$proc=Get-CimInstance Win32_Process -Filter ('ProcessId=' + $id) -ErrorAction SilentlyContinue;" ^
+ "if(-not $proc){Write-Host ('Process ' + $id + ' is not running.'); Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue; exit 0};" ^
+ "if($proc.CommandLine -notmatch 'serial_bridge\.py'){Write-Host ('PID ' + $id + ' is not serial_bridge.py. Stop cancelled.'); exit 1};" ^
+ "Stop-Process -Id $id -Force;" ^
+ "Remove-Item -LiteralPath $pidPath -Force -ErrorAction SilentlyContinue;" ^
+ "Remove-Item -LiteralPath (Join-Path $wd 'server.port') -Force -ErrorAction SilentlyContinue;" ^
+ "Remove-Item -LiteralPath (Join-Path $wd 'server.url') -Force -ErrorAction SilentlyContinue;" ^
+ "Write-Host ('Stopped server PID: ' + $id);"
+
+if errorlevel 1 (
+ echo Failed to stop server.
+ pause
+ exit /b 1
+)
+
+echo Server stopped.
+pause
diff --git a/john103C6T6NewVer/styles.css b/john103C6T6NewVer/styles.css
new file mode 100644
index 0000000..2e06369
--- /dev/null
+++ b/john103C6T6NewVer/styles.css
@@ -0,0 +1,1145 @@
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: "Trebuchet MS", "Verdana", "Georgia", serif;
+ background: radial-gradient(circle at 10% 10%, #ffefd5 0%, #fce4ec 35%, #e1f5fe 100%);
+ color: #1d2a38;
+ min-height: 100vh;
+}
+
+.shell {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 24px;
+}
+
+.header h1 {
+ margin: 0;
+ letter-spacing: 0.01em;
+}
+
+.header p {
+ margin-top: 6px;
+ color: #344357;
+}
+
+.panel {
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.68);
+ border: 1px solid rgba(29, 42, 56, 0.1);
+ padding: 16px;
+ margin-top: 14px;
+ box-shadow: 0 20px 40px -30px rgba(0, 0, 0, 0.35);
+}
+
+.api-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+#apiBase {
+ min-width: 260px;
+ flex: 1;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1px solid #b6c7de;
+}
+
+button {
+ border: 0;
+ cursor: pointer;
+ border-radius: 8px;
+ padding: 10px 14px;
+ background: #2f5d95;
+ color: #fff;
+ font-weight: 600;
+}
+
+button:hover {
+ background: #254a75;
+}
+
+.status {
+ padding: 8px 12px;
+ border-radius: 999px;
+ background: #ffecb3;
+ font-weight: 600;
+}
+
+.hint {
+ color: #445566;
+ margin-bottom: 0;
+ margin-top: 8px;
+}
+
+.serial-block {
+ margin-top: 12px;
+ border-top: 1px dashed #d8e2ef;
+ padding-top: 12px;
+}
+
+.serial-row {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+#comPortSelect {
+ min-width: 180px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ border: 1px solid #b6c7de;
+ background: #fff;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16px;
+}
+
+.card h2 {
+ margin-top: 0;
+}
+
+.list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.item {
+ border: 1px solid #d8e2ef;
+ border-radius: 10px;
+ padding: 12px;
+ background: #fff;
+ box-shadow: 0 10px 20px -22px rgba(0, 0, 0, 0.4);
+}
+
+.item-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.value {
+ font-size: 26px;
+ font-weight: 700;
+ margin: 4px 0;
+}
+
+.sub {
+ font-size: 13px;
+ color: #526379;
+}
+
+.controls {
+ margin-top: 10px;
+ display: grid;
+ gap: 8px;
+}
+
+label {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+ font-size: 13px;
+ color: #304257;
+}
+
+.controls input[type="number"],
+.controls input[type="range"] {
+ width: 120px;
+ padding: 6px 8px;
+}
+
+.toggle {
+ display: inline-flex;
+ background: #edf2f7;
+ border-radius: 10px;
+ padding: 2px;
+ width: fit-content;
+}
+
+.toggle button {
+ background: #dde6f2;
+ color: #1f2d3c;
+}
+
+.toggle button.active {
+ background: #2f5d95;
+ color: #fff;
+}
+
+.modeRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+}
+
+.valveAuto,
+.valveManual {
+ border-top: 1px dashed #d6e0eb;
+ margin-top: 8px;
+ padding-top: 8px;
+ display: grid;
+ gap: 6px;
+}
+
+.hidden {
+ display: none;
+}
+
+.status-ok {
+ color: #0f7f2f;
+ font-weight: 600;
+}
+
+.status-warn {
+ color: #9a5700;
+ font-weight: 600;
+}
+
+.status-error {
+ color: #a61f1f;
+ font-weight: 600;
+}
+
+.footer {
+ margin-top: 12px;
+ color: #3b4a60;
+ min-height: 28px;
+}
+
+@media (max-width: 880px) {
+ .grid {
+ grid-template-columns: 1fr;
+ }
+
+ .api-row {
+ align-items: stretch;
+ }
+}
+
+
+/* Compact 16 sensors / 32 valves layout */
+.shell {
+ max-width: 1540px;
+ padding: 18px;
+}
+
+.hero {
+ padding: 22px;
+ margin-bottom: 14px;
+}
+
+.hero h1 {
+ margin: 0 0 8px;
+ font-size: clamp(28px, 4vw, 54px);
+ line-height: 0.95;
+}
+
+.hero p {
+ max-width: 780px;
+ margin: 0;
+ font-size: 15px;
+}
+
+.grid {
+ grid-template-columns: minmax(300px, 0.72fr) minmax(620px, 1.7fr);
+ gap: 14px;
+}
+
+.panel {
+ padding: 14px;
+ border-radius: 18px;
+}
+
+.panel h2 {
+ margin: 0 0 10px;
+ font-size: 18px;
+}
+
+.controls,
+.connection-bar,
+.api-bar,
+.settings-grid {
+ gap: 8px;
+}
+
+button,
+select,
+input {
+ min-height: 34px;
+ padding: 7px 10px;
+ border-radius: 10px;
+ font-size: 13px;
+}
+
+.list {
+ display: grid;
+ gap: 8px;
+}
+
+#sensors {
+ grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
+}
+
+#valves {
+ grid-template-columns: repeat(auto-fill, minmax(215px, 1fr));
+}
+
+.item.compact-item {
+ gap: 8px;
+ padding: 10px;
+ border-radius: 14px;
+ min-width: 0;
+}
+
+.compact-head,
+.compact-main,
+.compact-controls,
+.compact-actions,
+.inline-control,
+.range-control {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.compact-head,
+.compact-main,
+.compact-actions {
+ justify-content: space-between;
+}
+
+.compact-head h3,
+.compact-head h4 {
+ margin: 0;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 14px;
+}
+
+.compact-main {
+ flex-wrap: wrap;
+}
+
+.compact-controls {
+ align-items: stretch;
+ flex-direction: column;
+ gap: 7px;
+}
+
+.compact-actions {
+ gap: 6px;
+}
+
+.compact-actions button,
+.mini-btn {
+ min-height: 30px;
+ padding: 5px 9px;
+ border-radius: 9px;
+ font-size: 12px;
+}
+
+.inline-control,
+.range-control {
+ min-width: 0;
+ flex: 1 1 auto;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ font-size: 12px;
+ color: var(--muted);
+}
+
+.inline-control input {
+ width: 74px;
+}
+
+.range-control input[type="range"] {
+ flex: 1 1 90px;
+ min-width: 90px;
+}
+
+.small-value {
+ font-weight: 800;
+ font-size: 15px;
+ color: var(--ink);
+}
+
+.sensor-item .small-value {
+ font-size: 20px;
+}
+
+.sensor-zone,
+.valve-zone,
+.status-pill {
+ flex: 0 0 auto;
+ padding: 4px 8px;
+ border-radius: 999px;
+ background: rgba(17, 38, 60, 0.08);
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 800;
+}
+
+.status-pill.open {
+ background: rgba(34, 166, 112, 0.18);
+ color: #0d6b45;
+}
+
+.status-pill.closed {
+ background: rgba(221, 91, 81, 0.15);
+ color: #9c2f26;
+}
+
+.toggle {
+ flex: 0 0 auto;
+ gap: 3px;
+ padding: 3px;
+ border-radius: 11px;
+}
+
+.toggle button {
+ min-height: 28px;
+ padding: 4px 8px;
+ border-radius: 8px;
+ font-size: 11px;
+}
+
+.meta-row,
+.log,
+.hint {
+ font-size: 12px;
+}
+
+@media (max-width: 1100px) {
+ .grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 560px) {
+ .shell {
+ padding: 10px;
+ }
+
+ .hero,
+ .panel {
+ padding: 12px;
+ }
+
+ #sensors,
+ #valves {
+ grid-template-columns: 1fr;
+ }
+}
+
+
+/* Channel cards like MCU panel */
+.valve-item.channel-card {
+ padding: 9px;
+ background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(232, 236, 222, 0.72));
+}
+
+#valves {
+ grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
+}
+
+.channel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 12px;
+ color: var(--muted);
+}
+
+.channel-head strong {
+ font-size: 15px;
+ color: var(--ink);
+}
+
+.channel-title {
+ display: grid;
+ gap: 2px;
+ min-width: 0;
+}
+
+.channel-title small {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 10px;
+ font-weight: 800;
+ color: var(--muted);
+}
+
+.channel-head span,
+.channel-connect,
+.valve-state-row span {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ white-space: nowrap;
+}
+
+.channel-lamp {
+ display: inline-block;
+ width: 17px;
+ height: 17px;
+ border-radius: 50%;
+ border: 1px solid rgba(19, 32, 47, 0.28);
+ box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.8), 0 1px 3px rgba(19, 32, 47, 0.22);
+ background: #d8dde2;
+}
+
+.channel-lamp.on {
+ background: radial-gradient(circle at 35% 30%, #b5ff8d, #159400 62%, #0e6000);
+}
+
+.channel-lamp.off {
+ background: linear-gradient(145deg, #f5f6f8, #cfd5da);
+}
+
+.channel-lamp.alarm {
+ background: radial-gradient(circle at 35% 30%, #ffb9b9, #ff1010 62%, #930000);
+}
+
+.channel-id-grid {
+ display: grid;
+ grid-template-columns: 0.72fr 0.82fr 0.9fr 0.9fr;
+ align-items: end;
+ gap: 6px;
+}
+
+.channel-id-grid label {
+ display: grid;
+ gap: 3px;
+ min-width: 0;
+ font-size: 11px;
+ color: var(--muted);
+}
+
+.channel-id-grid input,
+.channel-id-grid select {
+ width: 100%;
+ min-height: 28px;
+ padding: 4px 7px;
+ border-radius: 7px;
+ font-size: 12px;
+}
+
+.channel-connect {
+ min-height: 28px;
+ justify-content: center;
+ padding-bottom: 3px;
+ font-size: 11px;
+ color: var(--muted);
+}
+
+.channel-body {
+ display: grid;
+ grid-template-columns: 44px minmax(0, 1fr);
+ gap: 8px;
+ align-items: stretch;
+}
+
+.temperature-widget {
+ display: grid;
+ grid-template-columns: 19px 15px;
+ grid-template-rows: 1fr auto;
+ gap: 3px;
+ min-height: 152px;
+}
+
+.temp-scale {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-end;
+ font-size: 10px;
+ line-height: 1;
+ color: #0d3550;
+}
+
+.temp-bar {
+ position: relative;
+ overflow: hidden;
+ align-self: stretch;
+ border: 1px solid rgba(13, 53, 80, 0.4);
+ background: repeating-linear-gradient(to top, rgba(13, 53, 80, 0.1) 0 1px, transparent 1px 18px), #f6fbff;
+}
+
+.temp-bar b {
+ position: absolute;
+ left: 1px;
+ right: 1px;
+ bottom: 1px;
+ min-height: 3px;
+ background: linear-gradient(180deg, #ff6a3d, #ff1d1d);
+}
+
+.temp-now {
+ grid-column: 1 / -1;
+ text-align: center;
+ font-size: 11px;
+ font-weight: 800;
+ color: var(--ink);
+}
+
+.channel-workarea {
+ display: grid;
+ gap: 7px;
+ min-width: 0;
+}
+
+.angle-panel,
+.position-panel {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ gap: 4px 8px;
+ padding: 7px 9px;
+ border: 1px solid rgba(19, 32, 47, 0.12);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.64);
+}
+
+.angle-panel span,
+.position-panel span {
+ font-size: 12px;
+ color: var(--muted);
+}
+
+.angle-panel strong,
+.position-panel strong {
+ font-size: 20px;
+ color: var(--ink);
+}
+
+.angle-panel small,
+.position-panel small {
+ grid-column: 1 / -1;
+ font-size: 11px;
+ color: var(--muted);
+}
+
+.position-panel {
+ background: rgba(245, 251, 255, 0.72);
+}
+
+.channel-data-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 5px;
+}
+
+.channel-data-grid div {
+ display: grid;
+ gap: 2px;
+ min-width: 0;
+ padding: 6px 7px;
+ border: 1px solid rgba(19, 32, 47, 0.1);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.58);
+}
+
+.channel-data-grid span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 10px;
+ color: var(--muted);
+}
+
+.channel-data-grid strong {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+ color: var(--ink);
+}
+
+.delta.ok {
+ color: #0d6b45;
+}
+
+.delta.hot {
+ color: #b73520;
+}
+
+.delta.cold {
+ color: #1759a6;
+}
+
+.channel-position {
+ gap: 6px;
+ font-size: 11px;
+}
+
+.valve-state-row,
+.channel-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.valve-state-row {
+ font-size: 11px;
+ color: var(--ink);
+}
+
+.channel-actions button {
+ min-height: 28px;
+ padding: 4px 10px;
+ border-radius: 8px;
+ font-size: 11px;
+}
+
+.channel-mode-line {
+ justify-content: space-between;
+ gap: 5px;
+}
+
+.channel-mode-line .toggle {
+ margin-right: auto;
+}
+
+@media (max-width: 560px) {
+ .channel-id-grid {
+ grid-template-columns: 1fr;
+ align-items: stretch;
+ }
+
+ .channel-connect {
+ justify-content: flex-start;
+ }
+}
+
+
+/* Full 16 channel operator blocks */
+#sensors,
+#valves {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.full-channel-card {
+ gap: 9px;
+}
+
+.full-channel-id-grid {
+ grid-template-columns: 0.7fr 0.8fr 1.5fr;
+}
+
+.full-channel-body {
+ grid-template-columns: 48px minmax(0, 1fr);
+}
+
+.top-metrics-row {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 7px;
+}
+
+.main-position-panel {
+ background: rgba(228, 247, 255, 0.82);
+}
+
+.main-position-panel strong {
+ font-size: 28px;
+ line-height: 1;
+}
+
+.full-channel-data-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.full-state-row {
+ justify-content: space-around;
+ padding: 6px 8px;
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.56);
+ border: 1px solid rgba(19, 32, 47, 0.1);
+}
+
+@media (max-width: 1260px) {
+ #sensors,
+ #valves {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 900px) {
+ #sensors,
+ #valves {
+ grid-template-columns: 1fr;
+ }
+
+ .full-channel-id-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .full-channel-data-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 560px) {
+ .top-metrics-row {
+ grid-template-columns: 1fr;
+ }
+
+ .full-channel-id-grid,
+ .full-channel-data-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Presentation upgrade for operator dashboard */
+:root {
+ --panel-blue: #123a5f;
+ --panel-blue-2: #1f5f93;
+ --panel-steel: #eef4f8;
+ --panel-line: rgba(31, 95, 147, 0.24);
+ --ok-green: #159447;
+ --warn-red: #d63b2a;
+ --amber: #f5b43f;
+}
+
+body {
+ background:
+ radial-gradient(circle at 12% 12%, rgba(45, 111, 167, 0.18), transparent 30%),
+ radial-gradient(circle at 88% 4%, rgba(245, 180, 63, 0.22), transparent 24%),
+ linear-gradient(135deg, #f6eadb 0%, #f7f1f5 42%, #e8f1fb 100%);
+}
+
+.shell {
+ max-width: 1680px;
+}
+
+.hero {
+ position: relative;
+ overflow: hidden;
+ border: 1px solid rgba(18, 58, 95, 0.14);
+ box-shadow: 0 18px 60px rgba(18, 58, 95, 0.14);
+ background:
+ linear-gradient(120deg, rgba(255, 255, 255, 0.92), rgba(234, 243, 250, 0.88)),
+ repeating-linear-gradient(90deg, rgba(18, 58, 95, 0.04) 0 1px, transparent 1px 18px);
+}
+
+.hero::after {
+ content: "";
+ position: absolute;
+ right: -90px;
+ top: -110px;
+ width: 320px;
+ height: 320px;
+ border-radius: 50%;
+ background: conic-gradient(from 220deg, rgba(31, 95, 147, 0.22), rgba(245, 180, 63, 0.2), rgba(31, 95, 147, 0.08));
+ filter: blur(2px);
+}
+
+.hero h1 {
+ position: relative;
+ z-index: 1;
+ letter-spacing: -0.035em;
+ color: #0d2e4d;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.9);
+}
+
+.hero p {
+ position: relative;
+ z-index: 1;
+}
+
+.panel {
+ border: 1px solid rgba(18, 58, 95, 0.14);
+ box-shadow: 0 16px 44px rgba(18, 58, 95, 0.12);
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(246, 250, 253, 0.92)),
+ repeating-linear-gradient(0deg, rgba(18, 58, 95, 0.025) 0 1px, transparent 1px 20px);
+}
+
+.panel h2 {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: #0d2e4d;
+}
+
+.panel h2::before {
+ content: "";
+ width: 11px;
+ height: 28px;
+ border-radius: 999px;
+ background: linear-gradient(180deg, var(--panel-blue-2), var(--amber));
+ box-shadow: 0 0 0 4px rgba(31, 95, 147, 0.08);
+}
+
+button {
+ box-shadow: 0 8px 18px rgba(18, 58, 95, 0.14);
+ transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.12s ease;
+}
+
+button:hover {
+ transform: translateY(-1px);
+ filter: saturate(1.08);
+ box-shadow: 0 11px 24px rgba(18, 58, 95, 0.18);
+}
+
+input,
+select {
+ border: 1px solid rgba(18, 58, 95, 0.22);
+ box-shadow: inset 0 1px 2px rgba(18, 58, 95, 0.06);
+ background: linear-gradient(180deg, #ffffff, #f7fbff);
+}
+
+.full-channel-card {
+ position: relative;
+ overflow: hidden;
+ border: 1px solid rgba(18, 58, 95, 0.22);
+ box-shadow:
+ 0 14px 32px rgba(18, 58, 95, 0.15),
+ inset 0 1px 0 rgba(255, 255, 255, 0.86);
+ background:
+ linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(232, 241, 248, 0.95)),
+ repeating-linear-gradient(90deg, rgba(18, 58, 95, 0.035) 0 1px, transparent 1px 16px);
+}
+
+.full-channel-card::before {
+ content: "";
+ position: absolute;
+ inset: 0 auto 0 0;
+ width: 6px;
+ background: linear-gradient(180deg, var(--panel-blue-2), var(--ok-green), var(--amber));
+}
+
+.full-channel-card::after {
+ content: "";
+ position: absolute;
+ inset: 0 0 auto 0;
+ height: 44px;
+ background: linear-gradient(90deg, rgba(18, 58, 95, 0.1), transparent 60%);
+ pointer-events: none;
+}
+
+.channel-head,
+.channel-id-grid,
+.channel-body {
+ position: relative;
+ z-index: 1;
+}
+
+.channel-head {
+ min-height: 42px;
+ padding: 8px 10px 8px 14px;
+ margin: -2px -2px 2px 0;
+ border-radius: 12px;
+ background: linear-gradient(90deg, rgba(18, 58, 95, 0.1), rgba(255, 255, 255, 0.64));
+}
+
+.channel-title strong {
+ font-size: 18px;
+ letter-spacing: -0.02em;
+}
+
+.channel-title small {
+ font-size: 11px;
+ color: #42647f;
+}
+
+.channel-lamp {
+ width: 20px;
+ height: 20px;
+ border: 2px solid rgba(255, 255, 255, 0.88);
+ box-shadow:
+ inset 0 1px 3px rgba(255, 255, 255, 0.8),
+ 0 0 0 1px rgba(18, 58, 95, 0.16),
+ 0 3px 9px rgba(18, 58, 95, 0.22);
+}
+
+.channel-lamp.on {
+ box-shadow:
+ inset 0 1px 3px rgba(255, 255, 255, 0.82),
+ 0 0 0 1px rgba(21, 148, 71, 0.28),
+ 0 0 14px rgba(21, 148, 71, 0.55);
+}
+
+.channel-lamp.alarm {
+ box-shadow:
+ inset 0 1px 3px rgba(255, 255, 255, 0.82),
+ 0 0 0 1px rgba(214, 59, 42, 0.28),
+ 0 0 14px rgba(214, 59, 42, 0.48);
+}
+
+.channel-id-grid {
+ padding: 9px;
+ border-radius: 12px;
+ border: 1px solid rgba(18, 58, 95, 0.12);
+ background: rgba(255, 255, 255, 0.62);
+}
+
+.channel-id-grid label {
+ font-weight: 800;
+ text-transform: uppercase;
+ letter-spacing: 0.035em;
+}
+
+.channel-id-grid input,
+.channel-id-grid select {
+ font-weight: 800;
+ color: #0d2e4d;
+}
+
+.temperature-widget {
+ padding: 8px 4px;
+ border-radius: 14px;
+ border: 1px solid rgba(18, 58, 95, 0.16);
+ background: linear-gradient(180deg, #fafdff, #eaf3f8);
+ box-shadow: inset 0 1px 4px rgba(18, 58, 95, 0.08);
+}
+
+.temp-bar {
+ border-radius: 8px;
+ border-color: rgba(18, 58, 95, 0.34);
+ box-shadow: inset 0 2px 8px rgba(18, 58, 95, 0.12);
+}
+
+.temp-bar b {
+ border-radius: 7px 7px 2px 2px;
+ background: linear-gradient(180deg, #ffb347, #ff4f2d 52%, #d91414);
+ box-shadow: 0 0 12px rgba(255, 79, 45, 0.42);
+}
+
+.temp-now {
+ color: #0d2e4d;
+}
+
+.top-metrics-row {
+ align-items: stretch;
+}
+
+.position-panel,
+.angle-panel {
+ border-color: rgba(31, 95, 147, 0.22);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 8px 18px rgba(18, 58, 95, 0.08);
+}
+
+.position-panel span,
+.angle-panel span {
+ font-weight: 900;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.position-panel strong,
+.angle-panel strong {
+ color: #0b365b;
+ text-shadow: 0 1px 0 #fff;
+}
+
+.main-position-panel {
+ background:
+ linear-gradient(135deg, rgba(226, 246, 255, 0.96), rgba(255, 255, 255, 0.9)),
+ radial-gradient(circle at 90% 15%, rgba(31, 95, 147, 0.15), transparent 35%);
+}
+
+.angle-panel {
+ background:
+ linear-gradient(135deg, rgba(255, 244, 218, 0.96), rgba(255, 255, 255, 0.9)),
+ radial-gradient(circle at 90% 15%, rgba(245, 180, 63, 0.22), transparent 35%);
+}
+
+.channel-data-grid div {
+ border-color: rgba(18, 58, 95, 0.12);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.78);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(239, 246, 251, 0.76));
+}
+
+.channel-data-grid span {
+ font-weight: 900;
+ text-transform: uppercase;
+ letter-spacing: 0.035em;
+}
+
+.channel-data-grid strong {
+ font-size: 13px;
+}
+
+.full-state-row {
+ background: linear-gradient(90deg, rgba(21, 148, 71, 0.08), rgba(255, 255, 255, 0.72), rgba(214, 59, 42, 0.08));
+}
+
+.channel-actions {
+ padding-top: 2px;
+}
+
+.quickPosition[data-position="100"] {
+ background: linear-gradient(180deg, #2eaf68, #128246);
+ color: #fff;
+}
+
+.quickPosition[data-position="0"] {
+ background: linear-gradient(180deg, #f06a57, #c93024);
+ color: #fff;
+}
+
+.channel-mode .active {
+ background: linear-gradient(180deg, #234d73, #123a5f);
+ color: #fff;
+}
+
+.hidden {
+ display: none !important;
+}
+
+#modbusTransport {
+ min-width: 160px;
+}
+
+#tcpHost {
+ min-width: 150px;
+}
+
+#tcpPort {
+ width: 92px;
+}
+
+#modbusSlaveId {
+ width: 78px;
+}
+
+.serial-row label {
+ font-weight: 800;
+ color: #24445e;
+}
+
+/* Single full-width channel workspace */
+.grid {
+ grid-template-columns: 1fr;
+}
+
+.panel:has(#valves) {
+ display: block;
+}
+
+.grid > *:has(#valves),
+#valves {
+ display: none;
+}
diff --git a/new rev/john103C8T6/Core/Src/main.c b/new rev/john103C8T6/Core/Src/main.c
index 22b08f9..f280742 100644
--- a/new rev/john103C8T6/Core/Src/main.c
+++ b/new rev/john103C8T6/Core/Src/main.c
@@ -1,4 +1,4 @@
-/* USER CODE BEGIN Header */
+/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
@@ -434,9 +434,18 @@ void reinit_t_sens(void)
// sens[i].Init.InitParam.ROM = rom_address;
sens[i].Init.InitParam.Ind = i;
sens[i].Init.init_func = &Dallas_SensorInitByInd;
- sens[i].Init.Resolution = DALLAS_CONFIG_9_BITS;
- MB_DATA.HoldRegs.set_Temp[i] = sens[i].set_temp = 20.;
- MB_DATA.HoldRegs.set_hyst[i] = sens[i].hyst = 1;
+ sens[i].Init.Resolution = DALLAS_CONFIG_9_BITS; sens[i].set_temp = 20.;
+ sens[i].hyst = 1;
+ MB_DATA.HoldRegs.set_Temp[i] = (uint16_t)(sens[i].set_temp * MB_ROOM_TEMP_SCALE);
+ MB_DATA.HoldRegs.set_hyst[i] = (uint16_t)(sens[i].hyst * MB_ROOM_TEMP_SCALE);
+ MB_DATA.HoldRegs.room_cfg[i].setpoint_x10 = MB_DATA.HoldRegs.set_Temp[i];
+ MB_DATA.HoldRegs.room_cfg[i].hysteresis_x10 = MB_DATA.HoldRegs.set_hyst[i];
+ MB_DATA.HoldRegs.room_cfg[i].valve_position_pct = 0;
+ MB_DATA.HoldRegs.room_cfg[i].valve_angle_max_deg = MB_ROOM_VALVE_ANGLE_MAX_DEFAULT;
+ MB_DATA.HoldRegs.room_cfg[i].mode = ROOM_MODE_AUTO;
+ MB_DATA.HoldRegs.room_cfg[i].command = ROOM_COMMAND_STOP;
+ MB_DATA.HoldRegs.room_cfg[i].location = 1;
+ MB_DATA.HoldRegs.room_cfg[i].apply = 0;
Dallas_AddNewSensors(&hdallas, &sens[i]);
}
@@ -503,6 +512,70 @@ FuncStat packStruct(MB_DataStructureTypeDef* MB_DATA, int sizeARR)
return FuncOK;
}
+
+static uint16_t clamp_room_percent(uint16_t value)
+{
+ return value > 100U ? 100U : value;
+}
+
+static void update_room_modbus(MB_DataStructureTypeDef* MB_DATA)
+{
+ for (int i = 0; i < MAX_SENSE; i++)
+ {
+ MB_RoomInputRegsTypeDef* room = &MB_DATA->InRegs.room[i];
+ MB_RoomHoldingRegsTypeDef* cfg = &MB_DATA->HoldRegs.room_cfg[i];
+ uint16_t open_state = 0U;
+ uint16_t close_state = 0U;
+ uint16_t position_pct;
+ uint16_t angle_max;
+
+ if (cfg->valve_angle_max_deg == 0U)
+ {
+ cfg->valve_angle_max_deg = MB_ROOM_VALVE_ANGLE_MAX_DEFAULT;
+ }
+
+ if (i < 16)
+ {
+ open_state = (MB_DATA->Coils.relay_struct_on.all >> i) & 0x1U;
+ close_state = (MB_DATA->Coils.relay_struct_off.all >> i) & 0x1U;
+ }
+
+ position_pct = clamp_room_percent(cfg->valve_position_pct);
+ if (cfg->mode == ROOM_MODE_AUTO)
+ {
+ if (open_state)
+ {
+ position_pct = 100U;
+ }
+ else if (close_state)
+ {
+ position_pct = 0U;
+ }
+ }
+ cfg->valve_position_pct = position_pct;
+ angle_max = cfg->valve_angle_max_deg;
+
+ room->channel = (uint16_t)(i + 1);
+ room->location = cfg->location;
+ for (int reg = 0; reg < 4; reg++)
+ {
+ room->ds18b20_id[reg] = ((uint16_t)MB_DATA->InRegs.ID.DevAddr[i][reg * 2]) |
+ ((uint16_t)MB_DATA->InRegs.ID.DevAddr[i][reg * 2 + 1] << 8);
+ }
+ room->temperature_x10 = (i < hdallas.onewire->RomCnt) ? (uint16_t)((int16_t)(sens[i].temperature * MB_ROOM_TEMP_SCALE)) : 0U;
+ room->setpoint_x10 = MB_DATA->HoldRegs.set_Temp[i];
+ room->hysteresis_x10 = MB_DATA->HoldRegs.set_hyst[i];
+ room->valve_position_pct = position_pct;
+ room->valve_angle_deg = (uint16_t)((position_pct * angle_max) / 100U);
+ room->valve_angle_max_deg = angle_max;
+ room->is_connected = (i < hdallas.onewire->RomCnt) ? (uint16_t)sens[i].isConnected : 0U;
+ room->valve_open = open_state;
+ room->valve_close = close_state;
+ room->mode = cfg->mode;
+ room->command_state = cfg->command;
+ room->reserved = 0U;
+ }
+}
FuncStat Field_modbus(MB_DataStructureTypeDef* MB_DATA, Flags_TypeDef* flag)
{
@@ -516,15 +589,23 @@ FuncStat Field_modbus(MB_DataStructureTypeDef* MB_DATA, Flags_TypeDef* flag)
MB_DATA->Coils.init_param = 0;
for(int i = 0; i < hdallas.onewire->RomCnt; i++)
- {
- sens[i].set_temp = MB_DATA->HoldRegs.set_Temp[i];
- sens[i].hyst = MB_DATA->HoldRegs.set_hyst[i];
+ { if (MB_DATA->HoldRegs.room_cfg[i].apply)
+ {
+ MB_DATA->HoldRegs.set_Temp[i] = MB_DATA->HoldRegs.room_cfg[i].setpoint_x10;
+ MB_DATA->HoldRegs.set_hyst[i] = MB_DATA->HoldRegs.room_cfg[i].hysteresis_x10;
+ MB_DATA->HoldRegs.room_cfg[i].apply = 0U;
+ }
+ MB_DATA->HoldRegs.room_cfg[i].setpoint_x10 = MB_DATA->HoldRegs.set_Temp[i];
+ MB_DATA->HoldRegs.room_cfg[i].hysteresis_x10 = MB_DATA->HoldRegs.set_hyst[i];
+ sens[i].set_temp = ((float)MB_DATA->HoldRegs.set_Temp[i]) / MB_ROOM_TEMP_SCALE;
+ sens[i].hyst = ((float)MB_DATA->HoldRegs.set_hyst[i]) / MB_ROOM_TEMP_SCALE;
}
}
+ update_room_modbus(MB_DATA);
return FuncOK;
};
@@ -534,6 +615,31 @@ FuncStat value_control(void )
for(int i = 0; i < hdallas.onewire->RomCnt; i++)
{
+ if (i < 16 && MB_DATA.HoldRegs.room_cfg[i].mode == ROOM_MODE_MANUAL)
+ {
+ uint16_t manual_pct = clamp_room_percent(MB_DATA.HoldRegs.room_cfg[i].valve_position_pct);
+ if (MB_DATA.HoldRegs.room_cfg[i].command == ROOM_COMMAND_CLOSE)
+ {
+ manual_pct = 0U;
+ }
+ else if (MB_DATA.HoldRegs.room_cfg[i].command == ROOM_COMMAND_OPEN && manual_pct == 0U)
+ {
+ manual_pct = 100U;
+ }
+ MB_DATA.HoldRegs.room_cfg[i].valve_position_pct = manual_pct;
+ if (manual_pct > 0U)
+ {
+ MB_DATA.Coils.relay_struct_off.all &= ~(1 << i);
+ MB_DATA.Coils.relay_struct_on.all |= 1 << i;
+ }
+ else
+ {
+ MB_DATA.Coils.relay_struct_on.all &= ~(1 << i);
+ MB_DATA.Coils.relay_struct_off.all |= 1 << i;
+ }
+ continue;
+ }
+
if (sens[i].temperature < sens[i].set_temp - sens[i].hyst)
{
diff --git a/new rev/john103C8T6/MDK-ARM/john103C8T6.uvguix.z b/new rev/john103C8T6/MDK-ARM/john103C8T6.uvguix.z
index 2379d96..aee3b64 100644
--- a/new rev/john103C8T6/MDK-ARM/john103C8T6.uvguix.z
+++ b/new rev/john103C8T6/MDK-ARM/john103C8T6.uvguix.z
@@ -93,25 +93,25 @@
2
3
- -32000
- -32000
+ -1
+ -1
-1
-1
- -25
- -2177
- -243
- 988
+ 805
+ -2875
+ -1424
+ 1528
0
- 669
- 01000000040000000100000001000000010000000100000000000000020000000000000001000000010000000000000028000000280000000100000004000000030000000100000061463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6E6577207265765C636F72655C53544D33325F4D6F646275735C5372635C6D6F646275735F636F696C732E63000000000E6D6F646275735F636F696C732E6300000000C5D4F200FFFFFFFF60463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6E6577207265765C636F72655C53544D33325F4D6F646275735C5372635C6D6F646275735F636F72652E63000000000D6D6F646275735F636F72652E6300000000FFDC7800FFFFFFFF61463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6E6577207265765C636F72655C53544D33325F4D6F646275735C5372635C6D6F646275735F64657669642E63000000000E6D6F646275735F64657669642E6300000000BECEA100FFFFFFFF58463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6E6577207265765C6A6F686E313033433854365C436F72655C5372635C6D61696E2E6300000000066D61696E2E6300000000F0A0A100FFFFFFFF0100000010000000C5D4F200FFDC7800BECEA100F0A0A100BCA8E1009CC1B600F7B88600D9ADC200A5C2D700B3A6BE00EAD6A300F6FA7D00B5E99D005FC3CF00C1838300CACAD500010000000000000002000000F4F6FFFFD1FFFFFF0000000043040000
+ 769
+ 0100000004000000010000000100000001000000010000000000000002000000000000000100000001000000000000002800000028000000010000000400000003000000010000007A463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6A6F686E5F70726F6A5C647331386232302D4D4F444255535C6E6577207265765C636F72655C53544D33325F4D6F646275735C5372635C6D6F646275735F636F696C732E63000000000E6D6F646275735F636F696C732E6300000000C5D4F200FFFFFFFF79463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6A6F686E5F70726F6A5C647331386232302D4D4F444255535C6E6577207265765C636F72655C53544D33325F4D6F646275735C5372635C6D6F646275735F636F72652E63000000000D6D6F646275735F636F72652E6300000000FFDC7800FFFFFFFF7A463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6A6F686E5F70726F6A5C647331386232302D4D4F444255535C6E6577207265765C636F72655C53544D33325F4D6F646275735C5372635C6D6F646275735F64657669642E63000000000E6D6F646275735F64657669642E6300000000BECEA100FFFFFFFF71463A5C7365745C776F726B73706163655C736574636F72705C7365743530365C6769745F70726F6A6563745C64733132386232305C6A6F686E5F70726F6A5C647331386232302D4D4F444255535C6E6577207265765C6A6F686E313033433854365C436F72655C5372635C6D61696E2E6300000000066D61696E2E6300000000F0A0A100FFFFFFFF0100000010000000C5D4F200FFDC7800BECEA100F0A0A100BCA8E1009CC1B600F7B88600D9ADC200A5C2D700B3A6BE00EAD6A300F6FA7D00B5E99D005FC3CF00C1838300CACAD500010000000000000002000000F4F6FFFFD1FFFFFF0000000043040000
@@ -530,7 +530,7 @@
0
16
- 03000000DC0400007D07000035050000
+ 03000000DC040000FD09000035050000
16
@@ -1150,7 +1150,7 @@
0
16
- 0300000066000000ED00000017030000
+ 0300000066000000ED000000A8040000
16
@@ -1170,7 +1170,7 @@
0
16
- 03000000DC0400007D07000035050000
+ 03000000DC040000FD09000035050000
16
@@ -1190,7 +1190,7 @@
0
16
- 03000000DC0400007D07000035050000
+ 03000000DC040000FD09000035050000
16
@@ -1250,7 +1250,7 @@
0
16
- 03000000DC0400007D07000035050000
+ 03000000DC040000FD09000035050000
16
@@ -1270,7 +1270,7 @@
0
16
- 03000000DC0400007D07000035050000
+ 03000000DC040000FD09000035050000
16
@@ -1799,7 +1799,7 @@
3312
- 000000000B000000000000000020000000000000FFFFFFFFFFFFFFFFF4000000D800000090050000DC000000000000000100000004000000010000000000000000000000FFFFFFFF08000000CB00000057010000CC000000F08B00005A01000079070000D601000045890000FFFF02000B004354616262656450616E650020000000000000F4F6FFFFD1FFFFFF90FBFFFF5A000000F40000004F00000090050000D80000000000000040280046080000000B446973617373656D626C7900000000CB00000001000000FFFFFFFFFFFFFFFF14506572666F726D616E636520416E616C797A6572000000005701000001000000FFFFFFFFFFFFFFFF14506572666F726D616E636520416E616C797A657200000000CC00000001000000FFFFFFFFFFFFFFFF0E4C6F67696320416E616C797A657200000000F08B000001000000FFFFFFFFFFFFFFFF0D436F646520436F766572616765000000005A01000001000000FFFFFFFFFFFFFFFF11496E737472756374696F6E205472616365000000007907000001000000FFFFFFFFFFFFFFFF0F53797374656D20416E616C797A657200000000D601000001000000FFFFFFFFFFFFFFFF104576656E742053746174697374696373000000004589000001000000FFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000001000000FFFFFFFFCB00000001000000FFFFFFFFCB000000000000000040000000000000FFFFFFFFFFFFFFFF9C0400004F000000A004000011020000000000000200000004000000010000000000000000000000FFFFFFFF2B000000E2050000CA0900002D8C00002E8C00002F8C0000308C0000318C0000328C0000338C0000348C0000358C0000368C0000378C0000388C0000398C00003A8C00003B8C00003C8C00003D8C00003E8C00003F8C0000408C0000418C000050C3000051C3000052C3000053C3000054C3000055C3000056C3000057C3000058C3000059C300005AC300005BC300005CC300005DC300005EC300005FC3000060C3000061C3000062C3000063C3000001800040000000000000A0FAFFFFD1FFFFFF90FBFFFF93010000A00400004F000000900500001102000000000000404100462B0000000753796D626F6C7300000000E205000001000000FFFFFFFFFFFFFFFF0A5472616365204461746100000000CA09000001000000FFFFFFFFFFFFFFFF00000000002D8C000001000000FFFFFFFFFFFFFFFF00000000002E8C000001000000FFFFFFFFFFFFFFFF00000000002F8C000001000000FFFFFFFFFFFFFFFF0000000000308C000001000000FFFFFFFFFFFFFFFF0000000000318C000001000000FFFFFFFFFFFFFFFF0000000000328C000001000000FFFFFFFFFFFFFFFF0000000000338C000001000000FFFFFFFFFFFFFFFF0000000000348C000001000000FFFFFFFFFFFFFFFF0000000000358C000001000000FFFFFFFFFFFFFFFF0000000000368C000001000000FFFFFFFFFFFFFFFF0000000000378C000001000000FFFFFFFFFFFFFFFF0000000000388C000001000000FFFFFFFFFFFFFFFF0000000000398C000001000000FFFFFFFFFFFFFFFF00000000003A8C000001000000FFFFFFFFFFFFFFFF00000000003B8C000001000000FFFFFFFFFFFFFFFF00000000003C8C000001000000FFFFFFFFFFFFFFFF00000000003D8C000001000000FFFFFFFFFFFFFFFF00000000003E8C000001000000FFFFFFFFFFFFFFFF00000000003F8C000001000000FFFFFFFFFFFFFFFF0000000000408C000001000000FFFFFFFFFFFFFFFF0000000000418C000001000000FFFFFFFFFFFFFFFF000000000050C3000001000000FFFFFFFFFFFFFFFF000000000051C3000001000000FFFFFFFFFFFFFFFF000000000052C3000001000000FFFFFFFFFFFFFFFF000000000053C3000001000000FFFFFFFFFFFFFFFF000000000054C3000001000000FFFFFFFFFFFFFFFF000000000055C3000001000000FFFFFFFFFFFFFFFF000000000056C3000001000000FFFFFFFFFFFFFFFF000000000057C3000001000000FFFFFFFFFFFFFFFF000000000058C3000001000000FFFFFFFFFFFFFFFF000000000059C3000001000000FFFFFFFFFFFFFFFF00000000005AC3000001000000FFFFFFFFFFFFFFFF00000000005BC3000001000000FFFFFFFFFFFFFFFF00000000005CC3000001000000FFFFFFFFFFFFFFFF00000000005DC3000001000000FFFFFFFFFFFFFFFF00000000005EC3000001000000FFFFFFFFFFFFFFFF00000000005FC3000001000000FFFFFFFFFFFFFFFF000000000060C3000001000000FFFFFFFFFFFFFFFF000000000061C3000001000000FFFFFFFFFFFFFFFF000000000062C3000001000000FFFFFFFFFFFFFFFF000000000063C3000001000000FFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000001000000FFFFFFFFE205000001000000FFFFFFFFE2050000000000000010000001000000FFFFFFFFFFFFFFFFF00000004F000000F4000000C1040000010000000200001004000000010000000000000000000000FFFFFFFF05000000ED0300006D000000C3000000C4000000739400000180001000000100000000F6FFFFD1FFFFFFF0F6FFFFB2020000000000004F000000F0000000C10400000000000040410056050000000750726F6A65637401000000ED03000001000000FFFFFFFFFFFFFFFF05426F6F6B73010000006D00000001000000FFFFFFFFFFFFFFFF0946756E6374696F6E7301000000C300000001000000FFFFFFFFFFFFFFFF0954656D706C6174657301000000C400000001000000FFFFFFFFFFFFFFFF09526567697374657273000000007394000001000000FFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000001000000FFFFFFFFED03000001000000FFFFFFFFED030000000000000080000000000000FFFFFFFFFFFFFFFF00000000FD010000900500000102000000000000010000000400000001000000000000000000000000000000000000000000000001000000C6000000FFFFFFFF0F0000008F070000930700009407000095070000960700009007000091070000B5010000B801000038030000B9050000BA050000BB050000BC050000CB0900000180008000000000000000F6FFFF8301000090FBFFFF200200000000000001020000900500009E02000000000000404100460F0000001343616C6C20537461636B202B204C6F63616C73000000008F07000001000000FFFFFFFFFFFFFFFF0755415254202331000000009307000001000000FFFFFFFFFFFFFFFF0755415254202332000000009407000001000000FFFFFFFFFFFFFFFF0755415254202333000000009507000001000000FFFFFFFFFFFFFFFF15446562756720287072696E74662920566965776572000000009607000001000000FFFFFFFFFFFFFFFF0757617463682031000000009007000001000000FFFFFFFFFFFFFFFF0757617463682032000000009107000001000000FFFFFFFFFFFFFFFF10547261636520457863657074696F6E7300000000B501000001000000FFFFFFFFFFFFFFFF0E4576656E7420436F756E7465727300000000B801000001000000FFFFFFFFFFFFFFFF09554C494E4B706C7573000000003803000001000000FFFFFFFFFFFFFFFF084D656D6F7279203100000000B905000001000000FFFFFFFFFFFFFFFF084D656D6F7279203200000000BA05000001000000FFFFFFFFFFFFFFFF084D656D6F7279203300000000BB05000001000000FFFFFFFFFFFFFFFF084D656D6F7279203400000000BC05000001000000FFFFFFFFFFFFFFFF105472616365204E617669676174696F6E00000000CB09000001000000FFFFFFFFFFFFFFFFFFFFFFFF0000000001000000000000000000000001000000FFFFFFFFC802000001020000CC0200009E02000000000000020000000400000000000000000000000000000000000000000000000000000002000000C6000000FFFFFFFF8F07000001000000FFFFFFFF8F07000001000000C6000000000000000080000001000000FFFFFFFFFFFFFFFF00000000C1040000000A0000C5040000010000000100001004000000010000000000000000000000FFFFFFFF06000000C5000000C7000000B4010000D2010000CF010000779400000180008000000100000000F6FFFFB602000080FDFFFF3F03000000000000C5040000000A00004E0500000000000040820056060000000C4275696C64204F757470757401000000C500000001000000FFFFFFFFFFFFFFFF0D46696E6420496E2046696C657300000000C700000001000000FFFFFFFFFFFFFFFF0A4572726F72204C69737400000000B401000001000000FFFFFFFFFFFFFFFF0E536F757263652042726F7773657200000000D201000001000000FFFFFFFFFFFFFFFF0E416C6C205265666572656E63657300000000CF01000001000000FFFFFFFFFFFFFFFF0742726F77736572000000007794000001000000FFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000001000000FFFFFFFFC500000001000000FFFFFFFFC5000000000000000000000000000000
+ 000000000B000000000000000020000000000000FFFFFFFFFFFFFFFFF4000000D800000090050000DC000000000000000100000004000000010000000000000000000000FFFFFFFF08000000CB00000057010000CC000000F08B00005A01000079070000D601000045890000FFFF02000B004354616262656450616E650020000000000000F4F6FFFFD1FFFFFF90FBFFFF5A000000F40000004F00000090050000D80000000000000040280046080000000B446973617373656D626C7900000000CB00000001000000FFFFFFFFFFFFFFFF14506572666F726D616E636520416E616C797A6572000000005701000001000000FFFFFFFFFFFFFFFF14506572666F726D616E636520416E616C797A657200000000CC00000001000000FFFFFFFFFFFFFFFF0E4C6F67696320416E616C797A657200000000F08B000001000000FFFFFFFFFFFFFFFF0D436F646520436F766572616765000000005A01000001000000FFFFFFFFFFFFFFFF11496E737472756374696F6E205472616365000000007907000001000000FFFFFFFFFFFFFFFF0F53797374656D20416E616C797A657200000000D601000001000000FFFFFFFFFFFFFFFF104576656E742053746174697374696373000000004589000001000000FFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000001000000FFFFFFFFCB00000001000000FFFFFFFFCB000000000000000040000000000000FFFFFFFFFFFFFFFF9C0400004F000000A004000011020000000000000200000004000000010000000000000000000000FFFFFFFF2B000000E2050000CA0900002D8C00002E8C00002F8C0000308C0000318C0000328C0000338C0000348C0000358C0000368C0000378C0000388C0000398C00003A8C00003B8C00003C8C00003D8C00003E8C00003F8C0000408C0000418C000050C3000051C3000052C3000053C3000054C3000055C3000056C3000057C3000058C3000059C300005AC300005BC300005CC300005DC300005EC300005FC3000060C3000061C3000062C3000063C3000001800040000000000000A0FAFFFFD1FFFFFF90FBFFFF93010000A00400004F000000900500001102000000000000404100462B0000000753796D626F6C7300000000E205000001000000FFFFFFFFFFFFFFFF0A5472616365204461746100000000CA09000001000000FFFFFFFFFFFFFFFF00000000002D8C000001000000FFFFFFFFFFFFFFFF00000000002E8C000001000000FFFFFFFFFFFFFFFF00000000002F8C000001000000FFFFFFFFFFFFFFFF0000000000308C000001000000FFFFFFFFFFFFFFFF0000000000318C000001000000FFFFFFFFFFFFFFFF0000000000328C000001000000FFFFFFFFFFFFFFFF0000000000338C000001000000FFFFFFFFFFFFFFFF0000000000348C000001000000FFFFFFFFFFFFFFFF0000000000358C000001000000FFFFFFFFFFFFFFFF0000000000368C000001000000FFFFFFFFFFFFFFFF0000000000378C000001000000FFFFFFFFFFFFFFFF0000000000388C000001000000FFFFFFFFFFFFFFFF0000000000398C000001000000FFFFFFFFFFFFFFFF00000000003A8C000001000000FFFFFFFFFFFFFFFF00000000003B8C000001000000FFFFFFFFFFFFFFFF00000000003C8C000001000000FFFFFFFFFFFFFFFF00000000003D8C000001000000FFFFFFFFFFFFFFFF00000000003E8C000001000000FFFFFFFFFFFFFFFF00000000003F8C000001000000FFFFFFFFFFFFFFFF0000000000408C000001000000FFFFFFFFFFFFFFFF0000000000418C000001000000FFFFFFFFFFFFFFFF000000000050C3000001000000FFFFFFFFFFFFFFFF000000000051C3000001000000FFFFFFFFFFFFFFFF000000000052C3000001000000FFFFFFFFFFFFFFFF000000000053C3000001000000FFFFFFFFFFFFFFFF000000000054C3000001000000FFFFFFFFFFFFFFFF000000000055C3000001000000FFFFFFFFFFFFFFFF000000000056C3000001000000FFFFFFFFFFFFFFFF000000000057C3000001000000FFFFFFFFFFFFFFFF000000000058C3000001000000FFFFFFFFFFFFFFFF000000000059C3000001000000FFFFFFFFFFFFFFFF00000000005AC3000001000000FFFFFFFFFFFFFFFF00000000005BC3000001000000FFFFFFFFFFFFFFFF00000000005CC3000001000000FFFFFFFFFFFFFFFF00000000005DC3000001000000FFFFFFFFFFFFFFFF00000000005EC3000001000000FFFFFFFFFFFFFFFF00000000005FC3000001000000FFFFFFFFFFFFFFFF000000000060C3000001000000FFFFFFFFFFFFFFFF000000000061C3000001000000FFFFFFFFFFFFFFFF000000000062C3000001000000FFFFFFFFFFFFFFFF000000000063C3000001000000FFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000001000000FFFFFFFFE205000001000000FFFFFFFFE2050000000000000010000001000000FFFFFFFFFFFFFFFFF00000004F000000F4000000C1040000010000000200001004000000010000000000000000000000FFFFFFFF05000000ED0300006D000000C3000000C4000000739400000180001000000100000000F6FFFFD1FFFFFFF0F6FFFF43040000000000004F000000F0000000C10400000000000040410056050000000750726F6A65637401000000ED03000001000000FFFFFFFFFFFFFFFF05426F6F6B73010000006D00000001000000FFFFFFFFFFFFFFFF0946756E6374696F6E7301000000C300000001000000FFFFFFFFFFFFFFFF0954656D706C6174657301000000C400000001000000FFFFFFFFFFFFFFFF09526567697374657273000000007394000001000000FFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000001000000FFFFFFFFED03000001000000FFFFFFFFED030000000000000080000000000000FFFFFFFFFFFFFFFF00000000FD010000900500000102000000000000010000000400000001000000000000000000000000000000000000000000000001000000C6000000FFFFFFFF0F0000008F070000930700009407000095070000960700009007000091070000B5010000B801000038030000B9050000BA050000BB050000BC050000CB0900000180008000000000000000F6FFFF8301000090FBFFFF200200000000000001020000900500009E02000000000000404100460F0000001343616C6C20537461636B202B204C6F63616C73000000008F07000001000000FFFFFFFFFFFFFFFF0755415254202331000000009307000001000000FFFFFFFFFFFFFFFF0755415254202332000000009407000001000000FFFFFFFFFFFFFFFF0755415254202333000000009507000001000000FFFFFFFFFFFFFFFF15446562756720287072696E74662920566965776572000000009607000001000000FFFFFFFFFFFFFFFF0757617463682031000000009007000001000000FFFFFFFFFFFFFFFF0757617463682032000000009107000001000000FFFFFFFFFFFFFFFF10547261636520457863657074696F6E7300000000B501000001000000FFFFFFFFFFFFFFFF0E4576656E7420436F756E7465727300000000B801000001000000FFFFFFFFFFFFFFFF09554C494E4B706C7573000000003803000001000000FFFFFFFFFFFFFFFF084D656D6F7279203100000000B905000001000000FFFFFFFFFFFFFFFF084D656D6F7279203200000000BA05000001000000FFFFFFFFFFFFFFFF084D656D6F7279203300000000BB05000001000000FFFFFFFFFFFFFFFF084D656D6F7279203400000000BC05000001000000FFFFFFFFFFFFFFFF105472616365204E617669676174696F6E00000000CB09000001000000FFFFFFFFFFFFFFFFFFFFFFFF0000000001000000000000000000000001000000FFFFFFFFC802000001020000CC0200009E02000000000000020000000400000000000000000000000000000000000000000000000000000002000000C6000000FFFFFFFF8F07000001000000FFFFFFFF8F07000001000000C6000000000000000080000001000000FFFFFFFFFFFFFFFF00000000C1040000000A0000C5040000010000000100001004000000010000000000000000000000FFFFFFFF06000000C5000000C7000000B4010000D2010000CF010000779400000180008000000100000000F6FFFF4704000000000000D004000000000000C5040000000A00004E0500000000000040820056060000000C4275696C64204F757470757401000000C500000001000000FFFFFFFFFFFFFFFF0D46696E6420496E2046696C657300000000C700000001000000FFFFFFFFFFFFFFFF0A4572726F72204C69737400000000B401000001000000FFFFFFFFFFFFFFFF0E536F757263652042726F7773657200000000D201000001000000FFFFFFFFFFFFFFFF0E416C6C205265666572656E63657300000000CF01000001000000FFFFFFFFFFFFFFFF0742726F77736572000000007794000001000000FFFFFFFFFFFFFFFF00000000000000000000000000000000000000000000000001000000FFFFFFFFC500000001000000FFFFFFFFC5000000000000000000000000000000
59392
@@ -1884,7 +1884,7 @@
..\..\core\STM32_Modbus\Src\modbus_devid.c
3
- 331
+ 25
26
1
@@ -1893,8 +1893,8 @@
../Core/Src/main.c
0
- 304
- 1
+ 1
+ 46
1
0
diff --git a/new rev/john103C8T6/MDK-ARM/john103C8T6/john103C8T6.build_log.htm b/new rev/john103C8T6/MDK-ARM/john103C8T6/john103C8T6.build_log.htm
index a4d41dd..82aae55 100644
--- a/new rev/john103C8T6/MDK-ARM/john103C8T6/john103C8T6.build_log.htm
+++ b/new rev/john103C8T6/MDK-ARM/john103C8T6/john103C8T6.build_log.htm
@@ -21,7 +21,7 @@ Target DLL: UL2CM3.DLL V1.164.14.0
Dialog DLL: TCM.DLL V1.56.4.0
Project:
-F:\set\workspace\setcorp\set506\git_project\ds128b20\new rev\john103C8T6\MDK-ARM\john103C8T6.uvprojx
+F:\set\workspace\setcorp\set506\git_project\ds128b20\john_proj\ds18b20-MODBUS\new rev\john103C8T6\MDK-ARM\john103C8T6.uvprojx
Project File Date: 05/28/2026
Output:
@@ -49,7 +49,7 @@ Package Vendor: Keil
Collection of Component Files used:
* Component: ::CMSIS Driver:Flash(API)@2.3.0
-Build Time Elapsed: 00:00:00
+Build Time Elapsed: 00:00:01