Я давно хотел попробовать не просто LoRa-модули как отдельные радиожелезки, а именно Meshtastic - готовую систему для обмена сообщениями без сотовой сети, Wi-Fi и интернета.
Идея Meshtastic выглядит очень привлекательно: берём несколько маленьких устройств с LoRa, прошиваем их, настраиваем общий канал - и получаем автономную mesh-сеть для коротких сообщений. Без SIM-карт, без роутеров, без облаков и без базовых станций оператора.
Но мне было интересно пойти чуть дальше простого “привет, как слышно”. Я решил сделать эксперимент: подключить одну Meshtastic-ноду к ноутбуку, принимать с неё сообщения, отправлять их в локальную языковую модель через Ollama, а ответ нейросети возвращать обратно в mesh.
В итоге получилась такая схема:
Meshtastic-нода №2 ↓ LoRa Meshtastic-нода №1 ↓ USB Python-скрипт на ноутбуке ↓ HTTP Ollama + локальная LLM ↓ Python-скрипт ↓ USB Meshtastic-нода №1 ↓ LoRa Meshtastic-нода №2
То есть фактически - радиоинтерфейс к локальной нейросети.
В этой статье расскажу:
что такое Meshtastic;
как он работает поверх LoRa;
почему в больших городских сетях важны общие настройки;
как я настроил две ноды Heltec ESP32 LoRa 32 V4;
какое расстояние получилось получить в моём тесте;
как я подключил Meshtastic к локальной модели на ноутбуке;
и какие выводы сделал после эксперимента.
Что такое Meshtastic
Meshtastic - это open-source проект, который позволяет использовать недорогие LoRa-радиомодули как платформу для дальней автономной связи там, где нет обычной инфраструктуры связи.
Если говорить проще, Meshtastic - это что-то среднее между:
рацией;
очень простым текстовым мессенджером;
mesh-сетью для коротких сообщений и телеметрии.
Обычный мессенджер вроде Telegram или WhatsApp требует интернет, серверы, мобильную сеть или Wi-Fi. Meshtastic работает иначе: телефон или ноутбук подключается к локальной ноде по Bluetooth, Wi-Fi или USB, а сама нода уже передаёт сообщения в радиоэфир через LoRa.
Важно: Meshtastic - это не интернет через LoRa. Это низкоскоростной радиоканал для коротких сообщений, координат, телеметрии и простых команд.
Главная особенность - интернет не нужен для самого обмена сообщениями. Ноды могут ретранслировать сообщения друг друга, за счёт чего получается mesh-сеть: если одна нода не добивает напрямую до другой, сообщение может пройти через промежуточные узлы.
Meshtastic не является LoRaWAN, Helium или TTN. Это отдельная peer-to-peer/mesh-система, которая использует LoRa как радиотранспорт.
Как это работает: LoRa, mesh и прыжки
В основе Meshtastic лежит LoRa - радиотехнология для передачи небольших объёмов данных на большие расстояния с низким энергопотреблением.
За дальность приходится платить скоростью. LoRa - это не Wi-Fi на километры. Это скорее радиоканал для коротких сообщений.
Meshtastic поверх LoRa добавляет удобную логику:
у каждой ноды есть свой идентификатор;
сообщения могут отправляться в общий канал;
сообщения могут отправляться конкретной ноде;
ноды могут ретранслировать пакеты;
у сообщения есть ограничение по числу ретрансляций.
Это число ретрансляций называется hop limit.
Если совсем упростить: hop limit показывает, сколько раз пакет ещё можно переслать дальше.
Например, если hop limit равен 3, сообщение может пройти через несколько промежуточных нод, но не будет бесконечно гулять по сети.
Нода А → Нода Б → Нода В → Нода Г hop 1 hop 2 hop 3
Это особенно важно для больших сетей. Если каждый узел будет постоянно ретранслировать всё подряд с большим hop limit, эфир быстро забьётся повторами.
Почему сообщения должны быть короткими
Одно из главных ограничений Meshtastic - размер пакета.
В документации Meshtastic указывается ограничение полезной нагрузки пакета примерно до 237 байт без protobuf overhead. Это очень мало.
Для английского текста это может быть несколько коротких фраз. Для русского текста меньше, потому что кириллица в UTF-8 занимает больше байт. А если добавить служебные накладные расходы, становится понятно: Meshtastic не предназначен для длинных сообщений, кода, статей, логов и простыней текста.
Вывод: Meshtastic отлично подходит для коротких сообщений, но плохо подходит для длинных ответов нейросети.
Поэтому при подключении локальной LLM это ограничение становится критичным. Нейросеть по умолчанию любит отвечать подробно, а LoRa-сеть этого не любит.
В моём мосте я сразу заложил несколько правил:
бот реагирует только на сообщения с префиксом
/ai;ответ модели ограничивается по длине;
модель получает системный промпт “отвечай очень кратко”;
reasoning/thinking отключается или вырезается;
длинный ответ при необходимости режется на части.
Иначе LLM легко сгенерирует абзац на несколько тысяч символов, который просто не имеет смысла пытаться гонять через LoRa.
Московский Meshtastic и переход на MEDIUM_FAST
Отдельный интересный момент - настройки московского сообщества Meshtastic.
В московском канале было объявление, что с 16 апреля Москва переходит на MEDIUM_FAST. https://t.me/meshtastic_moscow/82432/169148. Там же было сказано, что на этот пресет уже перешло больше 74% устройств, а цель перехода - улучшить работу сети и вернуть ей возможность для дальнейшего роста.
В объявлении рекомендовались такие параметры для московской сети 868 МГц:
Шаблон: MEDIUM_FAST Частота слота: 2 Переопределить частоту: 869.075 Число прыжков: 5-7 Канал: MediumFast или пустой Primary Channel PSK: AQ==
Также отдельно рекомендовалось увеличить период трансляции местоположения до 12+ часов и отключить умную позицию, если другое не требуется для своего сценария.
В больших городских mesh-сетях проблема не только в дальности, но и в загрузке эфира. Чем больше устройств, тем важнее уменьшать лишний трафик, не флудить позициями, не ставить слишком частые телеметрии и выбирать настройки, которые позволяют сети жить при большем числе участников.
MEDIUM_FAST - это компромисс в сторону меньшей задержки и меньшего времени пакета в эфире. Пакет быстрее освобождает эфир, но дальность может быть ниже, чем у более дальнобойных пресетов.
Важно: настройки сообщества - не универсальная инструкция для всех стран, городов и сценариев. Частоты, мощность и допустимые параметры передачи регулируются законодательством конкретной страны.
В статье я показываю свои настройки и настройки московского сообщества как часть моего эксперимента.
Моё железо
Для эксперимента я использовал две одинаковые ноды:
Heltec ESP32 LoRa 32 V4
Одна нода была базовой: она стояла у окна и была подключена к ноутбуку по USB.
Вторая нода была мобильной: я держал её в руке и отходил с ней от базовой точки.
Обе ноды были настроены на один канал и могли обмениваться сообщениями между собой.


Настройки Meshtastic: что именно я включил и почему
Каждая настройка влияет либо на совместимость с сетью, либо на дальность, либо на загрузку эфира, либо на энергопотребление.
Поэтому отдельно разберу свой конфиг.
На момент эксперимента у меня были такие настройки LoRa:
Параметр |
Значение в моём тесте |
|---|---|
Регион / страна |
|
Шаблон |
|
Передача включена |
да |
Hop limit / количество прыжков |
|
Частота слота |
|
RX Boost |
включён |
Override frequency |
|
TX power |
|

NZ_865, MEDIUM_FAST, frequency slot 2, override frequency 869.075, RX Boost включён, мощность 20 dBm.Почему я выбрал New Zealand 865MHz
Самый спорный пункт в моём конфиге - регион New Zealand 865MHz.
Я выбрал по чисто практической причине: в моём приложении/прошивке при выборе региона Russia мощность передатчика ограничивалась значением 20 dBm, а при выборе New Zealand 865MHz интерфейс позволял выставлять мощность выше - вплоть до 28 dBm.
На скрине у меня стоит 20 dBm, но сам выбор региона был связан именно с возможностью проверить, как устройство ведёт себя с более высокой доступной мощностью.
Здесь важно понимать шкалу:
20 dBm ≈ 100 мВт 28 dBm ≈ 630 мВт
Разница в 8 dB - это примерно в 6,3 раза больше по мощности передатчика. Но это не значит, что дальность тоже станет в 6,3 раза больше. В реальности дальность упирается не только в мощность, но и в:
антенны;
высоту установки;
прямую видимость;
зону Френеля;
застройку;
помехи;
чувствительность приёмника;
выбранный пресет LoRa;
загрузку эфира.
И есть ещё практическая сторона: при высокой мощности передатчик заметнее греется, а портативная нода быстрее разряжает аккумулятор. Поэтому выкрутить мощность - не бесплатная оптимизация, особенно если устройство работает от батареи.
Регион влияет на частотный план и ограничения, заложенные в прошивке. Поэтому я не рекомендую слепо повторять мой выбор
NZ_865. Это был экспериментальный стенд. Для реальной эксплуатации нужно выбирать региональный профиль, соответствующий вашей стране и локальному радиорегулированию.
Особенно аккуратно нужно относиться к Override Frequency. При его использовании обычный расчёт частоты по каналу игнорируется, а пользователь обязан соблюдать местные законы и правила.
Что делают параметры на экране LoRa
Регион / страна
Регион задаёт частотный план, набор доступных frequency slot’ов и ограничения, которые прошивка применяет для выбранной страны/диапазона. Это не просто подпись в интерфейсе.
Если выбрать другой регион, устройство может начать работать в другом участке спектра или открыть настройки мощности, которые не должны использоваться в вашей стране.
Использовать шаблон
Переключатель «Использовать шаблон» включает готовый modem preset.
То есть вместо ручной настройки:
bandwidth;
spreading factor;
coding rate;
ты выбираешь готовый профиль, например:
SHORT_FAST MEDIUM_FAST LONG_FAST LONG_SLOW
Это удобно, потому что все участники одной сети должны использовать совместимые радиопараметры. Если у одной ноды LONG_FAST, а у другой MEDIUM_FAST, они просто не будут нормально слышать друг друга.
Шаблон MEDIUM_FAST
MEDIUM_FAST - это компромисс в сторону скорости и меньшего времени пакета в эфире.
Для Medium Fast обычно указываются такие характеристики:
Параметр |
Значение |
|---|---|
Data rate |
около |
Spreading Factor |
|
Bandwidth |
|
Link Budget |
около |
Для сравнения, Long Fast медленнее, но имеет больший link budget. То есть MEDIUM_FAST быстрее освобождает эфир, но может проигрывать в дальности более медленным пресетам.
Почему мне это было удобно?
Потому что мой сценарий - не “передать одну координату раз в час”, а отправлять текстовые запросы и ответы от LLM. Для этого важно, чтобы пакеты не висели в эфире слишком долго.
Цена - потенциально меньшая дальность, чем у более медленных пресетов.
Игнорировать MQTT
Этот переключатель означает: игнорировать сообщения, которые пришли через LoRa, но где-то по пути были занесены в сеть через MQTT.
Это полезно, если хочется видеть только “чистый RF”, то есть пакеты, которые реально пришли по радио, а не через интернет-шлюз.
У меня он выключен, потому что MQTT в моём эксперименте не использовался.
OK в MQTT
OK to MQTT - это разрешение на то, чтобы другие узлы могли отправлять ваши пакеты в MQTT-брокеры.
Важно: это не криптографическая защита, а “вежливая просьба”, которую соблюдает официальная прошивка.
У меня OK to MQTT выключен, потому что я не хотел завязывать эксперимент на интернет-мосты. Вся идея была в том, чтобы проверить локальный радиоканал и локальную LLM.
Передача включена
Transmit Enabled включает или выключает передачу LoRa.
Если выключить - нода сможет слушать, но не будет передавать. Это полезно для некоторых тестов, мониторинга или безопасной замены антенн.
В моём случае передача включена, потому что нода должна была не только принимать запрос от второй ноды, но и отправлять ответ от локальной модели обратно.
Переопределить рабочий цикл
На скрине этот пункт выключен.
Override Duty Cycle Limit нужен для игнорирования лимита рабочего цикла передачи. Этот переключатель лучше не трогать, если вы точно не понимаете, что делаете.
Практическое правило: если вы участвуете в публичной сети, не отключайте ограничения эфира просто потому, что “так будет мощнее”. Это может вредить всей сети и нарушать регуляторные ограничения.
Количество прыжков / Hop limit
Hop limit задаёт, сколько раз пакет может быть ретранслирован другими нодами.
У меня стояло 7.
Почему так? Потому что я ориентировался на настройки московского Meshtastic-сообщества, где для городской сети рекомендовалось значение 5–7.
Но если говорить инженерно, для маленького приватного эксперимента из двух нод это избыточно. Для сети из двух устройств пакетам почти некуда прыгать. А в большой сети слишком высокий hop limit может создавать лишние ретрансляции и забивать эфир.
Частота слота
Frequency Slot - это номер частотного слота, то есть способ выбрать фактическую радиочастоту в рамках текущего региона и пресета.
В Meshtastic есть два разных понятия:
Понятие |
Что означает |
|---|---|
Messaging channel |
логический канал сообщений, имя и PSK |
Frequency slot |
радиочастотный слот, то есть где физически передаёт LoRa |
У меня стоял slot 2, потому что это значение рекомендовалось московским сообществом для MEDIUM_FAST.
RX Boost
RX Boost включает boosted gain mode для чипов серии SX126x.
По-простому: приёмник потребляет немного больше энергии, но может получить чуть лучшую чувствительность.
Для ноды у окна, подключенной к ноутбуку по USB, это почти бесплатная настройка: питание есть, экономия батареи не так важна.
Для мобильной ноды на аккумуляторе уже стоит подумать, нужен ли RX Boost постоянно.
Переопределить частоту
У меня было указано:
869.075 MHz
Это значение было взято из настроек московского сообщества.
Когда включён Override Frequency, устройство перестаёт рассчитывать частоту по региону, пресету и frequency slot и использует указанное значение напрямую.
Роль устройства: почему CLIENT_MUTE

CLIENT_MUTE, режим ретрансляции - ALL, интервал вещания информации об узле - 3 часа.Главный параметр здесь - роль устройства.
В Meshtastic роль определяет, как нода ведёт себя в сети: просто отправляет и принимает свои сообщения, помогает ретранслировать чужие, работает как базовая станция, трекер, сенсор и так далее.
CLIENT
CLIENT - обычная универсальная роль.
Такую роль стоит выбирать, если нода:
используется как обычное пользовательское устройство;
может участвовать в mesh;
может ретранслировать чужие сообщения;
не стоит в перегруженной сети, где каждый лишний ретранслятор создаёт шум.
Для большинства одиночных нод это нормальный выбор.
CLIENT_MUTE
У меня стоял CLIENT_MUTE.
Это почти как CLIENT, но с одним ключевым отличием: такая нода не ретранслирует чужие сообщения.
У меня нода работала как интерфейс к ноутбуку и локальной модели. Я не ставил её на крышу как общественный ретранслятор и не хотел, чтобы она без необходимости участвовала в ретрансляции чужого трафика.
Для AI-моста CLIENT_MUTE - довольно аккуратный выбор:
нода принимает сообщения, адресованные ей или её каналу;
нода отправляет свои ответы;
но не добавляет лишних ретрансляций в эфир.
Такая нода не помогает сети расширяться. Если вы ставите устройство высоко, у окна, на балконе или крыше и хотите помогать локальной сети, лучше рассмотреть CLIENT или CLIENT_BASE.
CLIENT_BASE
CLIENT_BASE - роль для более стационарной “базовой” ноды.
Официально Meshtastic описывает CLIENT_BASE как роль, похожую на CLIENT, но с приоритетом при ретрансляции сообщений от избранных нод или к избранным нодам. То есть она полезна, если у тебя есть сильная хорошо расположенная “домашняя база”, а остальные твои устройства отмечены как favorites.
Если развивать мой проект дальше, логика могла бы быть такой:
нода у окна или на крыше:
CLIENT_BASE;мобильная нода в руке:
CLIENT_MUTEилиCLIENT;лишние домашние ноды:
CLIENT_MUTE, чтобы не плодить ретрансляции.
ROUTER и REPEATER
ROUTER и REPEATER имеют смысл только для устройств в стратегически важных местах: высоко, с хорошей антенной, с хорошим покрытием.
Если нода не стоит высоко и не имеет хорошего покрытия, чаще всего
CLIENT,CLIENT_BASEилиCLIENT_MUTEбудут лучше.
Режим ретрансляции: ALL
На скрине у меня в поле «Режим ретрансляции» стоит ALL.
Смысл этого параметра - определить, какие сообщения устройство может ретранслировать.
Но тут важный нюанс: если роль устройства CLIENT_MUTE, то сама роль уже говорит “не ретранслировать чужие сообщения”. Поэтому для моего практического сценария главным был именно CLIENT_MUTE.
Если бы устройство стояло в роли CLIENT, CLIENT_BASE, ROUTER или REPEATER, тогда rebroadcast mode стал бы важнее.
Интервал вещания информации об узле
На скрине стоит:
3 часа
Это интервал, с которым нода периодически отправляет информацию о себе: имя, short name и другую NodeInfo-информацию.
В больших сетях не стоит делать такие интервалы слишком частыми без необходимости: каждое служебное сообщение тоже занимает эфир.
Аппаратные переключатели на экране Device
На этом же экране есть несколько настроек, которые не влияют напрямую на дальность, но влияют на поведение устройства.
Настройка |
Что делает |
|---|---|
Двойное нажатие как кнопка |
Позволяет использовать двойной тап по акселерометру как пользовательскую кнопку |
Маякнуть при тройном нажатии |
Отправляет позицию на основной канал при тройном нажатии |
Сердцебиение светодиодом |
Управляет миганием светодиода |
Часовой пояс |
Нужен для корректного отображения времени на устройстве и в логах |
GPIO кнопки |
Пин для внешней кнопки |
GPIO зуммера |
Пин для внешнего PWM-зуммера |
Почему MQTT у меня выключен

MQTT в Meshtastic нужен для связи mesh-сетей через интернет. Устройство или клиент может публиковать пакеты в MQTT-брокер и получать их оттуда. Это удобно, если нужно связать удалённые группы нод через интернет.
У меня MQTT выключен, потому что цель эксперимента была другой:
LoRa → USB → Python → локальная Ollama → USB → LoRa
То есть без интернет-моста.
MQTT включён
Если включить MQTT enabled, нода сможет работать с MQTT-брокером.
По умолчанию в интерфейсе указан публичный сервер:
mqtt.meshtastic.org
Для больших сетей и постоянной эксплуатации лучше понимать, что именно вы отправляете в MQTT и зачем.
Адрес, имя пользователя и пароль
На скрине видны:
mqtt.meshtastic.org meshdev large4cats
Это дефолтные параметры публичного MQTT-сервера Meshtastic. Если используется собственный брокер, пароль, конечно, нужно скрывать на скриншотах. В моём случае это не кастомный секрет, а дефолтное значение.
Шифрование включено
Параметр Encryption Enabled определяет, будут ли пакеты отправляться в MQTT зашифрованными.
У меня этот переключатель включён, но сам MQTT выключен, поэтому в эксперименте он фактически не играл роли.
JSON включён
JSON Enabled позволяет отправлять/принимать JSON-пакеты через MQTT. Это удобно для интеграций с внешними системами, которым проще читать JSON.
Для моего сценария JSON не нужен, потому что Python-скрипт работает не через MQTT, а напрямую с нодой по USB.
TLS включён
TLS Enabled включает защищённое соединение с MQTT-сервером.
У меня TLS выключен, потому что MQTT вообще не используется.
Если же строить полноценный интернет-мост через свой брокер, TLS лучше включать.
Корневая тема
На скрине указано:
msh/NZ_865
Root topic - это корневая тема MQTT, через которую можно разделять разные Meshtastic-сети на одном MQTT-сервере.
Прокси клиенту включён
Client Proxy Enabled означает, что устройство может использовать интернет-соединение клиента, например телефона, для подключения к MQTT-серверу. Если выключено - используется сетевое подключение самого устройства, если оно настроено.
У меня выключено.
Отчёты по карте
Map Reporting позволяет периодически отправлять незашифрованный отчёт на MQTT-сервер для отображения на онлайн-картах.
Такой отчёт может включать данные вроде имени устройства, ID, примерного местоположения, модели устройства, роли, региона LoRa, preset’а и имени primary channel.
У меня отчёты по карте выключены. Для эксперимента с локальной LLM это не нужно, а с точки зрения приватности лучше не публиковать лишнее.
Канал MediumFast и PSK AQ==
В московской инструкции отдельно говорилось проверить основной канал:
Название: MediumFast или пустое Primary Channel PSK: AQ==
Это важно для совместимости: ноды должны быть не только на одном LoRa preset’е и частоте, но и в совместимом messaging channel.
AQ== - это стандартный известный ключ дефолтного primary channel для публичной совместимости.
Если вы хотите приватный канал, нужно создавать отдельный channel с собственным PSK и делиться им только с нужными участниками.
Как я теперь понимаю свой конфиг
MEDIUM_FAST- логично для городской сети и для сценария с короткими AI-ответами;RX Boost- полезен для базовой ноды у окна на USB-питании;CLIENT_MUTE- аккуратно для ноды, которая не должна плодить лишние ретрансляции;MQTT выключен - правильно для проверки именно локального радиомоста.
NZ_865- выбран из-за доступной мощности.Override Frequency- продвинутая настройка, которую нельзя бездумно копировать;Hop limit 7- оправдан, если вы следуете настройкам конкретного городского сообщества, но для личного теста из двух нод избыточен;20–28 dBm- не надо воспринимать как ползунок дальности.
Мой вывод после эксперимента такой:
Meshtastic нужно настраивать по принципу “минимум трафика, совместимость с локальной сетью, законный регион, нормальная антенна и разумный hop limit”.
Именно это отличает полезную mesh-сеть от набора устройств, которые просто шумят в эфире.
Тест дальности: 702,71 м в городской среде
После настройки я решил проверить, на каком расстоянии две ноды ещё смогут обмениваться сообщениями.
Условия были не лабораторные:
базовая нода стояла у окна;
мобильная нода была в руке;
между точками были здания;
прямой видимости не было;
RSSI/SNR я не фиксировал;
расстояние измерял по карте.
В моём тесте связь ещё проходила примерно на расстоянии:
702,71 м

Это не предел Meshtastic или LoRa. Это результат конкретного стенда:
две Heltec ESP32 LoRa 32 V4;
конкретные антенны;
конкретное расположение;
базовая нода у окна;
вторая нода в руке;
городская/полугородская застройка;
отсутствие прямой видимости;
пресет
MEDIUM_FAST.
В других условиях результат может быть сильно лучше или сильно хуже.
В моём городском тесте, без прямой видимости и с базовой нодой у окна, связь между двумя Heltec ESP32 LoRa 32 V4 ещё проходила примерно на 702,71 м. Это практический результат конкретного стенда в конкретных условиях.
Если делать тест более правильно, стоило бы дополнительно снять:
RSSI;
SNR;
высоту установки обеих нод;
типы и характеристики антенн;
процент потерянных пакетов;
результаты на разных пресетах;
CSV из Range Test Module.
Мой тест был бытовым и прикладным: мне хотелось понять, “добивает или нет” в реальной городской среде.
Зачем подключать Meshtastic к локальной нейросети
После того как я убедился, что связь между нодами работает, захотелось сделать следующий шаг.
Идея была такая: пусть одна нода будет подключена к ноутбуку, а ноутбук будет выполнять роль “мозга”. Тогда с удалённой ноды можно отправить текстовый запрос, а в ответ получить сообщение от локальной нейросети.
Почему именно локальная модель, а не облачный API?
Потому что так эксперимент становится автономнее:
не нужен интернет;
запросы не уходят во внешний сервис;
можно использовать модель даже в офлайне;
Meshtastic остаётся настоящим радиоканалом, а не просто интерфейсом к облаку.
В моём случае использовалась Ollama и модель:
huihui_ai/qwen3.5-abliterated:35b
Модель относительно лёгкая и короткие сообщения генерирует в миг.
Архитектура моста
Логика работы такая:
Вторая Meshtastic-нода отправляет сообщение в mesh.
Первая нода принимает его по LoRa.
Первая нода подключена к ноутбуку по USB.
Python-скрипт на ноутбуке получает входящий пакет.
Если текст начинается с
/ai, скрипт отправляет запрос в Ollama.Ollama генерирует короткий ответ.
Python-скрипт отправляет ответ обратно через Meshtastic.
Вторая нода получает ответ.
Схематично:
/ai привет ↓ LoRa ↓ USB Serial ↓ Python ↓ http://localhost:11434/api/chat ↓ локальная LLM ↓ короткий ответ ↓ Meshtastic sendText() ↓ LoRa ↓ AI: Привет! Я локальный помощник...
Подключение к Meshtastic из Python
Для работы с нодой я использовал официальную Python-библиотеку Meshtastic.
Упрощённое подключение через USB выглядит так:
import meshtastic.serial_interface from pubsub import pub def on_receive(packet, interface=None): print(packet) pub.subscribe(on_receive, "meshtastic.receive") interface = meshtastic.serial_interface.SerialInterface(devPath="COM8")
В моём случае нода определилась как COM8.
Фактически скрипт подписывается на входящие пакеты и ждёт новые сообщения из mesh-сети.
Фильтрация сообщений по /ai
Я не хотел, чтобы бот отвечал вообще на все сообщения в mesh. Поэтому сделал простой префикс:
/ai
Если сообщение начинается с /ai, скрипт считает его запросом к локальной модели. Если нет - игнорирует.
Условно:
TRIGGER = "/ai" def on_receive(packet, interface=None): text = extract_text_from_packet(packet) if not text: return clean_text = text.strip() if not clean_text.lower().startswith(TRIGGER): print(f"Игнорирую обычное сообщение: {clean_text}") return user_text = clean_text[len(TRIGGER):].strip() if not user_text: return work_queue.put((interface, destination, channel_index, user_text))
Зачем очередь work_queue?
Потому что генерация ответа LLM может занимать долгое время. Не хочется блокировать обработчик входящих сообщений. Поэтому приём пакета и запрос к модели лучше развести: обработчик быстро кладёт задачу в очередь, а отдельный worker уже общается с Ollama.
Запрос к Ollama
Сначала я пробовал использовать /api/generate, но для чат-модели удобнее оказался /api/chat.
Ключевой момент для радиомоста - stream: false.
По умолчанию Ollama REST API может возвращать ответ потоком: модель генерирует текст, а клиент получает его частями. Для веб-интерфейса это удобно. Для Meshtastic - наоборот, опасно: нельзя отправлять каждый токен отдельным LoRa-пакетом.
Мой запрос к Ollama выглядит примерно так:
payload = { "model": "huihui_ai/qwen3.5-abliterated:35b", "messages": [ { "role": "system", "content": ( "Ты локальный помощник, отвечающий через Meshtastic. " "Отвечай только по-русски. " "Отвечай очень кратко: максимум 1-2 предложения. " "Не используй markdown, списки и рассуждения. " "Не показывай thinking, chain-of-thought или внутренние размышления." ) }, { "role": "user", "content": user_text + "\n\n/no_think" } ], "stream": False, "think": False, "keep_alive": "30m", "options": { "temperature": 0.3, "num_predict": 80, "num_ctx": 4096 } }
И сам HTTP-запрос:
response = requests.post( "http://localhost:11434/api/chat", json=payload, timeout=900 ) response.raise_for_status() data = response.json() answer = data.get("message", {}).get("content", "")
Почему нужен think: false
Некоторые современные модели умеют возвращать не только финальный ответ, но и отдельное поле с рассуждениями. Для обычного интерфейса это иногда полезно, но для Meshtastic это лишний мусор в эфире.
Для моего сценария это нужно отключать:
"think": False
На всякий случай я также добавил чистку ответа регуляркой:
def clean_model_output(text: str) -> str: text = re.sub(r"<think>.*?</think>", "", text, flags=re.IGNORECASE | re.DOTALL) text = text.replace("\r", " ").replace("\n", " ") text = re.sub(r"\s+", " ", text) return text.strip()
Это нужно потому, что конкретные модели и сборки могут вести себя по-разному. Если модель всё-таки вернёт <think>...</think>, я не хочу отправлять это в радиоэфир.
Отправка ответа обратно в mesh
Отправка текста обратно делается через sendText.
Простейший вариант:
interface.sendText("hello mesh")
В моём случае я отправляю ответ обратно отправителю:
interface.sendText( message, destinationId=destination, channelIndex=channel_index, wantAck=True )
Если direct message не проходит, можно отправлять broadcast в канал:
interface.sendText( message, channelIndex=channel_index, wantAck=False )
Для теста broadcast проще. Для реальной сети direct лучше: меньше мусора для остальных участников.
Ограничение длины ответа
Так как один пакет Meshtastic ограничен, я режу ответ на куски по байтам, а не по символам:
def split_utf8(text: str, max_bytes: int): chunks = [] current = "" for ch in text: candidate = current + ch if len(candidate.encode("utf-8")) > max_bytes: if current: chunks.append(current) current = ch else: current = candidate if current: chunks.append(current) return chunks
Почему по байтам?
Потому что для Meshtastic важен не “размер строки в символах”, а размер полезной нагрузки. Русский текст в UTF-8 занимает больше байт, чем латиница, поэтому простое ограничение “100 символов” не всегда корректно.
В моём коде я использовал значение вроде:
MAX_REPLY_BYTES = 170
Это меньше теоретического лимита, зато оставляет запас и снижает шанс упереться в накладные расходы.
Что получилось
На практике сценарий стал таким.
С удалённой ноды я отправляю:
/ai привет, кто ты?
Базовая нода принимает сообщение, ноутбук отправляет его в Ollama, модель генерирует ответ, а Python-скрипт отправляет его обратно в mesh:
AI: Привет! Я ваш локальный помощник, работающий через сеть Meshtastic...
Для пользователя это выглядит как обычная переписка в Meshtastic, только на другом конце не человек, а локальная LLM.

/ai уходит в локальную модель на ноутбуке, а ответ возвращается обратно через Meshtastic.
/ai отправляется в локальную модель через Ollama. Идентификаторы нод на скриншоте скрыты. Полный код
В статью я не стал вставлять весь код, потому что он довольно длинный и его удобнее смотреть в репозитории.
Ключевые части:
подключение к Meshtastic через
SerialInterface;подписка на входящие сообщения через
pubsub;фильтрация сообщений по
/ai;запрос в Ollama через
/api/chat;stream: false;think: false;ограничение ответа по байтам;
отправка ответа через
sendText.
Полный код моста я выложил на GitHub:
https://github.com/Tajozhnik/meshtastic-ollama-bridge
Что пошло не идеально
Модель отвечает долго
Модель huihui_ai/qwen3.5-abliterated:35b на некоторые запросы может генерировать ответ заметно дольше, чем хотелось бы для чата и иногда возникает чувство, что где-то что-то сломалось.
Для Meshtastic это не критично, потому что сама сеть тоже не про мгновенные длинные диалоги. Но пользовательский опыт получается ближе к радиотелеграмме: отправил запрос, подождал, получил короткий ответ.
Для практического использования я бы попробовал более лёгкую модель, например 7B/8B/14B, чтобы сократить задержку.
Нельзя давать модели говорить много
LLM по умолчанию любит объяснять. Meshtastic этого не любит.
Если не ограничить num_predict, system prompt и длину сообщения, модель легко сгенерирует огромный ответ, который не имеет смысла отправлять по LoRa.
Поэтому для такого моста лучше думать в стиле:
Вопрос → один короткий ответ
Не хватает RSSI/SNR
Я не снимал RSSI и SNR на крайней точке 702,71 м, поэтому мой тест нельзя назвать полноценным радиотехническим сравнением.
Это честный бытовой тест: связь есть или связи нет. Для следующей версии эксперимента нужно фиксировать метрики.
Что можно улучшить дальше
Следующие шаги, которые я бы сделал:
Прогнать тот же маршрут на
LONG_FASTи сравнить дальность.Включить Range Test Module и собрать CSV.
Записывать RSSI/SNR на разных точках.
Поднять базовую ноду выше.
Сравнить штатные и нормальные настроенные антенны.
Попробовать более лёгкую LLM.
Добавить RAG: локальную базу знаний, по которой бот сможет отвечать.
Сделать команды не только для LLM, но и для локальной автоматизации.
Например, можно отправлять:
/ai что делать если сел аккумулятор?
А можно сделать отдельные команды:
/status /weather /power /home light off
Тогда Meshtastic превращается не просто в мессенджер, а в низкоскоростной радиоинтерфейс к локальным сервисам.
Где тут реальная польза
Такой мост может пригодиться как интерфейс к коротким офлайн-ответам.
Особенно интересно выглядит вариант с RAG: на ноутбуке лежит локальная база документов, Python-скрипт ищет релевантные фрагменты, отдаёт их в LLM, а модель возвращает очень короткий ответ в Meshtastic.
В таком варианте Meshtastic отвечает за связь, а ноутбук - за интеллект.
Выводы
Meshtastic оказался очень интересной платформой для экспериментов с автономными системами.
Что у меня получилось:
поднять связь между двумя Heltec ESP32 LoRa 32 V4;
получить обмен сообщениями без интернета;
разобраться с LoRa-настройками, ролями устройства и MQTT;
проверить связь в городской среде на расстоянии около 702,71 м;
подключить одну ноду к ноутбуку по USB;
написать Python-мост между Meshtastic и Ollama;
отправлять запросы в локальную LLM через LoRa и получать ответы обратно.
Главный вывод: Meshtastic - это удобный транспортный слой для маленьких автономных систем. Если принять его ограничения - короткие сообщения, низкая скорость, аккуратное использование эфира - поверх него можно строить довольно интересные вещи.
Например, офлайн-бота, локальный справочник, радиоинтерфейс к автоматизации или мост к локальной нейросети.
И самое приятное: всё это работает без облака, без SIM-карты и без интернета.
Отдельный урок этого эксперимента - настройки Meshtastic важнее, чем кажется сначала. Регион, мощность, frequency slot, hop limit, роль устройства и MQTT - это не декоративные переключатели. Они определяют, с кем нода будет совместима, сколько эфира она будет занимать, будет ли она помогать сети или только создавать лишние ретрансляции, и не отправит ли она ваши данные в интернет через MQTT.
Поэтому перед тем как прикручивать к Meshtastic локальную LLM, RAG или автоматизацию, сначала стоит разобраться с базовой радиочастью.
Источники и полезные ссылки
Введение в Meshtastic: https://meshtastic.org/docs/introduction/
Первичная настройка региона: https://meshtastic.org/docs/getting-started/initial-config/
LoRa-настройки Meshtastic: https://meshtastic.org/docs/configuration/radio/lora/
Настройки радио: https://meshtastic.org/docs/overview/radio-settings/
Алгоритм mesh: https://meshtastic.org/docs/overview/mesh-algo/
Выбор правильной роли устройства: https://meshtastic.org/blog/choosing-the-right-device-role/
Конфигурация устройства: https://meshtastic.org/docs/configuration/radio/device/
MQTT модуль: https://meshtastic.org/docs/configuration/module/mqtt/
Модуль проверки дальности: https://meshtastic.org/docs/configuration/module/range-test/
Python API Meshtastic: https://python.meshtastic.org/
Ollama API: https://docs.ollama.com/
Подписывайтесь на мой Telegram-канал: https://t.me/CryptoUngated
P.S. И да, пока я всё это собирал и отлаживал, меня успела бросить девушка. Так что если проект показался полезным - буду рад тёплому комментарию, идеям и критике :)
Dreams_and_magic
Спасибо за отличную статью:)
В нашем городе есть Meshtastic и MeshCore, но есть нюансы с "руководителем" MeshCore, который косплеит из себя диктатора и РКН в одном флаконе:)
Насчёт нейросети, ИМХО если не парит необходимость использовать Интернет, то можно делать запросы к бесплатным нейросетям, например, таким как Mistral Large и NVIDIA: Nemotron 3 Super , они буду значительно "умнее" любых моделей на ноутбуке.
Zhabrozavr
Любопытненько. А что это за "руководители" и откуда они берутся? Я просто не в курсе таких "нюансов". Он сам себя назначает, организовав сеть, к которой присоединяются? Параллельную структуру можно организовать и можно ли вообще без неё?
Dreams_and_magic
Он типа организатор, поставил в начале года первые ретрансляторы мешкора.
Есть телеграм-канал местного сообщества мешкора где он хозяин, куда он не всех пускает и может забанить "недостойных", продвигает идеи внесения банов "плохих" репитеров в прошивках...:)
Я так подумал, что ещё один РНК мне не нужен:)
ash_lm
Ну значит остальных всё устраивает, если остальные сами его не забанили.
Dreams_and_magic
Видимо, устраивает:)
p0isk
Никто не мешает создать другую мт или мк сеть на других частотах.