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