Изображение сгенерировано ИИ
Изображение сгенерировано ИИ

В СКУД PERCo-S-20 есть штатный функционал уведомлений о критических событиях. Пожар, взлом, саботаж — обо всем этом система умеет сигнализировать по средствам СМС. Нам требовался сценарий оповещение “заинтересованных” руководителей о проходе конкретных «вип-персон» через определенные точки доступа. Проще говоря — оповещать кого надо когда ключевые сотрудники или клиенты прикладывают карты к считывателям.

Спойлер: платить за СМС-пакеты операторам связи мы перестали, менять контроллеры не пришлось, а задачу решили локально, элегантно и с нулевым бюджетом.

В этой статье будут: разбор "на коленке" бинарного протокола SMPP, пример конфига Docker-окружение, скрипт-перехватчик на Node.js и low-code магия в n8n (с нюансами работы Telegram в 2026 году).


Когда потребовалось перевести уведомления СКУД на более удобные форматы (в e-mail и мессенджеры) нас ждал классический тупик закрытых проприетарных систем - эксплуатируемая версия PERCo-S-20 позволяет слать уведомления строго в два направления: СМС или Viber. Всё. Никаких вебхуков, никаких Telegram-ботов, никакой кастомизации.

Обратились к производителю с запросом и получили рекомендацию обновления до современной PERCo-Web. Вот только около 40% используемых контроллеров оказались устаревшей версией и их необходимо было заменить.

Посчитали бюджет на замену рабочих контроллеров ради «просто уведомлений», решили посмотреть на "костыли".

Архитектура решения «на коленке»

Идея была простая:

  • Заставляем PERCo думать, что она шлет СМС через SMPP-провайдера.

  • Внутри Docker поднимаем микро-скрипт на Node.js, который слушает SMPP-порт, «прикидывается» сервером, забирает данные и моментально говорит PERCo: «Всё принято, статус 0 (Успешно)!»

  • Этот же скрипт заворачивает сырые бинарные данные в HEX-строку и отправляет вебхуком в n8n.

  • n8n парсит бинарную кашу, декодирует кириллицу и раскидывает человеческий текст по Email и Telegram.

Схема движения:

Для начала анатомия сырого SMPP-пакета

Данные из PERCo приходят в абсолютно «сыром» бинарном виде. Для понимания: команда отправки сообщения (submit_sm) выглядит как бесконечная Hex-строка. Чтобы n8n смог с ней работать, нужно понимать её структуру.

Типичный пакет от нашей PERCo длиной 105 байт состоит из трех блоков:

  1. Заголовок (Header): Первые 16 байт. Содержат длину пакета (0x00000069 = 105 байт), ID команды отправки (0x00000004 для submit_sm) и порядковый номер сообщения (Sequence Number) для контроля очереди.

  2. Обязательные параметры (Body): Метаданные доставки. Отсюда мы выковыриваем два главных поля:

    • Имя отправителя (Source Address): Буквенный альфа-нумерик (например, I_SEND_TEST). В будущем письме он станет полем «От кого» или темой.

    • Номер получателя (Destination Address): Телефон в формате 7xxxxxxxxxx. В n8n этот номер используется для маршрутизации или склейки с базой данных сотрудников.

  3. Полезная нагрузка (Message Payload): Сам текст СМС. PERCo выносит его в самый конец пакета в виде опционального TLV-параметра (Tag-Length-Value) с тегом 0x0424. И здесь кроется главная засада: текст упакован в двухбайтовую кодировку UCS-2 Big Endian (чтобы поддерживать кириллицу). Без декодера мы увидим лишь нечитаемый набор символов вроде 0442043504410432.

Разворачиваем окружение в Docker

Т.к. в компании уже есть работающая инфраструктура упакована в Docker, добавляем еще кирпичик.

Кусок рабочего кода для docker-compose.yml, который поднимает наш SMPP-приемник и связывает его с контейнером автоматизации n8n:
version: '3.8'

services:
  # Подключеие к БД:


  # Локальный SMPP-приемник, который слушает СКУД PERCo
  smpp-to-webhook:
    image: dockerhub.timeweb.cloud/library/node:18-alpine
    container_name: ai_smpp_receiver
    restart: always
    ports:
      - "3800:3800"
    volumes:
      - "./smpp_server.js:/app/index.js:ro"
    command: node /app/index.js
    depends_on:
      - n8n

  # Платформа автоматизации n8n:
  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    container_name: ai_n8n
    restart: always
    ports:
      - "5678:5678"
    environment:
      - N8N_HOST=ip_n8n
      - N8N_PORT=5678
      - N8N_PROTOCOL=http
      - N8N_WEBHOOK_URL=http://ip_n8n
      - NODE_ENV=production
      - N8N_SECURE_COOKIE=false
      - N8N_RESTRICT_FILE_ACCESS_TO=""
      - NODES_EXCLUDE=[]
      - NODE_TLS_REJECT_UNAUTHORIZED=0
    volumes:
      - n8n_data:/home/node/.n8n
      - /home/adminllm/security_ai/skud:/home/node/reports
    depends_on:
      - postgres

volumes:
  postgres_data:
  n8n_data:

Скрипт-перехватчик: Наш фейковый SMPP-сервер

В качестве легковесного прокси, который СКУД воспринимает как настоящий сервер СМС-оператора, используется скрипт smpp_server.js (создан с применением ИИ). Его задача — быстро ответить системе «ОК», чтобы она не уходила в ошибку тайм-аута и не блокировала очередь, а сам пакет переслать в n8n для дальнейшей работы:

smpp_server.js
const net = require('net');
const http = require('http');

net.createServer((socket) => {
    console.log('⚡ PERCo подключилась. Шлем сырой HEX...');
    
    socket.on('data', (data) => {
        if (data.length < 16) return;
        
        const commandId = data.readUInt32BE(4);
        const seqNumber = data.readUInt32BE(12);

        // 1. Bind Transceiver
        if (commandId === 0x00000009) {
            const response = Buffer.alloc(16);
            response.writeUInt32BE(16, 0);
            response.writeUInt32BE(0x80000009, 4);
            response.writeUInt32BE(0, 8);
            response.writeUInt32BE(seqNumber, 12);
            socket.write(response);
            return;
        }

        // 2. Enquire Link
        if (commandId === 0x00000015) {
            const response = Buffer.alloc(16);
            response.writeUInt32BE(16, 0);
            response.writeUInt32BE(0x80000015, 4);
            response.writeUInt32BE(0, 8);
            response.writeUInt32BE(seqNumber, 12);
            socket.write(response);
            return;
        }

        // 3. Submit SM (Пересылка сырого HEX в n8n)
        if (commandId === 0x00000004) {
            // Переводим весь бинарный пакет в верхний регистр HEX
            const rawHex = data.toString('hex').toUpperCase();

            // Отправляем сырые данные в n8n
            const postData = JSON.stringify({ raw_hex: rawHex, source: 'PERCo-SMPP-Raw' });

            const options = {
                hostname: 'ai_n8n',
                port: 5678,
                path: '/webhook/perco-gateway',
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Content-Length': Buffer.byteLength(postData)
                }
            };

            const req = http.request(options, () => {});
            req.on('error', (err) => console.error('❌ Ошибка n8n:', err.message));
            req.write(postData);
            req.end();

            // Отвечаем СКУД
            const response = Buffer.alloc(17);
            response.writeUInt32BE(17, 0);
            response.writeUInt32BE(0x80000004, 4);
            response.writeUInt32BE(0, 8);
            response.writeUInt32BE(seqNumber, 12);
            response.writeUInt8(0, 16);
            socket.write(response);
        }
    });

    socket.on('error', (err) => console.log('Сетевое событие:', err.message));
}).listen(3800, '0.0.0.0', () => console.log('? Транслятор сырого трафика запущен на порту 3800'));

Магия автоматизации в n8n

Workflow

Входящий JSON с Hex-строкой падает на узел Webhook (работает в режиме POST без аутентификации, работа в пределах DMZ). Далее данные передаются в узел Code (JS), задача которого - пройтись по байтам, вырезать заголовок, найти по тегу 0x0424 полезную нагрузку и превратить UCS-2 в русскоязычный текст, попутно достав номер телефона (возможно будем использовать в будущем).

Код ноды Code JS:
// Забираем сырую строку из правильного пути: body.raw_hex
const hex = $input.first().json.body?.raw_hex || ""; 
const result = [];

if (hex && hex.length >= 32) {
    // Функция для перевода Hex в обычную ASCII строку (с обрезкой null-терминатора)
    function hexToAscii(hexStr) {
        if (!hexStr) return '';
        let str = '';
        for (let i = 0; i < hexStr.length; i += 2) {
            const charCode = parseInt(hexStr.substr(i, 2), 16);
            if (charCode === 0) continue; 
            str += String.fromCharCode(charCode);
        }
        return str;
    }

    // Функция для перевода Hex в UTF-16BE (Big Endian)
    function hexToUtf16BE(hexStr) {
        if (!hexStr) return '';
        let str = '';
        for (let i = 0; i < hexStr.length; i += 4) {
            const charCode = parseInt(hexStr.substr(i, 4), 16);
            if (charCode === 0 || charCode === 0x000D || charCode === 0x000A) continue; 
            str += String.fromCharCode(charCode);
        }
        return str;
    }

    // 1. Ищем маркер data_coding и sm_default_msg_id, которые в пакете PERCo выглядят как "0800"
    const markerIndex = hex.indexOf("0800");

    if (markerIndex !== -1 && markerIndex + 6 <= hex.length) {
        // Байт sm_length (длина текста в байтах) идет сразу за "0800" (это 2 HEX-символа)
        const smLengthHex = hex.substring(markerIndex + 4, markerIndex + 6);
        const smLength = parseInt(smLengthHex, 16);
        
        // Текст начинается сразу после байта длины (индекс маркера + 6 символов)
        // Вырезаем кусок строки длиной smLength * 2 (так как 1 байт = 2 HEX-символа)
        const alertHex = hex.substring(markerIndex + 6, markerIndex + 6 + (smLength * 2));

        // 2. Вычисляем остальные поля по их реальным границам в пакете
        const length = parseInt(hex.substring(0, 8), 16);
        const sequence = parseInt(hex.substring(24, 32), 16);

        // Текстовые блоки до маркера
        const companyHex = hex.substring(38, 56);       // name_company
        const phoneHex = hex.substring(62, 84);         // phone
        const timestampHex = hex.substring(94, 126);    // timestamp

        result.push({
            json: {
                message: hexToUtf16BE(alertHex).trim(), // Наш декодированный текст
                packetLength: length,
                sequenceNumber: sequence,
                company: hexToAscii(companyHex).trim(),
                phoneNumber: hexToAscii(phoneHex).trim(),
                timestampRaw: hexToAscii(timestampHex).trim(),
                original_hex: hex
            }
        });
    } else {
        result.push({
            json: {
                message: "? Ошибка: В HEX-пакете не найден маркер кодировки 0800",
                original_hex: hex
            }
        });
    }
} else {
    result.push({
        json: {
            message: "? Ошибка: Не удалось прочитать raw_hex или пакет слишком короткий",
            original_hex: hex
        }
    });
}

return result;

После этой ноды мы получаем чистый, структурированный JSON:

{
  "sender": "name_company",
  "phone": "79000000000",
  "text": "Сотрудник Иванов И.И. вошел в здание. Точка доступа: Главный вход",
  "timestamp": "2026-09-25T11:20:00.000Z"
}

Отправка сообщений на e-mail и Telegram

После узла Code JS поток в n8n разветвляется на две параллельные ноды:

  • Нода Email (SMTP): Без проблем отправляет письмо на корпоративную почту. В поле «Тема» подставляется имя отправителя, а в тело письма - декодированный текст из СКУД.

  • Нода Telegram (HTTP Request): С отправкой в Telegram был нюанс. Из-за инфраструктурных ограничений и закрытого сетевого контура нам приходится работать через прокси-сервер и отправлять уведомления в свою закрытую группу (читать пуш уведомления без "танцев"). Стандартная нода Telegram не всегда гибка в таких условиях, поэтому мы использовали универсальную ноду HTTP Request в режиме JSON - шлем прямой POST-запрос на API Телеграма (/sendMessage), передавая chat_id нашей закрытой группы и кастомизированный текст.

Скриншот настроек ноды отправки сообщений в Телеграм

Что в итоге?

  • Нулевая стоимость расширения функционала (время конвертируется в зания, а знания в деньги): мы отказались от платных пакетов СМС-шлюзов и замены оборудования.

  • Контроль и логирование: n8n не просто пересылает сообщения, но и складывает логи. Можно легко посмотреть статистику без необходимости лезть в неповоротливые отчёты PERCo-S-20.

  • Мобильность: Заинтересованные лица получают мгновенные пуши в Telegram на своих смартфонах без дополнительных средств (чтения пуш достаточно) либо через почту, что гораздо удобнее СМС

В данный момент решение, которое было собрано буквально «на коленке» за пару вечеров, работает стабильно с требуемым функционалом.

Комментарии (1)


  1. VegaKita
    28.09.2026 11:07

    Посредством СМС, а не по средствам СМС.