Meshtastic позволяет обмениваться сообщениями через LoRa mesh-сеть даже без доступа к интернету. Но можно ли связать такую сеть с обычной электронной почтой?

Несколько месяцев назад мой отец купил пару устройств на базе Meshtastic - небольших LoRa-модулей, которые позволяют отправлять текстовые сообщения через mesh-сеть без интернета.

Для базовой связи это работает отлично: устройства автоматически ретранслируют сообщения друг другу и позволяют обмениваться текстом даже там, где нет мобильной связи.

Однако быстро возникла практическая проблема: писать сообщения только внутри Meshtastic-сети неудобно. Большая часть коммуникации всё равно происходит через обычные мессенджеры и email.

Возникла идея: сделать шлюз между электронной почтой и mesh-сетью.

Задачи были следующие:

  • читать новые письма из почтового ящика и отправлять их в mesh-сеть

  • принимать сообщения из mesh-сети и отправлять их обратно по email

После нескольких экспериментов и бессонных ночей появился Python-скрипт, который выполняет эти функции.

Как выглядит система

Финальная конфигурация простая:

[Email Server] <-> [Python Gateway] <-> [Meshtastic Node 1]
                                         |
                                         | LoRa Mesh
                                         |
                     [Phone App] <-> [Meshtastic Node 2]
  • дома работает компьютер, к которому подключены интернет и нода Meshtastic (USB или Bluetooth).

  • Вторая нода находится у пользователя и подключена к телефону через официальное приложение.

  • Python-скрипт превращает компьютер в мост между интернетом и LoRa mesh-сетью.

Архитектура скрипта

Скрипт выполняет три основные задачи:

  1. Проверка почтового ящика через IMAP и чтение новых писем.

  2. Отправка писем в mesh-сеть с фрагментацией сообщений.

  3. Прием сообщений из mesh-сети и отправка их как email.

Конфигурация

Все параметры хранятся в .env файле:

  • EMAIL / PASSWORD — учетные данные почты

  • IMAP_SERVER / SMTP_SERVER - адреса сервера с вашим почтовым ящиком

  • DEST_NODE — ID ноды получателя

  • BLE_NAME — имя Bluetooth устройства

  • ALLOWED_NODE — ID ноды, которой разрешено отправлять email

Скрипт автоматически создаёт .env при первом запуске и предлагает ввести параметры.

def ensure_env():
    if os.path.exists(".env"):
        return
    ...

Подключение к ноде через Bluetooth

Meshtastic ноды можно подключать к компьютеру двумя способами:

  • через USB

  • через Bluetooth

Подключение через USB обычно не вызывает проблем, однако Bluetooth оказался немного сложнее.

Изначально я пробовал подключаться напрямую по BLE-адресу устройства. Однако быстро выяснилось, что после длительного отключения или перезапуска ноды её Bluetooth-адрес может измениться. В результате скрипт переставал находить устройство и соединение терялось.

Чтобы избежать этой проблемы, я решил использовать более устойчивый способ: пользователь указывает только имя ноды, а скрипт сам находит её текущий адрес.

Сначала выполняется сканирование устройств с помощью CLI-утилиты Meshtastic:

def find_ble_address():

    print("Scanning for BLE nodes...")

    result = subprocess.run(
        ["meshtastic", "--ble-scan"],
        capture_output=True,
        text=True,
        timeout=15
    )

Команда meshtastic --ble-scan выводит список доступных BLE-устройств и их адресов. Вот пример вывода команды:

Found: name='ABCD_ab10' address='161929F1-8E3A-5472-8422-DE14A2C51C8A'
Found: name='ABCD_ab11' address='8EC80A96-76BD-FFD8-DC37-3C6AF8CFB511'
BLE scan finished

Скрипт анализирует этот вывод и ищет ноду с нужным именем:

for line in result.stdout.splitlines():

    match = re.search(r"name='([^']+)'.*address='([^']+)'", line)

    if not match:
        continue

    name,address = match.groups()

    if name == settings["BLE_NAME"]:
        return address

После этого используется найденный адрес для подключения:

return BLEInterface(address=addr)

В результате пользователю нужно указать только имя ноды в .env:

BLE_NAME=Meshtastic_1234

А скрипт автоматически найдёт её текущий BLE-адрес при запуске.
Это делает подключение намного стабильнее, особенно если нода часто перезапускается или долго находится вне зоны соединения.

Чтение и отправка почты

IMAP-подключение:

mail = imaplib.IMAP4_SSL(settings["IMAP_SERVER"],settings["IMAP_PORT"])
mail.login(settings["EMAIL"],settings["PASSWORD"])
mail.select("INBOX")
status,data = mail.search(None,"UNSEEN")

Скрипт извлекает отправителя, тему и тело письма. Для обработки MIME используется библиотека pyzmail, которая упрощает работу с различными форматами писем.

SMTP-подключение:

server = smtplib.SMTP(settings["SMTP_SERVER"],settings["SMTP_PORT"],timeout=10)
server.ehlo()
server.starttls()

server.login(settings["EMAIL"],settings["PASSWORD"])
server.send_message(msg)

Для создания письма также используется MIME.

msg = MIMEText(body)

msg["From"] = settings["EMAIL"]
msg["To"] = to_addr
msg["Subject"] = subject

Ограничения LoRa и фрагментация сообщений

LoRa поддерживает только небольшие пакеты данных. Полное письмо может быть длиннее допустимого размера.

Решение — разбивка письма ��а фрагменты.

def split_message(text, size):
    msg_id = generate_msg_id()
    total = (len(text)+size-1)//size
    return [
        f"MAIL {msg_id} {i//size+1}/{total}\n{text[i:i+size]}"
        for i in range(0, len(text), size)
    ]

Каждому фрагменту присваивается случайный ID и номер пакета. Это позволяет корректно собирать сообщения на принимающей стороне.

Отправка в mesh-сеть

Фрагменты отправляются через API Meshtastic.

iface.sendText(
    packet_text,
    destinationId=settings["DEST_NODE"]
)
time.sleep(3)

Между отправками добавлена небольшая задержка для минимизации потерь в сети. Скрипт поддерживает как USB, так и Bluetooth подключение к ноде.

Вот примеры того как выглядят отправленная почта через меш
Вот примеры того как выглядят отправленная почта через меш
И вот как она выглядит в приложении Meshtastic
И вот как она выглядит в приложении Meshtastic

Отправка email из mesh-сети

Для обратной отправки email используется простой текстовый формат: EML|адрес_получателя|тема|текст_сообщения. Скрипт проверяет входящие сообщения: если сообщение начинается с EML|, оно интерпретируется как команду на отправку email. После успешной отправки письма шлюз возвращает подтверждение обратно в mesh-сеть.

def parse_mesh_email(text):
    if not text.startswith("EML|"):
        return None
    parts = text.split("|",3)
    if len(parts) < 4:
        return None
    return parts[1].strip(), parts[2].strip(), parts[3].strip()

После успешной отправки email скрипт возвращает подтверждение в mesh-сеть:

send_email(to_addr,subject,body)

confirm = f"EMAIL SENT: {to_addr}"
iface.sendText(confirm, destinationId=settings["DEST_NODE"])

Проблема с ID

Meshtastic отображает ID ноды в шестнадцатеричном формате, а API возвращает десятичный. Из-за этого проверка отправителя не работала.

sender = str(packet.get("from"))

Чтобы не мучать пользователя с переводом одного в другое, пришлось немного нахулиганить и сделать такое преобразование при получении ID.

allowed = str(int(settings["ALLOWED_NODE"][1:],16))

sender = str(packet.get("from"))

После этого фильтрация сообщений заработала нормально.

Обработка входящих сообщений из mesh-сети

Для приема сообщений используется механизм подписки на события библиотеки Meshtastic.

При запуске скрипт подписывается на событие получения пакета:

pub.subscribe(on_receive, "meshtastic.receive")

После этого каждый входящий пакет передается в функцию on_receive

def on_receive(packet):

Первым делом проверяется, удалось ли корректно декодировать сообщение:

if "decoded" not in packet:
    return

Далее выполняется проверка отправителя. Скрипт обрабатывает сообщения только от заранее разрешённой ноды.

sender = str(packet.get("from"))

if sender != str(int(settings["ALLOWED_NODE"][1:],16)):
    return

Это простая защита от случайных или чужих сообщений в сети.

Также добавлена защита от повторной обработки одного и того же пакета. Иногда в mesh-сети сообщение может прийти несколько раз из-за ретрансляции.

packet_id = packet.get("id")

if packet_id in recent_packets:
    return

recent_packets.add(packet_id)

Если сообщение уже обрабатывалось, оно игнорируется.

После этого из пакета извлекается текст сообщения, который затем анализируется и при необходимости интерпретируется как команда на отправку email.

Почему именно email

Возникает логичный вопрос: зачем вообще использовать email поверх LoRa-сети?

На первый взгляд это выглядит странно, ведь Meshtastic уже поддерживает обмен текстовыми сообщениями. Однако у email есть несколько практических преимуществ.

Во-первых, это универсальный и очень простой интерфейс. Практически любое устройство уже умеет отправлять письма, поэтому для взаимодействия с mesh-сетью не требуется устанавливать дополнительное программное обеспечение и покупать Meshtastic.

Во-вторых, email хорошо подходит в качестве асинхронного транспорта. Сообщения могут накапливаться в почтовом ящике и отправляться в сеть по мере появления соединения с шлюзом.

Кроме того, такой подход позволяет легко интегрировать LoRa-сеть с существующей инфраструктурой: автоматическими уведомлениями, системами мониторинга или простыми скриптами.

В результате email выступает не столько как основной способ общения, сколько как удобный мост между обычным интернетом и автономной mesh-сетью.

Итог

В результате получился простой шлюз между электронной почтой и LoRa mesh-сетью на базе Meshtastic. Весь шлюз реализован примерно в ~400 строках Python-кода, а скомпелированный файл занимает ~14MB

Скрипт позволяет:

  • получать email через Meshtastic mesh-сеть

  • отправлять email из mesh-сети

  • использовать обычную почту как транспорт поверх LoRa

Это рабочее решение для экспериментов, полевых условий или просто интересных проектов на базе Meshtastic.

Код проекта доступен на GitHub, также есть скомпилированный бинарник для macOS который можно запускать напрямую: https://github.com/Nnnnest/mesh-email-gateway