добавил gui

This commit is contained in:
2026-06-25 17:25:41 +03:00
parent cdd8fc3f71
commit 41a50a1d1e
19 changed files with 5696 additions and 32 deletions

View File

@@ -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()` закомментированы.

View File

@@ -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.

105
john103C6T6NewVer/README.md Normal file
View File

@@ -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-порта

1640
john103C6T6NewVer/app.js Normal file
View File

@@ -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 = "<option value=\"\">Порт не выбран</option>";
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) => (
`<option value="${name}" ${name === location ? "selected" : ""}>${name}</option>`
)).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 = `
<div class="channel-head">
<div class="channel-title">
<strong>Канал ${channelNumber}</strong>
<small>${sensorName} · ${location} · зона ${sensor.zone || channelNumber}</small>
</div>
<span>связь <i class="channel-lamp ${connected ? "on" : "alarm"}"></i></span>
</div>
<div class="channel-id-grid full-channel-id-grid">
<label>уставка
<input class="targetTemp" type="number" step="0.5" value="${setpoint}">
</label>
<label>Расположение
<select class="sensorLocation" data-id="${sensorId}">
${locationOptions}
</select>
</label>
<label>ID DS18B20
<input readonly value="${ds18b20Id}">
</label>
</div>
<div class="channel-body full-channel-body">
<div class="temperature-widget" aria-label="Температура канала ${channelNumber}">
<div class="temp-scale">
<span>50</span><span>40</span><span>30</span><span>20</span><span>10</span><span>0</span><span>-5</span>
</div>
<div class="temp-bar"><b style="height: ${tempFill}%"></b></div>
<div class="temp-now">${tempText}°C</div>
</div>
<div class="channel-workarea">
<div class="top-metrics-row">
<div class="angle-panel">
<span>угол открытия</span>
<strong>${openDegrees}°</strong>
<small>максимум ${Math.round(maxDegrees)}°</small>
</div>
</div>
<div class="channel-data-grid full-channel-data-grid">
<div><span>температура</span><strong>${tempText}°C</strong></div>
<div><span>уставка</span><strong>${setpointText}°C</strong></div>
<div><span>отклонение</span><strong class="delta ${deltaClass}">${deltaText}</strong></div>
<div><span>расположение</span><strong>${location}</strong></div>
<div><span>ID DS18B20</span><strong>${ds18b20Id}</strong></div>
<div><span>режим</span><strong>${modeText}</strong></div>
<div><span>угол/max</span><strong>${openDegrees}° / ${Math.round(maxDegrees)}°</strong></div>
<div><span>команда</span><strong>${stateText}</strong></div>
<div><span>связь</span><strong>${connected ? "есть" : "нет"}</strong></div>
<div><span>канал</span><strong>${channelNumber}</strong></div>
</div>
<label class="range-control channel-position">положение заслонки
<input class="position" type="range" min="0" max="100" value="${position}">
<strong>${position}%</strong>
</label>
<div class="valve-state-row full-state-row">
<span><i class="channel-lamp ${openActive ? "on" : "off"}"></i> клапан откр ${channelNumber}</span>
<span><i class="channel-lamp ${closeActive ? "on" : "off"}"></i> клапан закр ${channelNumber}</span>
</div>
<div class="channel-actions">
<button class="quickPosition" data-position="100" type="button">откр ${channelNumber}</button>
<button class="quickPosition" data-position="0" type="button">закр ${channelNumber}</button>
</div>
<div class="channel-actions channel-mode-line">
<div class="toggle channel-mode">
<button class="modeBtn valveAuto ${mode === "auto" ? "active" : ""}" data-mode="auto" type="button">авто</button>
<button class="modeBtn valveManual ${mode === "manual" ? "active" : ""}" data-mode="manual" type="button">ручное</button>
</div>
<button class="applyTarget mini-btn" type="button">SP</button>
<button class="applyManual mini-btn" type="button">OK</button>
<button class="calibrateValve mini-btn" type="button">CAL</button>
</div>
</div>
</div>
`;
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);
});

View File

@@ -0,0 +1,72 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Тепло и клапаны — управление</title>
<link rel="stylesheet" href="styles.css?v=20260625-ds18b20-id-1" />
</head>
<body>
<div class="shell">
<header class="header">
<h1>Панель температуры и клапанов</h1>
<p>Мониторинг датчиков, управление уставками и ручным/авто управлением клапанов.</p>
</header>
<section class="panel">
<label for="apiBase">API endpoint</label>
<div class="api-row">
<input id="apiBase" placeholder="http://127.0.0.1:1923 (пусто = демо)" value="" />
<button id="refreshBtn">Обновить</button>
<button id="saveApiBtn">Сохранить адрес API</button>
<span id="status" class="status">Режим: не подключен</span>
</div>
<p class="hint">Если API не задан, интерфейс работает в автономном демо-режиме с локальным хранением.</p>
<div class="serial-block">
<div class="serial-row">
<label for="modbusTransport">Обмен</label>
<select id="modbusTransport">
<option value="rtu">Modbus RTU / COM</option>
<option value="tcp">Modbus TCP / IP</option>
</select>
<label for="modbusSlaveId">Slave ID</label>
<input id="modbusSlaveId" type="number" min="1" max="247" value="3">
</select>
<label class="rtu-field" for="comPortSelect">COM порт</label>
<select id="comPortSelect" class="rtu-field">
<option value="">Порт не выбран</option>
</select>
<button id="refreshPortsBtn" class="rtu-field">Обновить порты</button>
<label class="tcp-field hidden" for="tcpHost">IP</label>
<input id="tcpHost" class="tcp-field hidden" type="text" value="192.168.0.10" placeholder="192.168.0.10">
<label class="tcp-field hidden" for="tcpPort">TCP порт</label>
<input id="tcpPort" class="tcp-field hidden" type="number" min="1" max="65535" value="502">
<button id="connectPortBtn">Подключить</button>
<button id="calibrateAllBtn">Калибровать все</button>
<span id="serialStatus" class="status status-warn">COM: не подключён</span>
</div>
</div>
</section>
<section class="panel grid">
<article class="card">
<h2>Датчики температуры</h2>
<div id="sensors" class="list"></div>
</article>
<article class="card">
<h2>Клапаны</h2>
<div id="valves" class="list"></div>
</article>
</section>
<footer class="footer">
<span>Статус: </span>
<span id="globalStatus">Ожидание...</span>
</footer>
</div>
<script src="app.js?v=20260625-setpoint-draft-1"></script>
</body>
</html>

View File

@@ -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()

View File

@@ -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()

View File

@@ -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 -

View File

View File

@@ -0,0 +1 @@
23976

View File

@@ -0,0 +1 @@
8090

View File

@@ -0,0 +1 @@
http://127.0.0.1:8090

View File

@@ -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

View File

@@ -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

1145
john103C6T6NewVer/styles.css Normal file
View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
/* USER CODE BEGIN Header */ /* USER CODE BEGIN Header */
/** /**
****************************************************************************** ******************************************************************************
* @file : main.c * @file : main.c
@@ -434,9 +434,18 @@ void reinit_t_sens(void)
// sens[i].Init.InitParam.ROM = rom_address; // sens[i].Init.InitParam.ROM = rom_address;
sens[i].Init.InitParam.Ind = i; sens[i].Init.InitParam.Ind = i;
sens[i].Init.init_func = &Dallas_SensorInitByInd; sens[i].Init.init_func = &Dallas_SensorInitByInd;
sens[i].Init.Resolution = DALLAS_CONFIG_9_BITS; sens[i].Init.Resolution = DALLAS_CONFIG_9_BITS; sens[i].set_temp = 20.;
MB_DATA.HoldRegs.set_Temp[i] = sens[i].set_temp = 20.; sens[i].hyst = 1;
MB_DATA.HoldRegs.set_hyst[i] = 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]); Dallas_AddNewSensors(&hdallas, &sens[i]);
} }
@@ -503,6 +512,70 @@ FuncStat packStruct(MB_DataStructureTypeDef* MB_DATA, int sizeARR)
return FuncOK; 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) 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; MB_DATA->Coils.init_param = 0;
for(int i = 0; i < hdallas.onewire->RomCnt; i++) for(int i = 0; i < hdallas.onewire->RomCnt; i++)
{ if (MB_DATA->HoldRegs.room_cfg[i].apply)
{ {
sens[i].set_temp = MB_DATA->HoldRegs.set_Temp[i]; MB_DATA->HoldRegs.set_Temp[i] = MB_DATA->HoldRegs.room_cfg[i].setpoint_x10;
sens[i].hyst = MB_DATA->HoldRegs.set_hyst[i]; 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; return FuncOK;
}; };
@@ -534,6 +615,31 @@ FuncStat value_control(void )
for(int i = 0; i < hdallas.onewire->RomCnt; i++) 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) if (sens[i].temperature < sens[i].set_temp - sens[i].hyst)
{ {

File diff suppressed because one or more lines are too long

View File

@@ -21,7 +21,7 @@ Target DLL: UL2CM3.DLL V1.164.14.0
Dialog DLL: TCM.DLL V1.56.4.0 Dialog DLL: TCM.DLL V1.56.4.0
<h2>Project:</h2> <h2>Project:</h2>
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 Project File Date: 05/28/2026
<h2>Output:</h2> <h2>Output:</h2>
@@ -49,7 +49,7 @@ Package Vendor: Keil
<h2>Collection of Component Files used:</h2> <h2>Collection of Component Files used:</h2>
* Component: ::CMSIS Driver:Flash(API)@2.3.0 * Component: ::CMSIS Driver:Flash(API)@2.3.0
Build Time Elapsed: 00:00:00 Build Time Elapsed: 00:00:01
</pre> </pre>
</body> </body>
</html> </html>

View File

@@ -1,4 +1,4 @@
/** /**
************************************************************************** **************************************************************************
* @file modbus_data.h * @file modbus_data.h
* @brief Заголовочный файл с описанием даты MODBUS. * @brief Заголовочный файл с описанием даты MODBUS.
@@ -105,6 +105,21 @@
* *
*/ */
#define mb_fill_rsv(_align_, _struct_) ((_align_ > mb_sizeof(_struct_)) ? (_align_ - mb_sizeof(_struct_)) : 0) #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; uint16_t status;
} MB_RtcCalendarRegsTypeDef; } 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 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; 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; uint16_t num_Tsens;
MB_RtcCalendarRegsTypeDef rtc; MB_RtcCalendarRegsTypeDef rtc;
@@ -148,10 +197,12 @@ typedef __PACKED_STRUCT//MB_DataInRegsTypeDef
typedef __PACKED_STRUCT //MB_DataInRegsTypeDef typedef __PACKED_STRUCT //MB_DataInRegsTypeDef
{ {
uint16_t set_Temp[MAX_SENSE]; 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 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; 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; } MB_DataHoldRegsTypeDef;