Skip to main content

5 posts tagged with "modbus"

View All Tags

Утилита modbus-slave. Эмулятор Modbus RTU датчиков

· 4 min read
dmn
maintainer

Когда разрабатываешь систему мониторинга или SCADA, часто нужно протестировать опрос датчиков — но реального оборудования под рукой нет. Или нужно показать демо заказчику без физических устройств. Или хочется отладить логику мастера не выезжая на объект.

Именно для этого мы написали modbus_slave — эмулятор Modbus RTU slave устройств на C, который работает на Linux и Windows, не требует зависимостей и умеет отдавать реальные данные из файлов.

Что умеет

  • Эмулирует до 30 независимых Modbus RTU устройств на одном последовательном порту
  • Каждое устройство отвечает на FC03 (Read Holding Registers), 20 регистров
  • Значения регистров — случайные (для тестирования) или из файла (реальные данные)
  • Работает на Linux x86_64, aarch64 (NAPI2, RK3568, Raspberry Pi) и Windows x64
  • Поддерживает RS-485 — RTS direction control через DTS/GPIO
  • Режим демона с логами в syslog и systemd service для автозапуска
  • Защита от устаревших данных — если скрипт-источник упал, регистры возвращают нули
  • Один статический бинарь без зависимостей — скопировал и запустил

Архитектура

Идея простая: один процесс слушает RS-485 шину и отвечает на запросы от любого из 30 адресов. Для мастера это выглядит как несколько физических устройств на линии — он не видит разницы.

Мастер (PC/ПЛК)          RS-485 шина          modbus_slave (NAPI2)
mbpoll -a 1 ──────────────────────────► slave ID 1 → /tmp/cpu.dat
mbpoll -a 2 ──────────────────────────► slave ID 2 → /tmp/time.dat
mbpoll -a 3 ──────────────────────────► slave ID 3 → random

Регистры читаются из обычных текстовых файлов — одно число на строку. Файлы живут в /tmp (tmpfs — RAM диск), SD карта не изнашивается.

Быстрый старт

Установка на NAPI2 / aarch64

# Скачать статический бинарь
wget https://github.com/lab240/modpoll-slave/raw/main/bin/modbus_slave_aarch64
chmod +x modbus_slave_aarch64

# Запустить — 3 датчика, порт ttyS7
./modbus_slave_aarch64 -p /dev/ttyS7 -b 115200 -a 3

Запуск на Windows

modbus_slave.exe -p COM4 -b 115200 -a 3

Проверка с mbpoll

mbpoll -m rtu -b 115200 -P none -a 1 -r 1 -c 20 /dev/ttyUSB0

Реальные данные из файлов

Самое интересное — каждому slave можно привязать файл с реальными значениями. Формат простой: одно целое число на строку.

# Запуск: slave 1 читает данные CPU, slave 2 — время, slave 3 — рандом
./modbus_slave -p /dev/ttyS7 -b 115200 -a 3 \
-f 1:/tmp/cpu.dat \
-f 2:/tmp/time.dat

Файл обновляется внешним скриптом атомарно через mv — никакой гонки данных:

# Температура ядер CPU (°C × 100)
while true; do
for zone in /sys/class/thermal/thermal_zone*/temp; do
val=$(( $(cat $zone) / 10 ))
echo $val
done > /tmp/cpu.tmp
mv /tmp/cpu.tmp /tmp/cpu.dat
sleep 5
done

Значение 4523 в регистре означает 45.23 °C — стандартное соглашение для передачи дробных чисел в Modbus.

Защита от падения скриптов

Если скрипт обновления данных упал — файл перестаёт обновляться, но данные в нём остаются старые. Мастер продолжал бы читать устаревшие значения.

Параметр -t задаёт максимальный возраст файла:

./modbus_slave -p /dev/ttyS7 -b 115200 -f 1:/tmp/cpu.dat -t 10

Если файл не обновлялся более 10 секунд — все регистры возвращают 0. Это сразу видно мастеру и SCADA системе. В лог пишется предупреждение (не чаще раза в минуту чтобы не спамить):

file /tmp/cpu.dat is stale (45s > 10s), returning zeros

По умолчанию t=10. Отключить: -t 0.

Режим демона и systemd

# Запуск как демон
./modbus_slave -d -p /dev/ttyS7 -b 115200 -a 3 -f 1:/tmp/cpu.dat

# Статус
./modbus_slave -s

# Остановка
./modbus_slave -k

# Логи
journalctl -t modbus_slave -f

Для автозапуска при загрузке — systemd service:

sudo cp modbus_slave /usr/local/bin/
sudo cp service/modbus_slave.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now modbus_slave

Статистика запросов пишется в лог каждые 60 секунд:

stats: slaves=3 total_ok=12480 total_err=0

Windows

На Windows всё то же самое, только порт называется COM4 вместо /dev/ttyS7, файлы в C:\temp\ вместо /tmp/, и -bg вместо -d:

modbus_slave.exe -bg -p COM4 -b 115200 -a 3 ^
-f 1:C:\temp\cpu.dat ^
-f 2:C:\temp\time.dat

Логи пишутся в C:\temp\modbus_slave.log.

Сборка из исходников

Код разделён на три файла:

ФайлСодержимое
modbus_core.hВся логика Modbus: CRC16, FC03, файлы, статистика
modbus_slave.cLinux: termios, fork, syslog, /proc
modbus_slave_win.cWindows: Win32 API, CreateFile, DCB
# Linux x86_64
gcc -O2 -Wall -o modbus_slave src/modbus_slave.c

# Linux aarch64 статический
aarch64-linux-gnu-gcc -O2 -Wall -static -o modbus_slave_aarch64 src/modbus_slave.c

# Windows .exe с Linux
x86_64-w64-mingw32-gcc -O2 -Wall -o modbus_slave.exe src/modbus_slave_win.c

Итог

modbus_slave решает конкретную задачу — дать возможность разрабатывать и тестировать Modbus мастер без реального железа, или превратить одноплатный компьютер в многоканальный шлюз данных в Modbus.

Код открытый, собирается одной командой, работает на том же NAPI2 где и всё остальное.

Репозиторий: github.com/lab240/modpoll-slave

Утилита mbscan - быстрый поиск Modbus устройств на линии

· 5 min read
dmn
maintainer

247 адресов за 2.5 секунды, ноль зависимостей, один .c файл. Рассказываем, зачем мы написали свой сканер Modbus-шины и как он работает.


Проблема: «а что вообще висит на шине?»

Кто работал с Modbus RTU, знает ситуацию: подключаешь шлюз к RS-485 шине, а там десяток устройств с неизвестными адресами. Или один датчик, но кто-то поставил ему адрес 117 вместо документированного 1. Или устройство просто не отвечает — и непонятно, проблема в адресе, скорости, чётности или в самом устройстве.

Стандартный подход - mbpoll или любой Modbus-клиент, которым вручную перебираешь адреса. Это работает, но медленно и неудобно: 247 возможных адресов, на каждый нужно отправить запрос, подождать таймаут, проверить ответ.

Мы решили автоматизировать это одной утилитой.

Репозиторий: github.com/lab240/mbscan


Что такое mbscan

mbscan - консольная утилита для сканирования Modbus RTU шины. Открывает последовательный порт, последовательно опрашивает диапазон адресов функцией FC03 (Read Holding Registers) и выводит найденные устройства с содержимым регистров.

Один файл на C, никаких внешних библиотек. Встроенная реализация CRC16, POSIX-совместимый код. Работает на Linux x86_64, aarch64, OpenWrt, Raspberry Pi — везде, где есть терминальный API POSIX.

Быстрый старт

# Сканируем всё на /dev/ttyUSB0 (по умолчанию: 115200-8N1, таймаут 100мс)
mbscan -p /dev/ttyUSB0

# Быстрый скан с таймаутом 10мс
mbscan -p /dev/ttyUSB0 -o 10

# Конкретный диапазон, читаем 4 регистра
mbscan -p /dev/ttyUSB0 -f 1 -t 30 -c 4

# 9600 бод, чётность 8E1
mbscan -p /dev/ttyS1 -b 9600 -d 8E1

Вывод выглядит так:

mbscan: scanning /dev/ttyUSB0 115200-8N1, addresses 1-247, timeout 100ms
mbscan: reading 4 register(s) starting at 0

Found slave 125: [0]=125 [1]=1 [2]=830 [3]=794

mbscan: done. Found 1 device(s).

Нашёл устройство на адресе 125, прочитал 4 регистра — готово.


Параметры

mbscan -p PORT [опции]

-p PORT Последовательный порт (обязательный)
-b BAUD Скорость (по умолчанию: 115200)
-d PARAMS Формат данных: 8N1, 8E1, 8O1, 7E1 и т.д. (по умолчанию: 8N1)
-f FROM Начальный адрес (по умолчанию: 1)
-t TO Конечный адрес (по умолчанию: 247)
-o MS Таймаут на адрес в мс (по умолчанию: 100)
-r REG Начальный регистр, 0-based (по умолчанию: 0)
-c COUNT Количество регистров для чтения (по умолчанию: 1)
-v Подробный вывод
-h Справка

Как это работает внутри

Алгоритм прямолинейный, но дьявол в деталях:

  1. Открывает последовательный порт, настраивает скорость, чётность, количество стоп-битов через termios.

  2. Для каждого адреса в диапазоне:

    • Сбрасывает буфер порта от предыдущих данных
    • Формирует 8-байтовый запрос Modbus RTU FC03 с CRC16
    • Отправляет запрос и ждёт ответ с настроенным таймаутом
    • Валидирует ответ: проверяет CRC, адрес slave, код функции
    • Если всё сходится — выводит найденное устройство с содержимым регистров
  3. Между запросами выдерживает межкадровую паузу Modbus (3.5 символьных времени) — это требование протокола, без него устройства могут путать конец одного кадра и начало другого.

CRC16 реализован встроенный - нет зависимости от libmodbus или других библиотек. Весь код в одном файле mbscan.c.


Скорость сканирования

Скорость определяется таймаутом на адрес. Если устройство не отвечает — ждём полный таймаут. Если отвечает — переходим к следующему сразу после получения ответа.

ТаймаутПолный скан (1–247)Когда использовать
10 мс~2.5 секКороткие кабели, лабораторный стенд
50 мс~12 секБольшинство установок
100 мс~25 секПо умолчанию, надёжно
200 мс~50 секДлинные линии RS-485

На практике 10 мс хватает для стенда с коротким кабелем. Для промышленных линий с десятками метров RS-485 лучше ставить 50-100 мс — на длинных линиях задержки растут из-за переотражений и ёмкости кабеля.


Сборка

Нативная компиляция

cd src
gcc -O2 -Wall -o mbscan mbscan.c

Статическая сборка (один бинарник без зависимостей):

gcc -O2 -Wall -static -o mbscan mbscan.c

Пакет для OpenWrt

Каталог mbscan кладётся в дерево пакетов OpenWrt:

cp -r mbscan /path/to/openwrt/package/
cd /path/to/openwrt
echo "CONFIG_PACKAGE_mbscan=y" >> .config
make package/mbscan/compile -j$(nproc)

Результат — .ipk (или .apk) пакет в bin/packages/*/base/.

Кросс-компиляция для aarch64

Если есть тулчейн OpenWrt:

/path/to/openwrt/staging_dir/toolchain-aarch64_generic_gcc-*/bin/aarch64-openwrt-linux-gcc \
-O2 -Wall -static -o mbscan-linux-aarch64 src/mbscan.c

Готовые бинарники для x86_64 и aarch64 доступны на странице Releases.


Интеграция с luci-app-mbpoll

mbscan - не просто самостоятельная утилита. Он используется как бэкенд для вкладки Scan Bus в веб-интерфейсе luci-app-mbpoll - нашем LuCI-приложении для опроса Modbus-устройств.

Схема простая: пользователь задаёт параметры порта и диапазон адресов в браузере, LuCI вызывает mbscan на устройстве, парсит вывод и отображает найденные устройства в таблице. Не нужно заходить по SSH, не нужно помнить синтаксис - всё через веб-интерфейс.

Репозиторий luci-app-mbpoll: github.com/lab240/luci-app-mbpoll


Где используется

Основная платформа - промышленные IoT-шлюзы NapiLab Napi на базе Rockchip RK3308 под управлением OpenWrt. Napi имеет встроенный RS-485 на /dev/ttyS1 и два USB-порта для дополнительных адаптеров — типичная конфигурация для Modbus-шлюза.

Но mbscan работает на любом Linux с последовательным портом: обычный x86_64 с USB-RS485 адаптером (CH341, CP2102, FTDI), Raspberry Pi, любая embedded-плата.


Лицензия

GPL-2.0 - как и OpenWrt, как и остальные наши инструменты.

Анализ сигналов Modbus RS485 на анализаторе

· One min read
dmn
maintainer

Анализ сигналов Modbus RS485

Покажем передачу modbus пакетов, отображенную на анализаторе цифровых сигналов.

Оборудование

  • Хост: Napi-C с программным RTS
  • Датчик: учебный Modbus Napi-датчик

Конфигурация каналов

  • Канал 1: RTS хоста
  • Канал 2: RX хоста
  • Канал 3: TX хоста
  • Канал 4: RX датчика
  • Канал 5: TX датчика

Последовательность обмена

  1. Хост поднимает сигнал RTS (передача)
  2. Посылает запрос по линии TX
  3. Датчик принимает запрос через RX
  4. Датчик передает ответ через TX
  5. Хост принимает ответ через RX

#rs485 #rts

Конфигурация UART на модуле CM4 в Токосборщике

· One min read
dmn
maintainer

В Токосборщике на модуле CM4 UART-ы расположились следующим образом:

✔️UART3 - внешний датчик Modbus ✔️UART9 - модуль расширений (Zigbee) ✔️UART7 - встроенный датчик тока

Для корректной работе Debian, необходимо подключить оверлеи с uart7,9 в файле /boot/orangepiEnv.txt

root@orangepicm4:~# cat /boot/orangepiEnv.txt 
verbosity=1
bootlogo=false
extraargs=cma=128M
overlay_prefix=rk356x
overlays=uart7-m2 uart9-m2
rootdev=UUID=a0f8ca89-7eb7-4a1e-947a-2341637b4782
rootfstype=ext4
console=serial

#fcucm4 #orangecm4 #fcu

Python-сниффер для анализа Modbus RTU трафика

· 3 min read
dmn
maintainer

Написал "на коленке" полезный сниффер modbus

modbus_sniffer_raw_pretty.py

#!/usr/bin/env python3
"""
Raw Modbus RTU Sniffer — listens to a serial port and prints decoded Modbus RTU frames.
Now includes decoding of address/count/value for popular function codes.
"""

import serial
import argparse
import time
import logging
import struct

logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.INFO)

def calculate_crc(data: bytes):
crc = 0xFFFF
for pos in data:
crc ^= pos
for _ in range(8):
lsb = crc & 0x0001
crc >>= 1
if lsb:
crc ^= 0xA001
return crc.to_bytes(2, 'little')

def validate_crc(frame: bytes):
if len(frame) < 4:
return False
data, received_crc = frame[:-2], frame[-2:]
return calculate_crc(data) == received_crc

def decode_payload(fc, payload):
if fc in [1, 2, 3, 4]: # Read coils, discrete inputs, HR, IR
if len(payload) >= 4:
address, count = struct.unpack(">HH", payload[:4])
return f"Read | Addr={address} | Count={count}"
elif fc in [5, 6]: # Write single coil/register
if len(payload) >= 4:
address, value = struct.unpack(">HH", payload[:4])
return f"Write Single | Addr={address} | Value={value}"
elif fc in [15, 16]: # Write multiple coils/registers
if len(payload) >= 5:
address, count, byte_count = struct.unpack(">HHB", payload[:5])
return f"Write Multiple | Addr={address} | Count={count} | Bytes={byte_count}"
return f"Payload: {payload.hex()}"

def print_frame_info(frame: bytes):
if not validate_crc(frame):
log.warning(f"❌ Invalid CRC: {frame.hex()}")
return
unit_id = frame[0]
function_code = frame[1]
payload = frame[2:-2]
desc = decode_payload(function_code, payload)
log.info(f"📥 Unit={unit_id} | FC={function_code}{desc}")

def read_frames(ser):
buffer = bytearray()
last_byte_time = time.time()
inter_char_timeout = 0.01
frame_timeout = 0.1

while True:
if ser.in_waiting:
byte = ser.read(1)
now = time.time()
if now - last_byte_time > frame_timeout and buffer:
print_frame_info(bytes(buffer))
buffer.clear()
buffer.append(byte[0])
last_byte_time = now
elif buffer and time.time() - last_byte_time > frame_timeout:
print_frame_info(bytes(buffer))
buffer.clear()
else:
time.sleep(0.005)

def main():
parser = argparse.ArgumentParser(description="Raw Modbus RTU Sniffer with decoding")
parser.add_argument('--port', required=True, help='Serial port (e.g. /dev/ttyUSB0)')
parser.add_argument('--baudrate', type=int, default=9600)
parser.add_argument('--parity', choices=['N', 'E', 'O'], default='N')
parser.add_argument('--stopbits', type=int, choices=[1, 2], default=1)
args = parser.parse_args()

try:
ser = serial.Serial(
port=args.port,
baudrate=args.baudrate,
parity={'N': serial.PARITY_NONE, 'E': serial.PARITY_EVEN, 'O': serial.PARITY_ODD}[args.parity],
stopbits=args.stopbits,
bytesize=8,
timeout=0
)
log.info(f"🔍 Listening on {args.port} at {args.baudrate} bps...")
read_frames(ser)
except Exception as e:
log.error(f"Failed to open serial port: {e}")

if __name__ == "__main__":
main()

использует библиотеку pyserial

pip install pyserial

Пример:

(venv) orangepi@cm4-right:~$ python3 modbus_sniffer_raw_pretty.py  --baudrate 9600 --port /dev/ttyS9
INFO:root:🔍 Listening on /dev/ttyS9 at 9600 bps...
INFO:root:📥 Unit=1 | FC=3 — Read | Addr=0 | Count=1
INFO:root:📥 Unit=1 | FC=3 — Read | Addr=10 | Count=2

Скрипт читает и распознает пакеты modbus, запросы modbus транслирует на экран.

#modbus #modbussniffer