ESPNNHL season preview: Rankings from 1-32 and what to know about every teamRTP DesportoPortugal-Noruega. Elogio de Jesus e Ronaldo titular na DinamarcaThe Jerusalem PostTwo states is not enough: Palestinians need sovereignty, Israelis need security - opinionDaily MaverickMADLANGA COMMISSION: Feroz Khan facing criminal charges for defying subpoena to appear at inquiryPunchMeet Danielle Adewusi, the doctor crowned Miss Universe Nigeria 2026Bollywood HungamaAnkur Rathee says Best Of The Best took him back to his college dancer days: “A version of myself I thought I had left behind”Sky TG24I titoli di Sky TG24 del 28 settembre, edizione delle 13SoompiSM Entertainment Announces Strict Legal Action Against Malicious Posts And Illegal AI-Generated Images Targeting Lim Yoona And aespa’s WinterIl Fatto QuotidianoRanucci-Rai, la destra contro la giudice: “È la stessa del caso Almasri”. I togati del Csm: “Tentativo di condizionamento”Egypt IndependentSisi moves to delegate certain Presidential powers to Defense Minister – here’s what that meansVilaWebEls treballadors de biblioteques de Barcelona critiquen la “tàctica de desgast” de l’Ajuntament després que anul·li la mesa de negociacióThe IndependentFive men arrested over RAF Fairford ‘bomb plot’ are British nationals, police say
The Daily Newsstand · Free, Always
Monday, September 28, 2026

Как мы СМС-трафик из PERCo-S-20 в e-mail и Telegram отправляли

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

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

В СКУД 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 на своих смартфонах без дополнительных средств (чтения пуш достаточно) либо через почту, что гораздо удобнее СМС

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.