Однажды встретились Orange PI 5, Heltect v3, свободное время и J4F и в Саратове появился второй LLM бот для Meshtastic. Сегодня расскажу как все это повторить если у вас в одном месте и в одно время появится примерно такое же.

Кратенько про Meshtastic. Сейчас у нас в Саратове по данным https://map.onemesh.ru/ 114 нод, по данным моей ноды - 150 из которых около ~40 постоянно онлайн). В качестве железа этого проекта используется стационарный Heltect v3 с увеличенной антенной закрепленный на окне и подключенный к WiFi и MQTT. Так как нода Meshtastic не умеет мультиконнект, то к ноде подключена интеграция Home Assistant которая умеет работать как прокси. Но это не обязательно, то же самое умеет meshmonitor, его можно запускать как угодно, даже есть инсталяторы под разные OS.

Eще забавный факт, вчера человек летел из Махачкалы с LILYGO T-Echo, судя по flightradar24 в 245км (в районе фролово) от Саратова на высоте примерно 10 км, и мы перекидывались сообщениям с ним почти до его подлета к Тамбову, и даже удалось перекинуться сообщениями с Пензой.

LLM нода - Orange PI 5 8G RAM c 513G m2 SSD. На нем крутится Ubuntu 22.04.5 c ollama и c закаченной моделькой phi4-mini

gals@orangepi5-8g:~ $ ollama list
NAME                ID              SIZE      MODIFIED
phi4-mini:latest    78fad5d182a7    2.5 GB    2 weeks ago

Типичная трата ресурсов при обработке запроса из meshtastic такая

Prometheus
Prometheus
А это сам запрос
А это сам запрос

А теперь к основной части, самому скрипту бота на python который работает как прослойка между Meshatastic и Ollama . Вообще скрипт задумывался как тупой пересыльщик сообщений из Meshatastic в телегу, а так же записыватель базы всех сообщений и нод в sqlite3 базу. Там все тупо, скучно и не интересно. На интересных особенностях остановимся далее. Актуальная версия доступна на github, вместе с systemd service, env и requirements.txt

Пробежимся по интерeсным моментам

Кроме основного дефолтного канала у меня в Meshtastic настроены каналы со своими отдельными ключами шифрования. 1 - семья, 2 - друзья, они зашиты в константу CHANNELS

Так как в Meshtastic есть ограничение на размер в сообщении в 230 байт, притом что кириллица это 2 байта, а emoji могут быть больше, а так же потому что ресурсы llm ноды ограничены мы добавляем системный промпт "Ты чатбот для Meshtastic. Отвечай по-русски. Ответ строго <= 110 символов. Без списков, без пояснений, без приветствий." в функцию ollama_reply, так же туда же добавляем "temperature": 0.4, "top_p": 0.9, что бы ответ был быстрее. Так как LLM может выдать ответ длиннее чем можно, и сообщения тогда вообще не уйдет ответ еще проходит через функцию clamp_200 которая обрезает сообщение до 110 символов. У нас в Cаратове как-то сложилось что обе модели LLM отвечают только на сообщения которые начинаются с !llm и это тупо захардкожено прямо в скрипте.

Скрытый текст
def ollama_reply(prompt: str) -> str:
    # Request to model to be very short
    system = (
        "Ты чатбот для Meshtastic. "
        "Отвечай по-русски. "
        "Ответ строго <= 110 символов. "
        "Без списков, без пояснений, без приветствий."
    )

    payload = {
        "model": OLLAMA_MODEL,
        "prompt": f"{system}\n\nВопрос: {prompt}\nОтвет:",
        "stream": False,
        # Not speed up
        "options": {
            "temperature": 0.4,
            "top_p": 0.9,
        },
    }

Второй интересный момент - функция send_response. В meshtastic есть 3 вида текстовых сообщений, обычные, ответы (ака thread) и так же реакции на сообщения. При этом API sendText из API умеет только первый тип, для обхода этого ограничения send_response использует _sendPacket из API напрямую. Что забавно, в реакции можно пихать не только emoji но и текст. В скрипте для примера тоже захордкожены реакции на hi и пинг

Выглядит это примерно так

Скрин c andorid
Скрин c andorid

Говорят что это работает только в приложении на Android, а на IOS этого нет, но сам не проверял.

Собственно у нас в саратове 2 llm, одна моя, вторая вроде крутится на видюхе (к сожалению и модель и модель видюхи забыл), и сама посерьезнеe и побольше, на скринах дальше можно глянуть ответы и сравнить их скорость. sg-n это описываемая тут нода, а NWAY - другая. Ради забавы ради позадавал вопросы и на китайском.

Скрытый текст

По идее если добавить в скрипте в ответ бота !llm то можно устроить теоретически бесконечный диалог :) но канал не резиновый и небольшую рекурсию можно устроить и одним запросом, напоминаю что sg-h это бот

В целом модель phi4-mini ведет себя отлично, даже без ограничений которые наложены запросом из мештастик бота. Эта же модель подключена к телеграм боту основанному на вот этом проекте где ограничений нет, и там она вполне хорошо держит конекст в диалоге, выдает довольно релевантные ответы, но иногда может призадуматься.

Загрузка системы во время диалога в телеге.
Загрузка системы во время диалога в телеге.
A вот один ответ на вопрос без лимитов
A вот один ответ на вопрос без лимитов

Если, вдруг, вы дочитали до конца и у вас есть Home Assistant c настроенной интеграцией с Meshtastci, то вот вам бонусом HA автоматизация которая по запросу !w выдает текущую погоду в канал

Скрытый текст
alias: Meshatic Weather
description: ""
triggers:
  - domain: meshtastic
    type: channel_message.received
    entity_id: meshtastic.gateway_sg_h_channel_primary
    trigger: device
conditions:
  - condition: template
    value_template: "{{ \"!w\" in (trigger.event.data.message | lower) }}"
actions:
  - action: weather.get_forecasts
    metadata: {}
    data:
      type: daily
    enabled: false
  - delay:
      hours: 0
      minutes: 0
      seconds: 12
      milliseconds: 0
  - action: meshtastic.broadcast_channel_message
    metadata: {}
    data:
      ack: true
      channel: meshtastic.gateway_sg_h_channel_primary
      message: >-
        Погода: ?️ {{state_attr('weather.home', 'temperature')
        }}{{state_attr('weather.home', 'temperature_unit')}} ?
        {{state_attr('weather.home', 'humidity')}}% ?
        {{state_attr('weather.home', 'wind_speed')}}{{state_attr('weather.home',
        'wind_speed_unit')}} / {{ ((state_attr('weather.home', 'wind_speed') |
        float(0)) / 3.6) | round(1) }}m/s {% set deg =
        state_attr('weather.home', 'wind_bearing') | float(0) %}{% set dirs = [
          "С",
          "СВ",
          "В",
          "ЮВ",
          "Ю",
          "ЮЗ",
          "З",
          "СЗ"
        ] %}{% set idx = ((deg + 22.5) // 45) | int %} ?↗️ {{ dirs[idx % 8]
        }}({{ deg }})° ?️ {{state_attr('weather.home', 'cloud_coverage')}}%
        ☀️(uv_index) {{state_attr('weather.home', 'uv_index')}} ?️
        {{state_attr('weather.home', 'pressure')}}{{state_attr('weather.home',
        'pressure_unit')}}{% set hpa = state_attr('weather.home', 'pressure') |
        float(0) %}{% set mmhg = (hpa * 0.750064) | round(0) %}{% if mmhg < 745
        %}{% set level = "низкое" %}{% elif mmhg <= 765 %}{% set level = "норм"
        %}{% else %}{% set level = "высокое" %}{% endif %}/{{ mmhg }}mm ({{
        level }}). {% set t = states('weather.home') %}{% set map = {
          'sunny': 'клево и солнечно',
          'clear-night': 'Темно и ясно',
          'cloudy': 'облачно немношк',
          'partlycloudy': 'Иногда облачность',
          'rainy': 'лужи и дождь',
          'pouring': 'не забываем зонт, ливень',
          'snowy': 'готовьте лыжи, снег',
          'snowy-rainy': 'херовато, снег с дождём',
          'windy': 'ветрено нафик',
          'windy-variant': 'может сдуть нафик',
          'fog': 'загадочно и туманно',
          'hail': 'не забудьте каску, град',
          'lightning': 'в укрвтие, гроза',
          'lightning-rainy': 'Гроза с дождём'
        } %}Воопщем - {{ map[t] if t in map else t }}
mode: single

в тексте выглядит это примерно так

Комментарии (4)


  1. rodial
    30.01.2026 07:54

    Если хочется заморочиться и теоретически ускорить генерацию в 2 раза, можно попробовать запустить модель на NPU RK3588S, который может использовать до 4GB RAM. Как минимум снять нагрузку с процессора.


    1. sergeygals Автор
      30.01.2026 07:54

      Думал над этим, но вменяемых инструкций не нашел. На самом деле сначала я хотел заюзать orange pi rv2 у которого даже в названии Ai есть, но не получилось. Счас копаю эту тему поглубже


      1. rodial
        30.01.2026 07:54

        orange pi rv2 обещает 2TOPS

        NPU в RK3588S обещает 6TOPS

        По поводу запуска на NPU в RK3588S я смог накопать только что нужны специальные драйвера в ядре и они есть в ubuntu rockchip 24.04. У меня ubuntu rockchip 22.04 и нужных драйверов нет, переустановить её никак руки не дойдут поэтому как дальше не знаю, но самым простым способом запустить мне видится https://github.com/Pelochus/ezrknn-llm/

        Попросил ИИ посчитать производительность модели на CPU RK3588S в сравнимых величинах, пишет что CPU RK3588S ~0.8 TOPS, но учётом квантизации полагаю скорость генерации будет сравнимой c orange pi rv2.


        1. sergeygals Автор
          30.01.2026 07:54

          Да, седня поковырял, ее тоже нашел, но не проперло, и поддерживает не все, например phi4-mini, которая лучше всех показала на pi 5 нету. модели переконвертировать надо.
          Зато поигрался с orange pi rv2, тулза называется ky-ort там те же проблемы поддерживает не все, надо конвертить, а то что было и я попробовал, дико жрет проц и глючит

          Вот например, генерация жрет 4 ядра в ноль
          Вот например, генерация жрет 4 ядра в ноль
          А вот в результат, почти 1,5 минуты генерил
          А вот в результат, почти 1,5 минуты генерил
          A вот phi3-mini, жуткие тормоза и глючит опять же
          A вот phi3-mini, жуткие тормоза и глючит опять же

          ну и еще один минус, все это запускается кастомными тулзами, то есть Ollama минус, а это значит код который работает с Ollama, скороее всего заиспользовать не получится и надо во что то другое переделывать, опять же не очень понятно как, c доками там не очень