
В СКУД 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 байт состоит из трех блоков:
Заголовок (Header): Первые 16 байт. Содержат длину пакета (
0x00000069= 105 байт), ID команды отправки (0x00000004дляsubmit_sm) и порядковый номер сообщения (Sequence Number) для контроля очереди.-
Обязательные параметры (Body): Метаданные доставки. Отсюда мы выковыриваем два главных поля:
Имя отправителя (Source Address): Буквенный альфа-нумерик (например,
I_SEND_TEST). В будущем письме он станет полем «От кого» или темой.Номер получателя (Destination Address): Телефон в формате
7xxxxxxxxxx. В n8n этот номер используется для маршрутизации или склейки с базой данных сотрудников.
Полезная нагрузка (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 на своих смартфонах без дополнительных средств (чтения пуш достаточно) либо через почту, что гораздо удобнее СМС
В данный момент решение, которое было собрано буквально «на коленке» за пару вечеров, работает стабильно с требуемым функционалом.
VegaKita
Посредством СМС, а не по средствам СМС.