Русская версия. English version: README.en.md
Связанные документы:
- RELEASE_NOTES.md
- CHANGELOG.md
- RELEASE_PAGE.md
- RELEASE_NOTES.en.md
- CHANGELOG.en.md
- RELEASE_PAGE.en.md
- LICENSE
- NOTICE
PSTG это MVP-проект асинхронного Modbus TCP polling-сервиса для сбора телеметрии, диагностики связи и локального мониторинга станций.
Проект разрабатывался специалистом сервисного отдела ООО "ЧЗМЭК" (ООО "Челябинский завод мобильных энергоустановок и конструкций") для личного использования: как рабочий инструмент, который упрощает разработку, отладку и сопровождение мониторинга станций без постоянной зависимости от "железного" оборудования, PLC, RTU, Modbus gateway и стендов.
Проект ориентирован на практические задачи:
SCADAindustrial automationindustrial IoTtelemetrymonitoringdiagnosticspump station monitoringModbus TCP pollingPLC integrationRTU / gateway integrationtime-series data collectionregister map validationindustrial telemetryModbus simulatorstation monitoring
Если нужно быстро понять, что проект живой и работает, делай так:
- Установи зависимости:
uv sync
uv pip install -e .- Запусти локальный Modbus simulator:
uv run python -m pstg.simulator.server- В другом окне терминала запусти collector:
uv run python -m pstg.app.collector- Проверь логи:
- в логах simulator должны идти строки
Updated fc... - в логах collector должны появляться результаты polling
Это самый быстрый способ проверить:
- что сервер поднялся
- что клиент подключается
- что регистры читаются
- что значения меняются во времени
Этот проект писался не как "витринный pet project", а как рабочий инженерный инструмент.
Сначала задача была очень практичной:
- нужно читать данные по
Modbus TCP - нужно тестировать это без постоянного доступа к оборудованию
- нужно быстро различать ошибку устройства и проблему связи
- нужно ускорить разработку мониторинга станций
Потом вокруг этого постепенно выросли:
- понятные domain-модели
- fallback-логика
- reconnect
- simulator
- тесты
Поэтому PSTG сейчас это аккуратный MVP, который уже полезен сам по себе и при этом оставляет хороший фундамент для следующих итераций.
Текущая зафиксированная версия документации и релизных материалов: 0.1.0 от 2026-02-28.
В репозитории есть отдельные файлы под разные сценарии использования:
README.md— основная техническая документация проекта, быстрый старт, архитектура, FAQ, тесты и simulator.RELEASE_NOTES.md— развернутый конспект релиза: что вошло вMVP, зачем это сделано и какую практическую ценность дает дляModbus TCP,telemetry,SCADAи мониторинга станций.CHANGELOG.md— краткая история изменений по версиям.RELEASE_PAGE.md— готовый текст для страницы релиза вGitHubилиGitLab.
Эти материалы синхронизированы по текущему состоянию проекта:
PSTGпозиционируется как инженерныйMVP- проект разрабатывался для личного использования специалистом сервисного отдела
ООО "ЧЗМЭК" - основной фокус —
Modbus TCP polling,telemetry,diagnostics,station monitoring,SCADA,industrial IoT - в релиз уже входят collector, fallback, reconnect, simulator, тесты и документация
- Быстрый старт
- Небольшая история проекта
- Релизные материалы
- Статус проекта
- Зачем появился этот проект
- Что делает проект сейчас
- Архитектура проекта
- Требования
- Установка
- Запуск collector
- Текущая конфигурация collector
- Контракт polling
- Подробное описание
PollResult - Подробное описание
RawBlockResult - Подробное описание
ErrorInfo - Локальный Modbus simulator
- Подробное описание конфига симулятора
- Как подключить collector к симулятору
- Как быстро проверить, что все работает
- Тесты
- Типовые сценарии использования
- FAQ / Частые проблемы
- Лицензия и упоминание
- Ограничения текущего MVP
- Что можно развивать дальше
Это именно MVP, а не завершенный production-ready продукт.
Сейчас в проекте уже есть:
- асинхронный polling
FC04 - fallback на
FC03 - структурированный результат чтения
- разделение ошибок на
DEVICEиTRANSPORT - reconnect-логика
- локальный
Modbus TCP simulator - тесты нескольких уровней: unit, scenario, integration
Пока еще нет:
- полноценного decode-слоя
- внешнего конфига collector
- сохранения в БД
- REST API
- reload конфигурации на лету
В реальной работе с промышленной автоматизацией почти всегда есть одни и те же неудобства:
- оборудование не всегда доступно
- PLC или gateway заняты
- стенд ограничен по времени
- карта регистров меняется по ходу интеграции
- нужно быстро проверить гипотезу, не трогая реальный объект
- нужно понимать, что именно происходит: нет связи, ошибка адреса, ошибка функции или просто пустой диапазон
PSTG появился как попытка сделать практичный инструмент, который можно:
- быстро запустить
- быстро изменить
- быстро проверить
- использовать в разработке, диагностике и дальнейшем развитии мониторинга станций
В этом смысле проект ближе к инженерному рабочему инструменту, чем к академической демонстрации.
Текущая реализация делает следующее:
- Подключается к устройству по
Modbus TCP - Читает
Input RegistersчерезFC04 - Если устройство отвечает
device error, делает fallback наFC03 - Возвращает результат цикла чтения в виде
PollResult - Отдельно хранит:
- состояние канала
- результат по каждому блоку чтения
- сырые регистры
- тип ошибки
- длительность чтения
Это уже позволяет:
- строить telemetry collector
- отлаживать register map
- наблюдать поведение канала связи
- тестировать fallback-логику
- писать дальнейший decode и monitoring-слой
src/
pstg/
app/
collector.py
modbus_corfig.py
read_config.py
domain/
connection_state.py
error_info.py
kind_state.py
modbus_config.py
modbus_device_read_settings.py
poll_result.py
raw_block_result.py
drivers/
open_connection_modbus_tcp.py
read_block.py
read_fc03_holding_register.py
read_fc04_input_regoster.py
simulator/
config.py
server.py
tests/
...
Слой orchestration.
Здесь находится:
- запуск collector
- сценарий опроса
- reconnect
- fallback
FC04 -> FC03
Слой работы с Modbus и transport-логикой.
Здесь находится:
- открытие TCP соединения
- чтение конкретных function code
- общая обвязка чтения блока
Слой доменных структур.
Здесь находятся dataclass и enum, которые описывают:
- результат polling
- результат чтения блока
- типы ошибок
- состояние соединения
Локальный Modbus TCP server для разработки без PLC.
Он нужен, чтобы:
- писать код без реального оборудования
- проверять адреса и register map
- тестировать reconnect
- смотреть, как collector работает на меняющихся данных
- Python
3.12+ uv
Из корня репозитория:
uv sync
uv pip install -e .Проверка:
uv --versionuv run python -m pstg.app.collectorОстановка:
Ctrl + C
Файл: modbus_corfig.py
DEVICE_IP = "127.0.0.1"DEVICE_PORT = 1502DEVICE_POLL_INTERVAL_S = 2
Файл: read_config.py
DEVICE_ID = 1DATA_OFFSET = 0READ_DATA_COUNT = 12
UP, если хотя бы один блок дал любой Modbus-ответDOWN, если ни один блок не ответил и были только transport-ошибки
True, если чтение успешно и ответ безisError()False, если былdevice errorилиtransport error
KindState.DEVICE= устройство ответило Modbus exceptionKindState.TRANSPORT= ошибка сети, timeout, TCP, reconnect или сбой клиента
PollResult описывает один полный цикл polling.
Это не просто "пакет данных", а снимок состояния взаимодействия с устройством на конкретном такте опроса.
Идентификатор запуска сервиса.
Сейчас поле заготовлено под дальнейшее развитие. Оно пригодится, когда понадобится:
- разделять несколько запусков collector
- связывать polling-сессии с логами
- хранить telemetry batches по
run_id
Порядковый номер цикла опроса.
Полезен для:
- диагностики пропусков циклов
- анализа drift
- проверки стабильности polling loop
- последующего хранения time-series с номером итерации
Unix timestamp в секундах, когда начался цикл опроса.
Нужен для:
- временной привязки данных
- корреляции с логами PLC
- анализа задержек сети и устройства
- последующей записи в monitoring и historian системы
Unix timestamp в секундах, когда закончился цикл опроса.
Позволяет:
- измерять длительность полного цикла
- понимать, когда данные реально были получены
- сравнивать expected polling interval и фактическое поведение
Итоговое состояние канала связи по результату цикла.
Важно: это не признак валидности полезных данных, а именно признак того, жив ли канал общения с устройством.
Смысл:
UP= устройство дало хотя бы один Modbus-ответDOWN= ответа не было, были только transport-проблемы
Список результатов по отдельным блокам чтения внутри одного цикла.
Обычно в текущей версии:
- первый блок:
FC04 - второй блок:
FC03
Это дает прозрачную диагностику:
- что было попыткой основного чтения
- был ли fallback
- какой блок вернул ошибку
- на каком function code реально пришел ответ
RawBlockResult описывает одну конкретную попытку чтения одним Modbus function code.
Это low-level результат, который полезен:
- для диагностики
- для наблюдения за каналом связи
- для анализа register map
- для построения decode-слоя
Номер Modbus function code.
Типичные значения:
4=Read Input Registers3=Read Holding Registers
Поле нужно для:
- анализа fallback
- логирования
- отладки реального поведения PLC / RTU / gateway
Стартовый Modbus адрес чтения, то есть offset.
Если:
addr = 0count = 12
то чтение началось с адреса 0 и включало диапазон до 11.
Это одно из самых полезных полей при отладке register map, address map, Modbus addressing, интеграции PLC и gateway.
Количество регистров, которое было запрошено подряд.
Это важно, потому что одно прикладное значение может занимать:
- 1 регистр
- 2 регистра
- 4 регистра
Например, когда позже появится decode:
uint16int16uint32int32float32- status words
- bit flags
Modbus Unit ID / Slave ID.
Часто в Modbus TCP это 1, но поле особенно важно, если:
- работа идет через gateway
- за одним IP скрыто несколько устройств
- эмулируется реальная схема Modbus TCP -> Modbus RTU
Флаг успешности именно этого блока чтения.
Смысл:
True= ответ успешный, безisError()False= былdevice errorилиtransport errorNone= блок фактически не использовался или еще не осмысленно заполнен
Важно: ok не равен состоянию канала. Канал оценивается через PollResult.connection_state.
Сырые значения Modbus регистров.
Это raw data, из которых потом можно строить:
- telemetry values
- counters
- analog signals
- digital state words
- alarm flags
- status bit masks
- engineering values после decode
В текущем проекте регистры сохраняются даже при device error, если библиотека вернула payload. Это сделано сознательно: для low-level диагностики и анализа нестандартного поведения Modbus-устройств.
Структурированная ошибка конкретного блока чтения.
Если поле None, значит для этого блока явная ошибка не зафиксирована.
Если поле заполнено, можно понять:
- ошибка transport или device
- есть ли Modbus exception code
- какой текст ошибки попал в результат
Длительность чтения этого блока в миллисекундах.
Полезно для:
- диагностики производительности
- оценки времени ответа PLC
- поиска lag и timeout
- анализа degradation сети или gateway
Unix timestamp завершения чтения этого блока.
Это полезно, если:
- чтения идут последовательно
- нужно видеть момент завершения каждого Modbus request
- потом будет time-series storage или audit trail
ErrorInfo это компактная структура для описания причины ошибки.
Человекочитаемое описание ошибки.
Нужно для:
- логирования
- диагностики
- быстрой инженерной отладки
Тип ошибки.
Значения:
DEVICE= устройство ответило Modbus exceptionTRANSPORT= ошибка сети, TCP, timeout, reconnect или сбой клиента
Modbus exception code, если устройство ответило ошибкой.
Типовые примеры:
ILLEGAL FUNCTIONILLEGAL DATA ADDRESSILLEGAL DATA VALUESERVER DEVICE FAILUREGATEWAY TARGET DEVICE FAILED TO RESPOND
Это одно из ключевых полей для интеграции с реальным оборудованием и отладки карт регистров.
Дополнительный тип transport-исключения.
Сейчас это поле зарезервировано под дальнейшее развитие и более подробную диагностику.
Симулятор нужен для разработки без PLC, RTU, gateway и другого оборудования.
Он умеет:
- поднимать локальный
Modbus TCP server - отдавать
FC04иFC03 - настраивать адреса регистров через JSON
- автоматически менять значения по таймеру
Это делает проект пригодным для:
- локальной разработки
- проверки гипотез
- тестирования polling logic
- отладки register map
- проверки telemetry pipeline без стенда
Запуск со встроенным конфигом:
uv run python -m pstg.simulator.serverЗначения по умолчанию:
- host:
127.0.0.1 - port:
1502 - device_id:
1 - FC04 адрес
0:[101, 102, 103, 104], автообновление раз в1секунду - FC03 адрес
0:[201, 202, 203, 204], автообновление раз в2секунды
Пример файла: simulator.example.json
uv run python -m pstg.simulator.server --config simulator.example.jsonuv run python -m pstg.simulator.server --config simulator.example.json --host 0.0.0.0 --port 1503 --device-id 2IP-адрес, на котором стартует локальный Modbus server.
Обычно:
127.0.0.1для локальной разработки0.0.0.0если надо подключаться с другой машины
TCP порт сервера.
Для локальной разработки удобно использовать не 502, а безопасный пользовательский порт, например 1502.
Unit ID / Slave ID, который должен совпадать с настройкой клиента.
Список блоков для FC04.
Подходит для эмуляции:
- аналоговых измерений
- показаний датчиков
- телеметрии
- числовых значений процесса
Список блоков для FC03.
Подходит для эмуляции:
- внутренних регистров PLC
- конфигурационных параметров
- fallback-данных
- нестандартных устройств, которые не отдают данные в
FC04
{
"host": "127.0.0.1",
"port": 1502,
"device_id": 1,
"input_registers": [
{ "address": 0, "values": [101, 102, 103, 104], "interval_s": 1.0, "step": 1 },
{ "address": 10, "values": [501, 502] }
],
"holding_registers": [
{ "address": 0, "values": [201, 202, 203, 204] },
{ "address": 20, "values": [901, 902, 903], "interval_s": 2.0, "step": 10 }
]
}Стартовый адрес блока.
Если:
address = 10values = [501, 502]
то будут заполнены адреса 10 и 11.
Начальные значения регистров.
Это raw 16-bit значения, которые потом клиент сможет:
- читать как есть
- декодировать
- интерпретировать как telemetry, status, alarms, flags
Необязательный интервал автообновления блока в секундах.
Если поле не задано, блок остается статичным.
Необязательный шаг изменения значений.
При каждом срабатывании таймера каждое значение увеличивается на step.
Пример:
{
"address": 0,
"values": [100, 200],
"interval_s": 1.0,
"step": 5
}Поведение:
- старт:
[100, 200] - через 1 секунду:
[105, 205] - через 2 секунды:
[110, 210]
Это полезно для:
- проверки трендов
- проверки того, что polling видит изменение данных
- разработки decode-слоя
- тестирования time-series логики
Убедись, что в collector настроено:
- host:
127.0.0.1 - port:
1502 device_id = 1
После этого:
- Запусти симулятор
- Запусти collector
- Смотри логи сервера и клиента
Ниже самый практичный сценарий быстрой проверки, без PLC и без стенда.
Из корня проекта:
uv sync
uv pip install -e .Убедись, что в collector стоят:
- host:
127.0.0.1 - port:
1502 device_id = 1
Файлы:
- modbus_corfig.py
- read_config.py
uv run python -m pstg.simulator.serverЧто должно появиться в логах:
- сервер стартовал
- включено автообновление хотя бы одного блока
- идут строки
Updated fc...
Типичный пример:
Starting Modbus simulator on 127.0.0.1:1502 device_id=1
Auto-update enabled for fc4 address=0 interval_s=1.0 step=1
Auto-update tasks started: 2
Updated fc4 address=0 values=[102, 103, 104, 105]
В другом окне терминала:
uv run python -m pstg.app.collectorВ логах collector должны появиться:
- попытка подключения
- успешный connect
- результаты polling
Если все в порядке, ты увидишь:
connection_state = UP- блок
FC04с регистрами - новые значения на следующих циклах
Смотри одновременно на:
- логи simulator
- логи collector
Если simulator пишет:
Updated fc4 address=0 values=[103, 104, 105, 106]
Updated fc4 address=0 values=[104, 105, 106, 107]
то в collector через несколько циклов тоже должны появляться новые значения.
Для этого можно:
- изменить конфиг simulator так, чтобы нужный диапазон
FC04стал невалидным - положить рабочие данные в
holding_registers - перезапустить simulator
- снова запустить collector
Тогда можно увидеть:
device errorнаFC04- fallback на
FC03 - итоговый
connection_state = UP
.\\.venv\\Scripts\\python.exe -m pytest tests -qили
uv run pytest tests -qЕсли тесты проходят, значит текущий MVP в рабочем состоянии:
- unit level
- scenario level
- integration level
- simulator level
В проекте уже есть тесты нескольких уровней. Это важно: они проверяют не только отдельные функции, но и текущее поведение проекта как telemetry MVP.
Проверяют изолированную логику без реального Modbus server.
Что покрыто:
- успешный
read_block device errorвread_block
Файлы:
- test_read_block_success.py
- test_read_block_device_error.py
Проверяют один сценарий orchestration через заглушки и monkeypatch.
Что покрыто:
- fallback
FC04 -> FC03 - reconnect после
DOWN
Файлы:
- test_poll_device_fallback.py
- test_poll_forever_reconnect.py
Проверяют проект на настоящем pymodbus.server.ModbusTcpServer.
Что покрыто:
- чтение
FC04 poll_deviceна успешном чтенииpoll_deviceнаdevice error
Файлы:
- test_real_server_fc04_read.py
- test_real_server_poll_device_success.py
- test_real_server_poll_device_device_error.py
Проверяют локальный development simulator.
Что покрыто:
- загрузка конфига
- запуск dev server
- чтение из simulator
- автообновление регистров по таймеру
Файлы:
- test_simulator_config.py
- test_simulator_server_runtime.py
- test_simulator_auto_update.py
Полный запуск:
.\\.venv\\Scripts\\python.exe -m pytest tests -qили
uv run pytest tests -qПоднимается simulator, collector подключается к нему, и можно спокойно писать код опроса, reconnect и decode.
Через simulator удобно проверять:
- адреса
- диапазоны чтения
- длину блока
- fallback
Автоизменяемые регистры полезны, чтобы видеть живой поток значений и проверять дальнейшую работу time-series слоя.
Даже на текущем MVP уровне проект уже полезен для инженерной диагностики:
- устройство отвечает или нет
- это
device errorилиtransport error - работает ли fallback
- стабильна ли связь
Проверь, что совпадают:
hostportdevice_id
Чаще всего проблема в том, что:
- simulator поднят на
1502 - а collector смотрит в другой порт
Или:
- simulator запущен с одним
device_id - а collector читает другой
Проверь файлы:
- modbus_corfig.py
- read_config.py
- simulator.example.json
Проверь, есть ли в конфиге блока:
interval_sstep
Если этих полей нет, блок будет статичным.
Также смотри логи сервера. При включенном автообновлении должны быть строки вида:
Auto-update enabled for fc4 address=0 interval_s=1.0 step=1
Updated fc4 address=0 values=[...]
Если запускаешь simulator без --config, используется встроенный дефолтный конфиг, в котором автообновление сейчас уже включено.
Обычно причина в том, что:
- читается слишком большой диапазон регистров
- меняется только часть блока
- шаг изменения слишком маленький
Например:
- collector читает
12регистров - а в simulator меняются только первые
4
В таком случае визуально может казаться, что "ничего не происходит".
Для явной проверки:
- временно уменьши
READ_DATA_COUNT - увеличь
step - смотри не только клиентские логи, но и серверные
Это ожидаемое поведение по текущему контракту.
Смысл такой:
connection_state = UPозначает, что устройство ответилоok = Falseозначает, что этот ответ был неуспешным для полезных данных
Пример:
- пришел
Modbus exception - канал жив
- ответ есть
- но данные невалидны
Поэтому:
connection_state = UPok = Falsecurrent_error_info.kind = DEVICE
Fallback выполняется только если основной FC04 вернул DEVICE error.
Если FC04:
- успешно вернул данные
то fallback не нужен.
Если FC04:
- не дал ответа и произошел
TRANSPORTerror
то это уже не "ошибка содержимого", а проблема связи.
Это сделано специально для low-level диагностики.
Некоторые устройства или библиотеки могут вернуть полезный payload даже в ошибочном ответе. Для дальнейшего анализа register map, нестандартного поведения PLC или gateway это бывает полезно.
Но использовать такие данные в прикладной логике нужно осторожно.
Причина не в тестах, а в правах доступа к .pytest_cache.
Это означает:
pytestне смог записать cache- сами тесты при этом могут быть полностью корректными
Если хочешь убрать предупреждение правильно, надо восстановить права записи для текущего пользователя в корень проекта и .pytest_cache.
Это нормально.
Есть два уровня логики:
- reconnect внутри
pymodbusклиента - reconnect на уровне самого
PSTG
Это не дублирование ради дублирования, а разные уровни защиты:
- библиотека пытается пережить краткий сбой
- приложение умеет пересоздать сессию целиком
Самый практичный порядок обычно такой:
- вынести конфиг collector во внешний файл или env
- добавить decode-слой
- определить, куда сохранять telemetry
- добавить API или экспорт данных
- расширять simulator под реальные сценарии
Проект распространяется под лицензией Apache License 2.0.
При использовании, распространении и публикации производных материалов нужно:
- сохранять файл LICENSE
- сохранять файл NOTICE
- не удалять упоминание проекта
PSTGи происхождения проекта изNOTICE
Для англоязычных публикаций и внешнего использования подготовлены отдельные файлы:
- конфиг collector пока hardcoded
- нет полноценного decode-слоя
- нет хранения в базе
- нет REST API
- нет reload конфига симулятора на лету
- coils и discrete inputs пока не настраиваются через JSON
- внешний конфиг для collector
- decode
uint16/int16/int32/float32 - запись в time-series storage
- REST API
- расширение simulator
- richer diagnostics и audit trail