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


В СКУД 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 на своих смартфонах без дополнительных средств (чтения пуш достаточно) либо через почту, что гораздо удобнее СМС
В данный момент решение, которое было собрано буквально «на коленке» за пару вечеров, работает стабильно с требуемым функционалом.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.