- C 69.6%
- HTML 21.8%
- Python 6.5%
- PHP 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Репозиторий на git.app-biz.ru был создан с README-заглушкой на две строки. Сливаю её историю с историей проекта, чтобы ничего не перезаписывать принудительно; содержимое README берётся из проекта — там полное описание железа, API и режимов работы. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
| .vscode | ||
| deploy | ||
| docs | ||
| main | ||
| release | ||
| tools | ||
| Фото устройства | ||
| .clangd | ||
| .gitignore | ||
| CMakeLists.txt | ||
| partitions.csv | ||
| README.md | ||
| sdkconfig.defaults | ||
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.
Как работает (для серийного производства)
Одна прошивка на все платы. Настройка — без кабеля и программ:
- Первое включение (настроек нет) → плата поднимает Wi-Fi точку
KiwiSMS-Setup-XXXX(парольkiwi12345), LED мигает. - Пользователь подключается к точке и открывает http://192.168.4.1 — там задаёт: свой Wi-Fi, DHCP или статический IP, API-ключ.
- Плата перезагружается, входит в сеть клиента, LED горит ровно — рабочий режим, REST API доступен на заданном IP.
- Заводской сброс: удерживать кнопку 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-Key503— очередь переполнена
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
GET http://<IP платы>/api/status— убедиться чтоregistered: true.POST http://<IP платы>/api/sms- Headers:
Content-Type: application/json(+X-API-Keyесли задан) - Body → raw → JSON:
{"phone":"+79XXXXXXXXX","message":"Test from Postman"}
- Headers:
- Ответ
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