добавил gui
This commit is contained in:
78
john103C6T6NewVer/MCU_PINS.md
Normal file
78
john103C6T6NewVer/MCU_PINS.md
Normal 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()` закомментированы.
|
||||
201
john103C6T6NewVer/MODBUS_MAP.md
Normal file
201
john103C6T6NewVer/MODBUS_MAP.md
Normal 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
105
john103C6T6NewVer/README.md
Normal 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
1640
john103C6T6NewVer/app.js
Normal 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);
|
||||
});
|
||||
72
john103C6T6NewVer/index.html
Normal file
72
john103C6T6NewVer/index.html
Normal 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>
|
||||
314
john103C6T6NewVer/mock_server.py
Normal file
314
john103C6T6NewVer/mock_server.py
Normal 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()
|
||||
1136
john103C6T6NewVer/serial_bridge.py
Normal file
1136
john103C6T6NewVer/serial_bridge.py
Normal 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()
|
||||
705
john103C6T6NewVer/server.err.log
Normal file
705
john103C6T6NewVer/server.err.log
Normal 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 -
|
||||
0
john103C6T6NewVer/server.out.log
Normal file
0
john103C6T6NewVer/server.out.log
Normal file
1
john103C6T6NewVer/server.pid
Normal file
1
john103C6T6NewVer/server.pid
Normal file
@@ -0,0 +1 @@
|
||||
23976
|
||||
1
john103C6T6NewVer/server.port
Normal file
1
john103C6T6NewVer/server.port
Normal file
@@ -0,0 +1 @@
|
||||
8090
|
||||
1
john103C6T6NewVer/server.url
Normal file
1
john103C6T6NewVer/server.url
Normal file
@@ -0,0 +1 @@
|
||||
http://127.0.0.1:8090
|
||||
78
john103C6T6NewVer/start_server.bat
Normal file
78
john103C6T6NewVer/start_server.bat
Normal 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
|
||||
30
john103C6T6NewVer/stop_server.bat
Normal file
30
john103C6T6NewVer/stop_server.bat
Normal 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
1145
john103C6T6NewVer/styles.css
Normal 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* USER CODE BEGIN Header */
|
||||
/* USER CODE BEGIN Header */
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file : main.c
|
||||
@@ -434,9 +434,18 @@ void reinit_t_sens(void)
|
||||
// sens[i].Init.InitParam.ROM = rom_address;
|
||||
sens[i].Init.InitParam.Ind = i;
|
||||
sens[i].Init.init_func = &Dallas_SensorInitByInd;
|
||||
sens[i].Init.Resolution = DALLAS_CONFIG_9_BITS;
|
||||
MB_DATA.HoldRegs.set_Temp[i] = sens[i].set_temp = 20.;
|
||||
MB_DATA.HoldRegs.set_hyst[i] = sens[i].hyst = 1;
|
||||
sens[i].Init.Resolution = DALLAS_CONFIG_9_BITS; sens[i].set_temp = 20.;
|
||||
sens[i].hyst = 1;
|
||||
MB_DATA.HoldRegs.set_Temp[i] = (uint16_t)(sens[i].set_temp * MB_ROOM_TEMP_SCALE);
|
||||
MB_DATA.HoldRegs.set_hyst[i] = (uint16_t)(sens[i].hyst * MB_ROOM_TEMP_SCALE);
|
||||
MB_DATA.HoldRegs.room_cfg[i].setpoint_x10 = MB_DATA.HoldRegs.set_Temp[i];
|
||||
MB_DATA.HoldRegs.room_cfg[i].hysteresis_x10 = MB_DATA.HoldRegs.set_hyst[i];
|
||||
MB_DATA.HoldRegs.room_cfg[i].valve_position_pct = 0;
|
||||
MB_DATA.HoldRegs.room_cfg[i].valve_angle_max_deg = MB_ROOM_VALVE_ANGLE_MAX_DEFAULT;
|
||||
MB_DATA.HoldRegs.room_cfg[i].mode = ROOM_MODE_AUTO;
|
||||
MB_DATA.HoldRegs.room_cfg[i].command = ROOM_COMMAND_STOP;
|
||||
MB_DATA.HoldRegs.room_cfg[i].location = 1;
|
||||
MB_DATA.HoldRegs.room_cfg[i].apply = 0;
|
||||
Dallas_AddNewSensors(&hdallas, &sens[i]);
|
||||
|
||||
}
|
||||
@@ -503,6 +512,70 @@ FuncStat packStruct(MB_DataStructureTypeDef* MB_DATA, int sizeARR)
|
||||
return FuncOK;
|
||||
|
||||
}
|
||||
|
||||
static uint16_t clamp_room_percent(uint16_t value)
|
||||
{
|
||||
return value > 100U ? 100U : value;
|
||||
}
|
||||
|
||||
static void update_room_modbus(MB_DataStructureTypeDef* MB_DATA)
|
||||
{
|
||||
for (int i = 0; i < MAX_SENSE; i++)
|
||||
{
|
||||
MB_RoomInputRegsTypeDef* room = &MB_DATA->InRegs.room[i];
|
||||
MB_RoomHoldingRegsTypeDef* cfg = &MB_DATA->HoldRegs.room_cfg[i];
|
||||
uint16_t open_state = 0U;
|
||||
uint16_t close_state = 0U;
|
||||
uint16_t position_pct;
|
||||
uint16_t angle_max;
|
||||
|
||||
if (cfg->valve_angle_max_deg == 0U)
|
||||
{
|
||||
cfg->valve_angle_max_deg = MB_ROOM_VALVE_ANGLE_MAX_DEFAULT;
|
||||
}
|
||||
|
||||
if (i < 16)
|
||||
{
|
||||
open_state = (MB_DATA->Coils.relay_struct_on.all >> i) & 0x1U;
|
||||
close_state = (MB_DATA->Coils.relay_struct_off.all >> i) & 0x1U;
|
||||
}
|
||||
|
||||
position_pct = clamp_room_percent(cfg->valve_position_pct);
|
||||
if (cfg->mode == ROOM_MODE_AUTO)
|
||||
{
|
||||
if (open_state)
|
||||
{
|
||||
position_pct = 100U;
|
||||
}
|
||||
else if (close_state)
|
||||
{
|
||||
position_pct = 0U;
|
||||
}
|
||||
}
|
||||
cfg->valve_position_pct = position_pct;
|
||||
angle_max = cfg->valve_angle_max_deg;
|
||||
|
||||
room->channel = (uint16_t)(i + 1);
|
||||
room->location = cfg->location;
|
||||
for (int reg = 0; reg < 4; reg++)
|
||||
{
|
||||
room->ds18b20_id[reg] = ((uint16_t)MB_DATA->InRegs.ID.DevAddr[i][reg * 2]) |
|
||||
((uint16_t)MB_DATA->InRegs.ID.DevAddr[i][reg * 2 + 1] << 8);
|
||||
}
|
||||
room->temperature_x10 = (i < hdallas.onewire->RomCnt) ? (uint16_t)((int16_t)(sens[i].temperature * MB_ROOM_TEMP_SCALE)) : 0U;
|
||||
room->setpoint_x10 = MB_DATA->HoldRegs.set_Temp[i];
|
||||
room->hysteresis_x10 = MB_DATA->HoldRegs.set_hyst[i];
|
||||
room->valve_position_pct = position_pct;
|
||||
room->valve_angle_deg = (uint16_t)((position_pct * angle_max) / 100U);
|
||||
room->valve_angle_max_deg = angle_max;
|
||||
room->is_connected = (i < hdallas.onewire->RomCnt) ? (uint16_t)sens[i].isConnected : 0U;
|
||||
room->valve_open = open_state;
|
||||
room->valve_close = close_state;
|
||||
room->mode = cfg->mode;
|
||||
room->command_state = cfg->command;
|
||||
room->reserved = 0U;
|
||||
}
|
||||
}
|
||||
FuncStat Field_modbus(MB_DataStructureTypeDef* MB_DATA, Flags_TypeDef* flag)
|
||||
{
|
||||
|
||||
@@ -516,15 +589,23 @@ FuncStat Field_modbus(MB_DataStructureTypeDef* MB_DATA, Flags_TypeDef* flag)
|
||||
|
||||
MB_DATA->Coils.init_param = 0;
|
||||
for(int i = 0; i < hdallas.onewire->RomCnt; i++)
|
||||
{
|
||||
sens[i].set_temp = MB_DATA->HoldRegs.set_Temp[i];
|
||||
sens[i].hyst = MB_DATA->HoldRegs.set_hyst[i];
|
||||
{ if (MB_DATA->HoldRegs.room_cfg[i].apply)
|
||||
{
|
||||
MB_DATA->HoldRegs.set_Temp[i] = MB_DATA->HoldRegs.room_cfg[i].setpoint_x10;
|
||||
MB_DATA->HoldRegs.set_hyst[i] = MB_DATA->HoldRegs.room_cfg[i].hysteresis_x10;
|
||||
MB_DATA->HoldRegs.room_cfg[i].apply = 0U;
|
||||
}
|
||||
MB_DATA->HoldRegs.room_cfg[i].setpoint_x10 = MB_DATA->HoldRegs.set_Temp[i];
|
||||
MB_DATA->HoldRegs.room_cfg[i].hysteresis_x10 = MB_DATA->HoldRegs.set_hyst[i];
|
||||
sens[i].set_temp = ((float)MB_DATA->HoldRegs.set_Temp[i]) / MB_ROOM_TEMP_SCALE;
|
||||
sens[i].hyst = ((float)MB_DATA->HoldRegs.set_hyst[i]) / MB_ROOM_TEMP_SCALE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
update_room_modbus(MB_DATA);
|
||||
return FuncOK;
|
||||
};
|
||||
|
||||
@@ -534,6 +615,31 @@ FuncStat value_control(void )
|
||||
|
||||
for(int i = 0; i < hdallas.onewire->RomCnt; i++)
|
||||
{
|
||||
if (i < 16 && MB_DATA.HoldRegs.room_cfg[i].mode == ROOM_MODE_MANUAL)
|
||||
{
|
||||
uint16_t manual_pct = clamp_room_percent(MB_DATA.HoldRegs.room_cfg[i].valve_position_pct);
|
||||
if (MB_DATA.HoldRegs.room_cfg[i].command == ROOM_COMMAND_CLOSE)
|
||||
{
|
||||
manual_pct = 0U;
|
||||
}
|
||||
else if (MB_DATA.HoldRegs.room_cfg[i].command == ROOM_COMMAND_OPEN && manual_pct == 0U)
|
||||
{
|
||||
manual_pct = 100U;
|
||||
}
|
||||
MB_DATA.HoldRegs.room_cfg[i].valve_position_pct = manual_pct;
|
||||
if (manual_pct > 0U)
|
||||
{
|
||||
MB_DATA.Coils.relay_struct_off.all &= ~(1 << i);
|
||||
MB_DATA.Coils.relay_struct_on.all |= 1 << i;
|
||||
}
|
||||
else
|
||||
{
|
||||
MB_DATA.Coils.relay_struct_on.all &= ~(1 << i);
|
||||
MB_DATA.Coils.relay_struct_off.all |= 1 << i;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sens[i].temperature < sens[i].set_temp - sens[i].hyst)
|
||||
|
||||
{
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -21,7 +21,7 @@ Target DLL: UL2CM3.DLL V1.164.14.0
|
||||
Dialog DLL: TCM.DLL V1.56.4.0
|
||||
|
||||
<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
|
||||
|
||||
<h2>Output:</h2>
|
||||
@@ -49,7 +49,7 @@ Package Vendor: Keil
|
||||
<h2>Collection of Component Files used:</h2>
|
||||
|
||||
* Component: ::CMSIS Driver:Flash(API)@2.3.0
|
||||
Build Time Elapsed: 00:00:00
|
||||
Build Time Elapsed: 00:00:01
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
**************************************************************************
|
||||
* @file modbus_data.h
|
||||
* @brief Заголовочный файл с описанием даты MODBUS.
|
||||
@@ -105,6 +105,21 @@
|
||||
*
|
||||
*/
|
||||
#define mb_fill_rsv(_align_, _struct_) ((_align_ > mb_sizeof(_struct_)) ? (_align_ - mb_sizeof(_struct_)) : 0)
|
||||
#define mb_fill_gap(_from_, _to_, _struct_) (((_to_) > ((_from_) + mb_sizeof(_struct_))) ? ((_to_) - (_from_) - mb_sizeof(_struct_)) : 0)
|
||||
|
||||
#define R_INPUT_ROOM_ADDR 400
|
||||
#define R_INPUT_ID_ADDR 1000
|
||||
#define R_INPUT_NUM_TSENS_ADDR 1200
|
||||
#define R_HOLDING_HYST_ADDR 100
|
||||
#define R_HOLDING_ROOM_ADDR 300
|
||||
|
||||
#define MB_ROOM_TEMP_SCALE 10U
|
||||
#define MB_ROOM_VALVE_ANGLE_MAX_DEFAULT 90U
|
||||
#define ROOM_MODE_AUTO 0U
|
||||
#define ROOM_MODE_MANUAL 1U
|
||||
#define ROOM_COMMAND_STOP 0U
|
||||
#define ROOM_COMMAND_OPEN 1U
|
||||
#define ROOM_COMMAND_CLOSE 2U
|
||||
|
||||
|
||||
|
||||
@@ -124,6 +139,38 @@ typedef __PACKED_STRUCT
|
||||
uint16_t status;
|
||||
} MB_RtcCalendarRegsTypeDef;
|
||||
|
||||
typedef __PACKED_STRUCT
|
||||
{
|
||||
uint16_t channel;
|
||||
uint16_t location;
|
||||
uint16_t ds18b20_id[4];
|
||||
uint16_t temperature_x10;
|
||||
uint16_t setpoint_x10;
|
||||
uint16_t hysteresis_x10;
|
||||
uint16_t valve_position_pct;
|
||||
uint16_t valve_angle_deg;
|
||||
uint16_t valve_angle_max_deg;
|
||||
uint16_t is_connected;
|
||||
uint16_t valve_open;
|
||||
uint16_t valve_close;
|
||||
uint16_t mode;
|
||||
uint16_t command_state;
|
||||
uint16_t reserved;
|
||||
} MB_RoomInputRegsTypeDef;
|
||||
|
||||
|
||||
typedef __PACKED_STRUCT
|
||||
{
|
||||
uint16_t setpoint_x10;
|
||||
uint16_t hysteresis_x10;
|
||||
uint16_t valve_position_pct;
|
||||
uint16_t valve_angle_max_deg;
|
||||
uint16_t mode;
|
||||
uint16_t command;
|
||||
uint16_t location;
|
||||
uint16_t apply;
|
||||
} MB_RoomHoldingRegsTypeDef;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -132,9 +179,11 @@ typedef __PACKED_STRUCT//MB_DataInRegsTypeDef
|
||||
{
|
||||
|
||||
uint16_t sens_Temp[MAX_SENSE];
|
||||
uint16_t reserve[mb_fill_rsv(1000, uint16_t[MAX_SENSE])];
|
||||
uint16_t reserve_to_room[mb_fill_rsv(R_INPUT_ROOM_ADDR, uint16_t[MAX_SENSE])];
|
||||
MB_RoomInputRegsTypeDef room[MAX_SENSE];
|
||||
uint16_t reserve_to_id[mb_fill_gap(R_INPUT_ROOM_ADDR, R_INPUT_ID_ADDR, MB_RoomInputRegsTypeDef[MAX_SENSE])];
|
||||
DS18B20_Drv_t ID;
|
||||
uint16_t reserve1[mb_fill_rsv(200, DS18B20_Drv_t)];
|
||||
uint16_t reserve_to_num_tsens[mb_fill_gap(R_INPUT_ID_ADDR, R_INPUT_NUM_TSENS_ADDR, DS18B20_Drv_t)];
|
||||
uint16_t num_Tsens;
|
||||
MB_RtcCalendarRegsTypeDef rtc;
|
||||
|
||||
@@ -148,10 +197,12 @@ typedef __PACKED_STRUCT//MB_DataInRegsTypeDef
|
||||
typedef __PACKED_STRUCT //MB_DataInRegsTypeDef
|
||||
{
|
||||
uint16_t set_Temp[MAX_SENSE];
|
||||
uint16_t reserve[mb_fill_rsv(100, uint16_t[MAX_SENSE])];
|
||||
uint16_t reserve_to_hyst[mb_fill_rsv(R_HOLDING_HYST_ADDR, uint16_t[MAX_SENSE])];
|
||||
uint16_t set_hyst[MAX_SENSE];
|
||||
uint16_t reserve1[mb_fill_rsv(100, uint16_t[MAX_SENSE])];
|
||||
uint16_t reserve_to_rtc[mb_fill_gap(R_HOLDING_HYST_ADDR, R_HOLDING_RTC_ADDR, uint16_t[MAX_SENSE])];
|
||||
MB_RtcCalendarRegsTypeDef rtc;
|
||||
uint16_t reserve_to_room_cfg[mb_fill_gap(R_HOLDING_RTC_ADDR, R_HOLDING_ROOM_ADDR, MB_RtcCalendarRegsTypeDef)];
|
||||
MB_RoomHoldingRegsTypeDef room_cfg[MAX_SENSE];
|
||||
} MB_DataHoldRegsTypeDef;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user