From 41a50a1d1e1610c00b29c395130605c674577aa9 Mon Sep 17 00:00:00 2001 From: andrey Date: Thu, 25 Jun 2026 17:25:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20gu?= =?UTF-8?q?i?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- john103C6T6NewVer/MCU_PINS.md | 78 + john103C6T6NewVer/MODBUS_MAP.md | 201 ++ john103C6T6NewVer/README.md | 105 ++ john103C6T6NewVer/app.js | 1640 +++++++++++++++++ john103C6T6NewVer/index.html | 72 + john103C6T6NewVer/mock_server.py | 314 ++++ john103C6T6NewVer/serial_bridge.py | 1136 ++++++++++++ john103C6T6NewVer/server.err.log | 705 +++++++ john103C6T6NewVer/server.out.log | 0 john103C6T6NewVer/server.pid | 1 + john103C6T6NewVer/server.port | 1 + john103C6T6NewVer/server.url | 1 + john103C6T6NewVer/start_server.bat | 78 + john103C6T6NewVer/stop_server.bat | 30 + john103C6T6NewVer/styles.css | 1145 ++++++++++++ new rev/john103C8T6/Core/Src/main.c | 120 +- .../john103C8T6/MDK-ARM/john103C8T6.uvguix.z | 36 +- .../john103C8T6/john103C8T6.build_log.htm | 4 +- new rev/john103C8T6/Modbus/modbus_data.h | 61 +- 19 files changed, 5696 insertions(+), 32 deletions(-) create mode 100644 john103C6T6NewVer/MCU_PINS.md create mode 100644 john103C6T6NewVer/MODBUS_MAP.md create mode 100644 john103C6T6NewVer/README.md create mode 100644 john103C6T6NewVer/app.js create mode 100644 john103C6T6NewVer/index.html create mode 100644 john103C6T6NewVer/mock_server.py create mode 100644 john103C6T6NewVer/serial_bridge.py create mode 100644 john103C6T6NewVer/server.err.log create mode 100644 john103C6T6NewVer/server.out.log create mode 100644 john103C6T6NewVer/server.pid create mode 100644 john103C6T6NewVer/server.port create mode 100644 john103C6T6NewVer/server.url create mode 100644 john103C6T6NewVer/start_server.bat create mode 100644 john103C6T6NewVer/stop_server.bat create mode 100644 john103C6T6NewVer/styles.css 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} +
+ связь +
+ +
+ + + +
+ +
+
+
+ 50403020100-5 +
+
+
${tempText}°C
+
+ +
+
+
+ угол открытия + ${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 @@ + + + + + + Тепло и клапаны — управление + + + +
+
+

Панель температуры и клапанов

+

Мониторинг датчиков, управление уставками и ручным/авто управлением клапанов.

+
+ +
+ +
+ + + + Режим: не подключен +
+

Если API не задан, интерфейс работает в автономном демо-режиме с локальным хранением.

+
+
+ + + + + + + + + + + + + + + + COM: не подключён +
+
+
+ +
+
+

Датчики температуры

+
+
+ +
+

Клапаны

+
+
+
+ +
+ Статус: + Ожидание... +
+
+ + + + 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 diff --git a/new rev/john103C8T6/Modbus/modbus_data.h b/new rev/john103C8T6/Modbus/modbus_data.h index 23717c1..9162337 100644 --- a/new rev/john103C8T6/Modbus/modbus_data.h +++ b/new rev/john103C8T6/Modbus/modbus_data.h @@ -1,4 +1,4 @@ -/** +/** ************************************************************************** * @file modbus_data.h * @brief Заголовочный файл с описанием даты MODBUS. @@ -105,6 +105,21 @@ * */ #define mb_fill_rsv(_align_, _struct_) ((_align_ > mb_sizeof(_struct_)) ? (_align_ - mb_sizeof(_struct_)) : 0) +#define mb_fill_gap(_from_, _to_, _struct_) (((_to_) > ((_from_) + mb_sizeof(_struct_))) ? ((_to_) - (_from_) - mb_sizeof(_struct_)) : 0) + +#define R_INPUT_ROOM_ADDR 400 +#define R_INPUT_ID_ADDR 1000 +#define R_INPUT_NUM_TSENS_ADDR 1200 +#define R_HOLDING_HYST_ADDR 100 +#define R_HOLDING_ROOM_ADDR 300 + +#define MB_ROOM_TEMP_SCALE 10U +#define MB_ROOM_VALVE_ANGLE_MAX_DEFAULT 90U +#define ROOM_MODE_AUTO 0U +#define ROOM_MODE_MANUAL 1U +#define ROOM_COMMAND_STOP 0U +#define ROOM_COMMAND_OPEN 1U +#define ROOM_COMMAND_CLOSE 2U @@ -124,6 +139,38 @@ typedef __PACKED_STRUCT uint16_t status; } MB_RtcCalendarRegsTypeDef; +typedef __PACKED_STRUCT +{ + uint16_t channel; + uint16_t location; + uint16_t ds18b20_id[4]; + uint16_t temperature_x10; + uint16_t setpoint_x10; + uint16_t hysteresis_x10; + uint16_t valve_position_pct; + uint16_t valve_angle_deg; + uint16_t valve_angle_max_deg; + uint16_t is_connected; + uint16_t valve_open; + uint16_t valve_close; + uint16_t mode; + uint16_t command_state; + uint16_t reserved; +} MB_RoomInputRegsTypeDef; + + +typedef __PACKED_STRUCT +{ + uint16_t setpoint_x10; + uint16_t hysteresis_x10; + uint16_t valve_position_pct; + uint16_t valve_angle_max_deg; + uint16_t mode; + uint16_t command; + uint16_t location; + uint16_t apply; +} MB_RoomHoldingRegsTypeDef; + @@ -132,9 +179,11 @@ typedef __PACKED_STRUCT//MB_DataInRegsTypeDef { uint16_t sens_Temp[MAX_SENSE]; - uint16_t reserve[mb_fill_rsv(1000, uint16_t[MAX_SENSE])]; + uint16_t reserve_to_room[mb_fill_rsv(R_INPUT_ROOM_ADDR, uint16_t[MAX_SENSE])]; + MB_RoomInputRegsTypeDef room[MAX_SENSE]; + uint16_t reserve_to_id[mb_fill_gap(R_INPUT_ROOM_ADDR, R_INPUT_ID_ADDR, MB_RoomInputRegsTypeDef[MAX_SENSE])]; DS18B20_Drv_t ID; - uint16_t reserve1[mb_fill_rsv(200, DS18B20_Drv_t)]; + uint16_t reserve_to_num_tsens[mb_fill_gap(R_INPUT_ID_ADDR, R_INPUT_NUM_TSENS_ADDR, DS18B20_Drv_t)]; uint16_t num_Tsens; MB_RtcCalendarRegsTypeDef rtc; @@ -148,10 +197,12 @@ typedef __PACKED_STRUCT//MB_DataInRegsTypeDef typedef __PACKED_STRUCT //MB_DataInRegsTypeDef { uint16_t set_Temp[MAX_SENSE]; - uint16_t reserve[mb_fill_rsv(100, uint16_t[MAX_SENSE])]; + uint16_t reserve_to_hyst[mb_fill_rsv(R_HOLDING_HYST_ADDR, uint16_t[MAX_SENSE])]; uint16_t set_hyst[MAX_SENSE]; - uint16_t reserve1[mb_fill_rsv(100, uint16_t[MAX_SENSE])]; + uint16_t reserve_to_rtc[mb_fill_gap(R_HOLDING_HYST_ADDR, R_HOLDING_RTC_ADDR, uint16_t[MAX_SENSE])]; MB_RtcCalendarRegsTypeDef rtc; + uint16_t reserve_to_room_cfg[mb_fill_gap(R_HOLDING_RTC_ADDR, R_HOLDING_ROOM_ADDR, MB_RtcCalendarRegsTypeDef)]; + MB_RoomHoldingRegsTypeDef room_cfg[MAX_SENSE]; } MB_DataHoldRegsTypeDef;