- BrainTools - https://www.braintools.ru -
Однажды встретились Orange PI 5, Heltect v3, свободное время и J4F и в Саратове появился второй LLM бот для Meshtastic. Сегодня расскажу как все это повторить если у вас в одном месте и в одно время появится примерно такое же.
Кратенько про Meshtastic. Сейчас у нас в Саратове по данным https://map.onemesh.ru/ [1] 114 нод, по данным моей ноды – 150 из которых около ~40 постоянно онлайн). В качестве железа этого проекта используется стационарный Heltect v3 с увеличенной антенной закрепленный на окне и подключенный к WiFi и MQTT. Так как нода Meshtastic не умеет мультиконнект, то к ноде подключена интеграция Home Assistant которая умеет работать как прокси. Но это не обязательно, то же самое умеет meshmonitor [2], его можно запускать как угодно, даже есть инсталяторы под разные 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 такая
А теперь к основной части, самому скрипту бота на python который работает как прослойка между Meshatastic и Ollama . Вообще скрипт задумывался как тупой пересыльщик сообщений из Meshatastic в телегу, а так же записыватель базы всех сообщений и нод в sqlite3 базу. Там все тупо, скучно и не интересно. На интересных особенностях остановимся далее. Актуальная версия доступна на github [3], вместе с 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}nnВопрос: {prompt}nОтвет:",
"stream": False,
# Not speed up
"options": {
"temperature": 0.4,
"top_p": 0.9,
},
}
Второй интересный момент – функция send_response. В meshtastic есть 3 вида текстовых сообщений, обычные, ответы (ака thread) и так же реакции [4] на сообщения. При этом API sendText из API умеет только первый тип, для обхода этого ограничения send_response использует _sendPacket из API напрямую. Что забавно, в реакции можно пихать не только emoji но и текст. В скрипте для примера тоже захордкожены реакции на hi и пинг
Выглядит это примерно так
Говорят что это работает только в приложении на Android, а на IOS этого нет, но сам не проверял.
Собственно у нас в саратове 2 llm, одна моя, вторая вроде крутится на видюхе (к сожалению и модель и модель видюхи забыл), и сама посерьезнеe и побольше, на скринах дальше можно глянуть ответы и сравнить их скорость. sg-n это описываемая тут нода, а NWAY – другая. Ради забавы ради позадавал вопросы и на китайском.





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

В целом модель phi4-mini ведет себя отлично, даже без ограничений которые наложены запросом из мештастик бота. Эта же модель подключена к телеграм боту основанному на вот этом проекте [5] где ограничений нет, и там она вполне хорошо держит конекст в диалоге, выдает довольно релевантные ответы, но иногда может призадуматься.
Если, вдруг, вы дочитали до конца и у вас есть 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
в тексте выглядит это примерно так

Автор: sergeygals
Источник [6]
Сайт-источник BrainTools: https://www.braintools.ru
Путь до страницы источника: https://www.braintools.ru/article/24926
URLs in this post:
[1] https://map.onemesh.ru/: https://map.onemesh.ru/
[2] meshmonitor: https://github.com/Yeraze/meshmonitor/
[3] на github: https://github.com/sergeygalkin/meshtastic/tree/main/bot
[4] реакции: http://www.braintools.ru/article/1549
[5] вот этом проекте: https://github.com/rikkichy/ollama-telegram
[6] Источник: https://habr.com/ru/articles/990588/?utm_source=habrahabr&utm_medium=rss&utm_campaign=990588
Нажмите здесь для печати.