Разработка СМС сервера на основе ESP32
  • C 69.6%
  • HTML 21.8%
  • Python 6.5%
  • PHP 2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
claude_bot b28431c772 Слияние с начальным коммитом репозитория
Репозиторий на git.app-biz.ru был создан с README-заглушкой на две
строки. Сливаю её историю с историей проекта, чтобы ничего не
перезаписывать принудительно; содержимое README берётся из проекта —
там полное описание железа, API и режимов работы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 00:40:42 +11:00
.vscode Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
deploy Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
docs Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
main Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
release Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
tools Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
Фото устройства Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
.clangd Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
.gitignore Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
CMakeLists.txt Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
partitions.csv Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
README.md Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00
sdkconfig.defaults Первый импорт: KiwiBOT SMS Server 2026-09-14 00:39:20 +11:00

KiwiBOT SMS Server — v1.0.0.1

Поддержка: Константин Фатеев · 8 984 139-09-93 (MAX, WhatsApp, Telegram) · t.me/S1PProg

Вход в панель управления

При первом запуске действуют заводские admin / admin — админка сразу предупредит об этом красной плашкой. Сменить их: Настройки → Безопасность.

Пароль хранится не в открытом виде, а солёным хешем SHA-256: даже прочитав память устройства, восстановить его нельзя. Сессия живёт 8 часов и пропадает при перезагрузке.

Вход по СМС (двухэтапная проверка)

Настройки → Безопасность → Вход по СМС: указываете номер, хаб шлёт на него код, вводите его — проверка включена. После этого вход в админку идёт в два шага: логин с паролем, затем одноразовый шестизначный код из СМС.

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

Важно: этот вход защищает только админку. Рабочее API отправки и приёма СМС по-прежнему авторизуется ключом X-API-Key — интеграция не ломается.


Kiwi SMS Host

SMS-шлюз на ESP32 (WROOM-32) + LTE-модем SIMCom A7682E. Прошивка ESP-IDF (C). Принимает HTTP-запрос с номером и текстом — отправляет СМС через сотовую сеть.

Железо

A7682E (гребёнка) Провод ESP32 DevKit V1
пин 1 GND чёрный GND
пин 2 URX синий TX2 (GPIO17)
пин 3 UTX белый RX2 (GPIO16)
пин 4 PWRK жёлтый D4 (GPIO4) — автовключение модема
пин 5 VDD красный VIN (5V)

SIM вставлена (PIN отключён), LTE-антенна на u.fl 4G.

Как работает (для серийного производства)

Одна прошивка на все платы. Настройка — без кабеля и программ:

  1. Первое включение (настроек нет) → плата поднимает Wi-Fi точку KiwiSMS-Setup-XXXX (пароль kiwi12345), LED мигает.
  2. Пользователь подключается к точке и открывает http://192.168.4.1 — там задаёт: свой Wi-Fi, DHCP или статический IP, API-ключ.
  3. Плата перезагружается, входит в сеть клиента, LED горит ровно — рабочий режим, REST API доступен на заданном IP.
  4. Заводской сброс: удерживать кнопку BOOT 5 секунд → настройки стёрты, снова режим настройки.

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

REST API

Если при настройке задан API-ключ — каждый запрос обязан нести заголовок X-API-Key: <ключ>.

POST /api/sms — поставить СМС в очередь отправки

{ "phone": "+79XXXXXXXXX", "message": "Привет, бро! 🚀" }
  • phone — международный формат: + и 10–15 цифр
  • message — латиница до 160 символов, либо кириллица и эмодзи до 70

Алфавит определяется автоматически: если в тексте есть символы вне ASCII, сообщение уходит в кодировке UCS2 (AT+CSCS="UCS2", DCS=8). Эмодзи вне базовой плоскости (😀) кодируются суррогатными парами. Лимит 70 символов для не-латиницы — ограничение стандарта GSM, а не прошивки.

Ответы:

  • 202 — {"status":"queued","uuid":"3f2a9c14-7b0e-4d81-9a52-6c1e8f0b4d73","queue_position":1,"uid":"KWI_000042"}
  • 400 — кривой JSON/номер/текст
  • 401 — нет или неверный X-API-Key
  • 503 — очередь переполнена

uuid — идентификатор этого сообщения. Выдаётся каждому сообщению, независимо от настроек, и живёт с ним до конца: по нему сообщение видно в очереди, в истории и в GET /api/sms/status. Сохраняйте его у себя рядом с записью о рассылке — без него узнать судьбу конкретного сообщения нельзя.

Не путать с uid (KWI_000042): тот дописывается в текст сообщения, чтобы получатель отличил повтор, и настройкой отключается.

GET /api/sms/status?uuid=... — что стало с сообщением

{ "status":"ok", "found":true,
  "uuid":"3f2a9c14-7b0e-4d81-9a52-6c1e8f0b4d73",
  "phone":"+79XXXXXXXXX", "text":"Привет!", "type":"transactional",
  "state":"sent", "in_queue":false, "ts":1785200000, "modem_id":17 }
state Что значит
pending ждёт в очереди; queue_position — место (1 = уйдёт следующим)
sending уходит прямо сейчас
sent принято SMS-центром оператора (не подтверждение доставки)
failed не ушло: err — текст отказа, fail_kind — его характер

fail_kind: bad_number — номер не принимает СМС, пропустите его; rejected — отказал оператор, рассылку надо прерывать; temporary — сбой сети, можно повторить.

404 значит «не найдено», а не «не отправлено». В памяти лежат последние 20 завершённых отправок, старые вытесняются новыми — спрашивайте статус в разумный срок после отправки, а не через сутки.

Общий обзор — во вкладке «Очередь» админки и в GET /api/sms/queue (до 24 ожидающих и до 16 последних завершённых, у каждой записи свой uuid).

GET /api/status — состояние

{ "status":"ok", "ip":"192.168.1.50", "modem_online":true,
  "model":"A7682E-LASA-R", "rssi":21, "registered":true,
  "operator":"0,2,\"25099\",7" }

rssi: 0–31 (больше = лучше), 99 = нет сигнала.

Анти-бан защита (очередь отправки)

Операторы блокируют SIM за пулемётную рассылку, поэтому отправка идёт через очередь в памяти устройства. POST /api/sms кладёт сообщение в очередь и сразу отвечает 202 Accepted — фоновая задача отправляет его по расписанию.

Настраивается во вкладке «Анти-бан»:

  • интервал между СМС (секунды; 0 = слать сразу);
  • длинная пауза: после N отправленных сообщений пауза на M минут;
  • уникальный ID: в конец текста дописывается id:ПРЕ_000123 — 3-символьный префикс + счётчик заданной длины (1–10 цифр). Счётчик хранится в NVS и не повторяется после перезагрузки;
  • стоп-пауза при отказе оператора (см. ниже).

Ёмкость очереди — 64 сообщения; при переполнении API отвечает 503.

Реакция на блокировку оператором

Если оператор отклоняет сообщение (+CMS ERROR: 21, 500, 512… — типичное срабатывание антиспам-фильтра), устройство немедленно останавливает всю отправку на заданное в настройках время (по умолчанию 30 минут), а API начинает отвечать на POST /api/sms кодом 429:

{ "status":"blocked", "reason":"operator_reject",
  "detail":"оператор отклонил отправку (CMS ERROR 21)",
  "cms_error":21, "retry_after_s":1782, "queued":false,
  "action":"остановите рассылку до истечения паузы" }

Заголовок Retry-After содержит то же значение в секундах. Отправляющая система по коду 429 обязана прервать рассылку — иначе SIM-карту заблокируют. Состояние видно в админке (красный баннер) и в queue.banned дашборда. Снять паузу досрочно: POST /api/sms/queue/resume или кнопка в админке.

Отчёт о реальной доставке (важно!)

+CMGS: OK от модема означает только, что SMSC принял сообщение — не то, что оно дошло до трубки. Оператор может молча не доставить его (например, антиспам-фильтр SMSC), и без отдельного запроса это никак не увидеть.

Прошивка запрашивает отчёт о доставке (SRR-бит в AT+CSMP) и до ~8 секунд слушает +CDS после каждой отправки. Реальный статус — в поле delivery записи очереди (GET /api/sms/queue):

state delivery Что значит
sent delivered Оператор подтвердил: дошла до трубки
sent failed SMSC принял, но сеть НЕ доставила — см. dlr_raw
sent pending Сеть ещё передаёт
sent (пусто) Отчёт не пришёл за 8с — не значит, что не дошло, просто нет данных
failed — Отказ ещё на этапе AT+CMGS (см. err)

В админке (вкладка Очередь) это отражено честными статусами: ✅ доставлено / ⚠️ сеть НЕ доставила / 🕓 сеть передаёт / 📨 принято SMSC.

Чтение входящих СМС

Входящие не лежат в SIM и не ждут там запроса. Фоновая задача забирает их из памяти SIM сразу по приходу и держит в своём буфере (50 шт., дублируется во flash и переживает перезагрузку). Так сделано вынужденно: память SIM вмещает от 10 до 30 сообщений в зависимости от карты, и переполненная блокирует приём новых на уровне сети — её нужно освобождать немедленно, а не когда интегратор соберётся забрать почту.

Устройство чистит SIM само, когда в ней остаётся 2 свободные ячейки, и только в тихий момент (минута без новых сообщений): удаление — это запись в память карты, и попасть ею в момент приёма означает сломать приём до перезапуска модема. Удаляются поимённо только те сообщения, которые уже перенесены в буфер и во flash; пришедшее во время очистки не трогается.

Буфер устройства — окно на последние 50 сообщений. Забранное по API больше вам не выдаётся (каждое сообщение вы получаете ровно один раз), но с устройства не исчезает: владелец видит его в админке со статусом «забрано», пока запись не вытеснят новые. При заполнении буфера первыми уходят самые старые из уже забранных, и только если таких нет — самое старое вообще.

Статусы входящего сообщения

status Что значит
new пришло, никем не забрано
delivering отдано вам в режиме ack=0, ждём подтверждения
taken забрано вами по API
acked вы подтвердили обработку через /ack
read прочитано вручную в админке, вам ещё не выдавалось

Переходы идут только вперёд: new → delivering → taken → acked. Ветка read — когда владелец прочитал сообщение в админке раньше, чем его забрала интеграция.

Кириллица и эмодзи декодируются автоматически, включая символы вне базовой плоскости (😀 приходит суррогатной парой D83D DE00 и склеивается в один символ).

Запрос Что делает
GET /api/sms/inbox новые сообщения и помечает их забранными (вам больше не выдаются)
GET /api/sms/inbox?ack=0 новые, ничего не помечает — подтвердите сами через /ack
GET /api/sms/inbox?mark_read=0 новые, статус «новое» не снимается
GET /api/sms/inbox?all=1 всё содержимое буфера, включая забранное; ничего не меняет
POST /api/sms/inbox/ack подтвердить обработку: {"ids":[7,8,9]}
POST /api/sms/inbox/read пометить прочитанными: {} — все новые, {"id":N} — одно
POST /api/sms/inbox/delete удалить из буфера: {"id":N}, пустое тело — весь буфер
POST /api/sms/inbox/clear очистить и буфер устройства, и память SIM/модема

Когда помечается забранным — точный порядок

Обычный GET /api/sms/inbox помечает выданное только после того, как ответ успешно ушёл к вам, а не до. Если связь оборвётся на передаче, пометка снимается и сообщения придут в следующем запросе.

Обратная сторона: при обрыве возможен повтор. Отсеивайте уже обработанные по полю id — оно сквозное и не переиспользуется.

Если потеря недопустима даже теоретически, работайте в два шага: GET /api/sms/inbox?ack=0 — сообщения не помечаются; записали у себя — POST /api/sms/inbox/ack со списком ids.

Формат сообщения в ответе:

{ "id":7, "message_id":"MSG-7",
  "from":"+79XXXXXXXXX", "text":"Текст сообщения",
  "time":"2026-07-26 14:30:00", "time_raw":"26/07/26,14:30:00+12",
  "received_ts":1785200000, "unread":true, "taken":false, "status":"new" }

Ответ также содержит:

Поле Что означает
count сколько сообщений выдано в этом ответе
unread_total сколько непрочитанных осталось в буфере
buffer_used / buffer_cap занятость буфера устройства (50 мест)
mem_used / mem_total занятость памяти SIM
marked_read, acked сколько помечено прочитанными / выданными

buffer_used и count — разные вещи: при полном буфере уже прочитанных сообщений count будет нулём. Следить за заполнением надо по buffer_used: дойдёт до buffer_cap — самые старые начнут вытесняться.

Разделение поведения: API по умолчанию забирает сообщения себе — интегратор получает каждое ровно один раз и отвечает за него. Админка только показывает: она запрашивает ack=0 и ничего не помечает, поэтому открытая вкладка «Входящие» не крадёт сообщения у вашей интеграции — и наоборот, видит всё, что интеграция уже забрала. Снять статус «новое» в браузере можно галочкой ✓ на сообщении или кнопкой «Всё прочитано».

Админ-панель

http://<IP платы>/admin — встроенная веб-админка (корень / тоже ведёт туда):

  • дашборд: отправлено СМС / сбои / API-запросы / отказы;
  • модем и SIM: модель, сигнал (бары), регистрация, оператор, номер, IMEI, ICCID;
  • баланс SIM по USSD (код настраивается: Билайн *102#, МТС/Мегафон *100#);
  • журнал событий и журнал API-запросов (кто, откуда, код ответа) с реальным временем (SNTP);
  • настройки: API-ключ, USSD-код, отображаемый номер;
  • сервис: перезапуск модема (AT+CRESET), перезагрузка платы, заводской сброс.

Если задан API-ключ, админка спросит его при первом открытии (хранится в браузере).

Служебные эндпоинты: GET /api/admin/dashboard|events|apilog, POST /api/admin/balance|settings|network|at|reboot|modem-restart|factory-reset — все требуют X-API-Key, если ключ задан.

Вкладка 📖 API — встроенный справочник всех методов с примерами запросов и ответов плюс интерактивный тестер: выбираете готовый запрос, правите тело, жмёте «Отправить» и сразу видите ответ устройства, код и время. Там же готовая команда curl для копирования.

Вкладка ⚙️ Настройки → AT-консоль — прямые AT-команды модему для диагностики (состояние хранилищ СМС, сигнал, регистрация, СМС-центр).

Тест через Postman

  1. GET http://<IP платы>/api/status — убедиться что registered: true.
  2. POST http://<IP платы>/api/sms
    • Headers: Content-Type: application/json (+ X-API-Key если задан)
    • Body → raw → JSON: {"phone":"+79XXXXXXXXX","message":"Test from Postman"}
  3. Ответ 200 + СМС на телефоне.

Сборка и прошивка (VS Code + ESP-IDF v5.2)

idf.py set-target esp32
idf.py -p COM19 build flash monitor

Выход из монитора: Ctrl+].

Структура прошивки

main/
├── main.c        — запуск, выбор режима, LED, кнопка сброса
├── config.h      — константы железа (пины, тайминги)
├── app_config.*  — настройки пользователя в NVS
├── wifi_mgr.*    — Wi-Fi: STA (DHCP/статика) и AP настройки
├── portal.*      — веб-портал первичной настройки (192.168.4.1)
├── modem.*       — драйвер A7682E: PWRK, AT-движок, отправка СМС
└── http_api.*    — REST API: POST /api/sms, GET /api/status