Полное руководство по
Mesh-сетям будущего
Исчерпывающий ресурс по Meshtastic, MeshCore и Reticulum — трём революционным технологиям децентрализованной связи. Узнайте, как создать независимую коммуникационную инфраструктуру без интернета и сотовых вышек.
Что такое Mesh-сети?
Mesh-сети (ячеистые сети) — это децентрализованная сетевая топология, в которой каждый узел может выступать как клиент, сервер и маршрутизатор одновременно.
Децентрализация
Нет единой точки отказа. Каждый узел сети равноправен и может маршрутизировать трафик для других участников. Сеть живёт, пока жив хотя бы один узел.
Радиосвязь
Использование LoRa, WiFi, Bluetooth и других радиотехнологий для создания сетей без зависимости от интернет-провайдеров и сотовых операторов.
Шифрование
Сквозное шифрование сообщений, аутентификация узлов и защита от прослушивания. Ваши данные принадлежат только вам.
Три технологии, одна цель
Meshtastic, MeshCore и Reticulum — три разных подхода к созданию децентрализованных сетей связи. Каждый решает задачу по-своему.
Meshtastic
Открытый проект для создания mesh-сетей на базе LoRa-радио. Идеален для обмена текстовыми сообщениями и GPS-координатами на больших расстояниях при минимальном энергопотреблении.
MeshCore
Библиотека для создания mesh-сетей поверх WiFi (ESP32). Обеспечивает высокоскоростную передачу данных в локальной mesh-сети без точки доступа.
Reticulum
Универсальный сетевой стек для создания зашифрованных mesh-сетей поверх любых сред передачи. Поддерживает LXMF для обмена сообщениями и Sideband как клиент.
Как работает Mesh-сеть?
В mesh-сети каждый узел действует как ретранслятор, расширяя покрытие сети и обеспечивая множественные пути для доставки данных.
Устройство включается, сканирует эфир и обнаруживает соседние узлы. Формируется таблица маршрутизации с информацией о доступных путях.
Узел периодически отправляет beacon-пакеты, объявляя о своём присутствии. Соседние узлы получают эти пакеты и обновляют свои таблицы маршрутизации.
Когда узел A хочет отправить сообщение узлу Z, он определяет оптимальный маршрут через промежуточные узлы (B, C, D...). Используются алгоритмы flooding или table-based routing.
Каждый промежуточный узел получает пакет, проверяет адрес назначения и пересылает его дальше. Это позволяет преодолевать расстояния, значительно превышающие дальность прямой связи.
Конечный узел получает сообщение и отправляет подтверждение (ACK) обратно по цепочке. Если ACK не получен, отправитель может повторить передачу.
Интересный факт: Mesh-сети могут масштабироваться практически бесконечно. Каждый новый узел не только потребляет ресурсы сети, но и увеличивает её пропускную способность и надёжность.
Где применяются Mesh-сети?
Походы и экспедиции
Связь в горах, лесах и пустынях, где нет сотового покрытия. GPS-трекинг группы, обмен координатами и текстовыми сообщениями. Meshtastic идеален для этого благодаря LoRa с дальностью до 20+ км.
Чрезвычайные ситуации
При стихийных бедствиях, когда инфраструктура связи разрушена, mesh-сети становятся единственным средством коммуникации. Быстрое развёртывание без зависимости от вышек.
Локальные сообщества
Соседские сети для обмена сообщениями, оповещений и координации. Community networks, независимые от провайдеров и корпораций.
Мероприятия и фестивали
Связь на массовых мероприятиях, где сотовые сети перегружены. Координация волонтёров, организаторов и служб безопасности.
Промышленность и IoT
Мониторинг датчиков на больших территориях: сельское хозяйство, трубопроводы, склады. MeshCore обеспечивает высокую скорость для передачи данных сенсоров.
Приватная коммуникация
Для тех, кто ценит приватность. Сквозное шифрование, отсутствие центральных серверов и полная независимость от корпоративных платформ обмена сообщениями.
Начните за 5 минут
Создать свою mesh-сеть проще, чем кажется. Вот базовый путь:
Совет: Для начала вам нужно минимум 2 устройства. Один узел — это просто радиоприёмник. Два узла — это уже сеть! Начните с Meshtastic на ESP32 + LoRa модуле — это самый доступный вариант.
Meshtastic
Открытый проект для создания децентрализованных mesh-сетей на базе LoRa-радио. Обмен сообщениями, GPS-координатами и телеметрией без интернета и сотовой связи.
Что такое Meshtastic?
Meshtastic — это проект с открытым исходным кодом, который позволяет создавать mesh-сети дальнего радиуса действия с использованием недорогих LoRa-радиомодулей. Проект начался в 2019 году и с тех пор вырос в глобальное сообщество с тысячами активных узлов по всему миру.
Meshtastic использует технологию LoRa (Long Range) — метод модуляции с расширенным спектром, который обеспечивает связь на расстояния до 20+ км в условиях прямой видимости при потреблении энергии менее 100 мВт. Это делает Meshtastic идеальным решением для ситуаций, где важна дальность связи и автономность, а не высокая скорость передачи данных.
Ключевая особенность: Meshtastic работает на частотах ISM-диапазона (868 МГц в Европе, 915 МГц в США, 433 МГц в Азии), которые не требуют лицензии для использования.
Архитектура Meshtastic
Meshtastic состоит из нескольких ключевых компонентов, работающих вместе для обеспечения надёжной mesh-связи.
Принцип маршрутизации
Meshtastic использует controlled flooding как основной механизм маршрутизации. Когда узел отправляет сообщение, оно широковещательно передаётся всем соседним узлам. Каждый узел, получивший сообщение, проверяет его уникальный ID и, если он ещё не видел это сообщение, ретранслирует его дальше. Для предотвращения бесконечных петель используется счётчик прыжков (hop_count), который ограничивает максимальное количество ретрансляций.
// Определение пакета Meshtastic (protobuf)
message MeshPacket {
// Уникальный ID отправителя (4 байта)
uint32 from = 1;
// ID получателя (0 = broadcast)
uint32 to = 2;
// Индекс канала (для мультиканальности)
uint32 channel = 3;
// Зашифрованные данные или plaintext
oneof payload_variant {
Data decoded = 4;
bytes encrypted = 5;
}
// Счётчик прыжков (TTL)
uint32 hop_limit = 6;
uint32 hop_start = 7;
// ID пакета для дедупликации
uint32 id = 8;
// Время отправки (Unix timestamp)
uint32 rx_time = 9;
// RSSI и SNR последнего приёма
float rx_rssi = 10;
float rx_snr = 11;
// Приоритет пакета
enum Priority {
UNSET = 0;
MIN = 1;
BACKGROUND = 10;
DEFAULT = 64;
RELIABLE = 100;
ACK = 120;
MAX = 255;
}
Priority priority = 12;
}
// Полезная нагрузка данных
message Data {
enum PortNum {
UNKNOWN_APP = 0;
TEXT_MESSAGE_APP = 1;
POSITION_APP = 3;
NODEINFO_APP = 4;
ROUTING_APP = 5;
TELEMETRY_APP = 67;
NEIGHBORINFO_APP = 70;
ATAK_PLUGIN = 71;
PRIVATE_APP = 256;
}
PortNum portnum = 1;
bytes payload = 2;
bool want_response = 3;
uint32 request_id = 4;
}
LoRa: основа Meshtastic
LoRa (Long Range) — это технология модуляции с расширенным спектром на основе chirp spread spectrum (CSS), разработанная компанией Semtech.
Параметры LoRa
| Параметр | Значение | Описание |
|---|---|---|
| Spreading Factor | SF7 — SF12 | Определяет скорость и дальность. SF12 — самая медленная, но самая дальняя |
| Bandwidth | 125 / 250 / 500 кГц | Ширина полосы. Уже = чувствительнее, но медленнее |
| Coding Rate | 4/5 — 4/8 | Избыточность кодирования. Выше = надёжнее, но медленнее |
| Частота | 433 / 868 / 915 МГц | ISM-диапазоны, зависят от региона |
| Мощность | +2 — +22 dBm | Мощность передатчика. Зависит от модуля |
| Чувствительность | до -148 dBm | Минимальный уровень сигнала для приёма |
| Скорость | 0.3 — 50 кбит/с | Зависит от SF, BW и CR |
| Дальность | до 20+ км | В условиях прямой видимости с хорошими антеннами |
Расчёт скорости LoRa
Скорость передачи данных в LoRa определяется формулой:
import math
def calculate_lora_datarate(sf, bw, cr, de=False, n_crc=True, ih=False):
"""
Расчёт скорости передачи данных LoRa.
Параметры:
sf : Spreading Factor (7-12)
bw : Bandwidth в кГц (125, 250, 500)
cr : Coding Rate (1-4, соответствует 4/5-4/8)
de : Low Data Rate Optimization
n_crc: Наличие CRC
ih : Implicit Header mode
Возвращает:
Скорость в бит/с
"""
# Symbol rate (символов в секунду)
rs = (bw * 1000) / (2 ** sf)
# Полезная нагрузка на символ
cr_fraction = 4 / (cr + 4)
# Расчёт скорости
if sf <= 6:
# SF6 использует другой режим
datarate = rs * sf * cr_fraction
else:
# Стандартный режим SF7-SF12
de_factor = 1 if de else 0
sf_effective = sf - 2 * de_factor
datarate = rs * sf_effective * cr_fraction
return round(datarate, 2)
# Примеры расчётов для разных конфигураций
configs = [
("Fast (SF7, BW250)", 7, 250, 1),
("Default (SF11, BW125)", 11, 125, 1),
("Long Range (SF12, BW125)", 12, 125, 2),
("Medium (SF9, BW125)", 9, 125, 1),
]
for name, sf, bw, cr in configs:
rate = calculate_lora_datarate(sf, bw, cr)
print(f"{name:35}: {rate:8} бит/с")
# Вывод:
# Fast (SF7, BW250) : 13671.88 бит/с
# Default (SF11, BW125) : 537.11 бит/с
# Long Range (SF12, BW125) : 268.55 бит/с
# Medium (SF9, BW125) : 2148.44 бит/с
def calculate_time_on_air(payload_len, sf, bw, cr=1,
de=False, n_crc=True, ih=False):
"""
Расчёт времени передачи пакета (Time on Air).
Параметры:
payload_len : длина полезной нагрузки в байтах
sf, bw, cr : параметры LoRa
de : Low Data Rate Optimization
n_crc : наличие CRC
ih : Implicit Header
Возвращает:
Время передачи в миллисекундах
"""
# Количество символов преамбулы
n_preamble = 8 # стандартное значение
# Длительность символа
t_sym = (2 ** sf) / (bw * 1000) # в секундах
# Время преамбулы
t_preamble = (n_preamble + 4.25) * t_sym
# Расчёт количества символов полезной нагрузки
de_flag = 1 if de else 0
ih_flag = 1 if ih else 0
crc_flag = 1 if n_crc else 0
numerator = 8 * payload_len - 4 * sf + 28 + 16 * crc_flag - 20 * ih_flag
denominator = 4 * (sf - 2 * de_flag)
n_payload = 8 + max(math.ceil(numerator / denominator) * (cr + 4), 0)
# Общее время
t_total = t_preamble + n_payload * t_sym
return t_total * 1000 # в мс
# Пример: время передачи 50-байтового сообщения
toa = calculate_time_on_air(50, 11, 125)
print(f"Time on Air: {toa:.1f} мс")
# Time on Air: 1152.0 мс
Важно: В Европе (868 МГц) действуют ограничения duty cycle: 1% для большинства каналов. Это означает, что при Time on Air = 1 секунда вы должны ждать 99 секунд перед следующей передачей. Meshtastic автоматически управляет этим.
Настройка Meshtastic
Подробная инструкция по настройке устройства Meshtastic через CLI и мобильное приложение.
Установка прошивки
# Установка meshtastic CLI через pip
$ pip install meshtastic
# Проверка версии
$ meshtastic --version
# meshtastic 2.3.2
# Подключение к устройству через serial
$ meshtastic --port /dev/ttyUSB0 --info
# Подключение через WiFi (если устройство в сети)
$ meshtastic --host 192.168.1.100 --info
# Подключение через BLE
$ meshtastic --ble "Meshtastic_XXXX" --info
Базовая конфигурация
# Установка региона (EU_868 для Европы)
$ meshtastic --set lora.region EU_868
# Установка модемной конфигурации
# Варианты: LONG_FAST, LONG_SLOW, LONG_MODERATE, SHORT_FAST, MEDIUM_SLOW и др.
$ meshtastic --set lora.modem_preset LONG_FAST
# Установка hop_limit (максимальное количество прыжков)
$ meshtastic --set lora.hop_limit 3
# Включение/выключение TX (для приёмников-ретрансляторов)
$ meshtastic --set lora.tx_enabled true
# Установка мощности передатчика (dBm)
$ meshtastic --set lora.tx_power 20
# Настройка канала (шифрование)
$ meshtastic --set lora.use_psk true
# Установка имени узла
$ meshtastic --set owner "MyMeshNode-01"
# Установка короткого имени (4 символа)
$ meshtastic --set owner_short "M001"
# Включение позиционного вещания
$ meshtastic --set position.position_broadcast_secs 900
# Настройка интервала телеметрии
$ meshtastic --set device.telemetry_interval_secs 1800
# Показать текущую конфигурацию
$ meshtastic --info
# Перезагрузка устройства
$ meshtastic --reboot
# Сброс к заводским настройкам
$ meshtastic --reset
Конфигурация через Python API
import meshtastic
import meshtastic.serial_interface
import time
class MeshtasticNode:
"""
Класс для управления узлом Meshtastic через Python API.
Поддерживает отправку/приём сообщений, чтение телеметрии
и управление конфигурацией.
"""
def __init__(self, port="/dev/ttyUSB0"):
"""Инициализация подключения к узлу."""
self.interface = None
self.port = port
self.messages = []
self.nodes = {}
def connect(self):
"""Подключение к устройству через serial."""
try:
self.interface = meshtastic.serial_interface.SerialInterface(
devPath=self.port
)
print(f"✅ Подключено к {self.port}")
# Получение информации об узле
my_info = self.interface.getMyNodeInfo()
print(f"📡 Мой узел: {my_info.get('user', {}).get('longName', 'Unknown')}")
print(f"🆔 Node ID: !{my_info.get('num', 0):08x}")
return True
except Exception as e:
print(f"❌ Ошибка подключения: {e}")
return False
def send_text(self, text, destination="^all"):
"""
Отправка текстового сообщения.
Args:
text: Текст сообщения
destination: ID получателя или "^all" для broadcast
"""
if not self.interface:
print("❌ Не подключено")
return False
try:
self.interface.sendText(
text=text,
destinationId=destination,
wantAck=True,
wantResponse=False
)
print(f"📤 Отправлено: {text}")
return True
except Exception as e:
print(f"❌ Ошибка отправки: {e}")
return False
def send_position(self, lat, lon, alt=0):
"""Отправка GPS-позиции."""
if not self.interface:
return False
self.interface.sendPosition(
latitude=lat,
longitude=lon,
altitude=alt
)
print(f"📍 Позиция отправлена: {lat}, {lon}, {alt}м")
def get_nodes(self):
"""Получение списка всех известных узлов."""
if not self.interface:
return {}
self.nodes = self.interface.nodes
return self.nodes
def print_node_list(self):
"""Вывод списка узлов в красивом формате."""
nodes = self.get_nodes()
print("\n📡 Известные узлы:")
print("=" * 70)
print(f"{'ID':12} {'Имя':20} {'Батарея':10} {'Последний':15}")
print("=" * 70)
for node_id, node in nodes.items():
user = node.get('user', {})
name = user.get('longName', 'Unknown')
metrics = node.get('deviceMetrics', {})
battery = metrics.get('batteryLevel', 0)
last_seen = node.get('lastHeard', 0)
if last_seen > 0:
time_diff = time.time() - last_seen
if time_diff < 60:
last_str = f"{int(time_diff)}с назад"
elif time_diff < 3600:
last_str = f"{int(time_diff/60)}м назад"
else:
last_str = f"{int(time_diff/3600)}ч назад"
else:
last_str = "Никогда"
battery_str = f"{battery}%" if battery > 0 else "N/A"
print(f"{node_id:12} {name:20} {battery_str:10} {last_str:15}")
def on_receive(self, packet, interface):
"""Обработчик входящих сообщений."""
decoded = packet.get('decoded', {})
msg_type = decoded.get('portnum', '')
if msg_type == 'TEXT_MESSAGE_APP':
text = decoded.get('payload', b'').decode('utf-8')
sender = packet.get('fromId', 'Unknown')
print(f"\n💬 Сообщение от {sender}: {text}")
self.messages.append({
'from': sender,
'text': text,
'time': time.time()
})
elif msg_type == 'POSITION_APP':
pos = decoded.get('position', {})
print(f"\n📍 Позиция от {packet.get('fromId', '?')}")
elif msg_type == 'TELEMETRY_APP':
telemetry = decoded.get('telemetry', {})
print(f"\n📊 Телеметрия от {packet.get('fromId', '?')}")
def disconnect(self):
"""Закрытие подключения."""
if self.interface:
self.interface.close()
print("🔌 Отключено")
# === Пример использования ===
if __name__ == "__main__":
node = MeshtasticNode(port="/dev/ttyUSB0")
if node.connect():
# Отправка broadcast-сообщения
node.send_text("Привет, mesh-сеть! 📡")
# Отправка позиции
node.send_position(55.7558, 37.6173, 150)
# Вывод списка узлов
node.print_node_list()
# Ожидание сообщений (30 секунд)
print("\n⏳ Ожидание сообщений...")
time.sleep(30)
# Отключение
node.disconnect()
Каналы и шифрование
Meshtastic поддерживает до 8 каналов одновременно. Каждый канал может иметь свой ключ шифрования и настройки. Каналы позволяют создавать изолированные группы внутри одной mesh-сети.
# Показать текущие каналы
$ meshtastic --get channels
# Добавить новый канал с именем "Team-Alpha"
$ meshtastic --ch-add Team-Alpha
# Установить PSK (Pre-Shared Key) для канала
$ meshtastic --ch-set psk "1PG7OiApB1nwvP+rz05pAQ==" --ch-index 1
# Установить канал как UPLINK (передача в интернет)
$ meshtastic --ch-set uplink_enabled true --ch-index 1
# Установить канал как DOWNLINK (приём из интернета)
$ meshtastic --ch-set downlink_enabled true --ch-index 1
# Удалить канал
$ meshtastic --ch-del --ch-index 1
# Переключиться на канал по умолчанию
$ meshtastic --set lora.region EU_868
# Генерация случайного PSK
$ meshtastic --ch-set psk random --ch-index 1
# Экспорт URL канала для шаринга
$ meshtastic --ch-url --ch-index 0
# https://meshtastic.org/e/... (длинная ссылка с настройками канала)
Безопасность: По умолчанию Meshtastic использует общий ключ для всех устройств на канале. Для приватной коммуникации обязательно установите собственный PSK. Ключ должен быть известен только участникам вашей группы.
MQTT-интеграция
Meshtastic поддерживает подключение к MQTT-брокеру для связи между локальными mesh-сетями через интернет.
# Включение MQTT
$ meshtastic --set mqtt.enabled true
# Установка адреса MQTT-брокера
$ meshtastic --set mqtt.address "mqtt.meshtastic.org"
# Установка имени пользователя и пароля
$ meshtastic --set mqtt.username "meshdev"
$ meshtastic --set mqtt.password "large4cats"
# Корневой топик
$ meshtastic --set mqtt.root "msh"
# Включение шифрования MQTT
$ meshtastic --set mqtt.encryption_enabled true
# Включение JSON-вывода
$ meshtastic --set mqtt.json_enabled true
# Включение TLS
$ meshtastic --set mqtt.tls_enabled true
# Карта (Map reporting)
$ meshtastic --set mqtt.map_reporting_enabled true
$ meshtastic --set mqtt.map_report_settings.publish_interval_secs 900
$ meshtastic --set mqtt.map_report_settings.position_precision 13
Роли узлов
CLIENT
Стандартная роль. Устройство работает как клиент mesh-сети, отправляет и получает сообщения, ретранслирует трафик.
ROUTER
Оптимизирован для ретрансляции. Не отправляет собственные beacon-пакеты, но активно маршрутизирует чужой трафик. Идеален для стационарных ретрансляторов.
CLIENT_MUTE
Клиент, который не ретранслирует чужие сообщения. Полезен для устройств с ограниченным питанием.
REPEATER
Чистый ретранслятор. Не имеет пользовательского интерфейса, только пересылает пакеты между узлами.
ROUTER_CLIENT
Комбинация роутера и клиента. Ретранслирует трафик и может отправлять/получать собственные сообщения.
TRACKER
Оптимизирован для GPS-трекинга. Регулярно отправляет свою позицию, минимизируя другой трафик.
# Установка роли ROUTER (ретранслятор)
$ meshtastic --set device.role ROUTER
# Установка роли CLIENT (обычный клиент)
$ meshtastic --set device.role CLIENT
# Установка роли TRACKER (GPS-трекер)
$ meshtastic --set device.role TRACKER
# Установка роли SENSOR (датчик)
$ meshtastic --set device.role SENSOR
# Установка роли TAK (для ATAK)
$ meshtastic --set device.role TAK
MeshCore
Высокопроизводительная mesh-сеть на базе WiFi для ESP32. Скорость передачи данных до нескольких Мбит/с без точки доступа и интернет-подключения.
Что такое MeshCore?
MeshCore — это библиотека для создания самоорганизующихся mesh-сетей на базе чипов ESP32 с использованием WiFi. В отличие от Meshtastic, который использует LoRa для дальней связи с низкой скоростью, MeshCore обеспечивает высокоскоростную передачу данных на средних расстояниях (до 100-500 метров между узлами).
MeshCore реализует протокол ESP-MESH — сетевой протокол, встроенный в ESP-IDF, который позволяет множеству устройств ESP32 формировать единую mesh-сеть без необходимости в центральном маршрутизаторе или точке доступа. Каждый узел может общаться с любым другим узлом в сети, а данные автоматически маршрутизируются через промежуточные узлы.
Ключевое отличие: MeshCore обеспечивает скорость передачи данных в тысячи раз выше, чем Meshtastic (Мбит/с против кбит/с), но на значительно меньших расстояниях.
Архитектура MeshCore
MeshCore использует древовидную топологию с автоматическим выбором корневого узла. Корневой узел (root node) может подключаться к внешней WiFi-сети для обеспечения доступа в интернет для всей mesh-сети.
Разработка с MeshCore
Базовая инициализация (Arduino/ESP-IDF)
/*
* MeshCore — Базовый пример узла mesh-сети
*
* Этот пример демонстрирует создание простого узла MeshCore,
* который может отправлять и получать сообщения в mesh-сети.
*
* Требуемое оборудование:
* - ESP32 (любая модель с WiFi)
* - USB-кабель для прошивки
*
* Библиотеки:
* - MeshCore (https://github.com/...)
* - WiFi
*/
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_mesh.h>
#include <esp_mesh_internal.h>
#include <MeshCore.h>
// === Конфигурация сети ===
#define MESH_SSID "MeshCore_Network"
#define MESH_PASSWORD "mesh_secure_pass"
#define MESH_PORT 5555
#define MESH_CHANNEL 6
#define MESH_MAX_LAYER 6 // Максимальная глубина дерева
// === Глобальные переменные ===
MeshCore mesh;
bool isRoot = false;
int nodeLayer = 0;
uint32_t lastSendTime = 0;
const uint32_t SEND_INTERVAL = 5000; // 5 секунд
// === Callback-функции ===
// Вызывается при получении сообщения
void onReceive(const uint8_t* data, size_t len, const mesh_addr_t& from) {
char message[256] = {0};
memcpy(message, data, min(len, sizeof(message) - 1));
Serial.printf(
"📨 Получено от " MACSTR ": %s\n",
MAC2STR(from.addr), message
);
// Обработка специальных команд
if (strcmp(message, "STATUS") == 0) {
char response[128];
snprintf(response, sizeof(response),
"Node: %s, Layer: %d, RSSI: %d",
WiFi.macAddress().c_str(),
nodeLayer,
WiFi.RSSI()
);
mesh.send((const uint8_t*)response, strlen(response));
}
}
// Вызывается при изменении состояния mesh
void onMeshEvent(mesh_event_t* event) {
switch (event->id) {
case MESH_EVENT_STARTED:
Serial.println("🟢 Mesh-сеть запущена");
break;
case MESH_EVENT_CONNECTED:
Serial.println("🔗 Подключено к mesh-сети");
break;
case MESH_EVENT_ROOT_ADDRESS:
Serial.println("👑 Этот узел стал ROOT");
isRoot = true;
break;
case MESH_EVENT_PARENT_CONNECTED:
Serial.println("📡 Подключено к родительскому узлу");
nodeLayer = event->info.parent_connected.self_layer;
Serial.printf(" Уровень в дереве: %d\n", nodeLayer);
break;
case MESH_EVENT_PARENT_DISCONNECTED:
Serial.println("❌ Потеряна связь с родителем");
break;
case MESH_EVENT_NO_PARENT_FOUND:
Serial.println("⚠️ Родительский узел не найден");
break;
case MESH_EVENT_CHILD_CONNECTED:
Serial.printf("👶 Новый дочерний узел подключён\n");
break;
case MESH_EVENT_CHILD_DISCONNECTED:
Serial.printf("👋 Дочерний узел отключился\n");
break;
case MESH_EVENT_ROUTING_TABLE_CHANGE:
Serial.println("🗺️ Таблица маршрутизации обновлена");
break;
default:
break;
}
}
void setup() {
Serial.begin(115200);
Serial.println("\n=== MeshCore Node ===");
// Инициализация WiFi в режиме mesh
WiFi.mode(WIFI_MODE_APSTA);
// Конфигурация mesh-сети
mesh_cfg_t cfg = {0};
cfg.channel = MESH_CHANNEL;
cfg.router.ssid = MESH_SSID;
cfg.router.password = MESH_PASSWORD;
cfg.mesh_id = {0x77, 0x77, 0x77, 0x77, 0x77, 0x77};
cfg.mesh_ap.max_connection = 6;
cfg.crypto_funcs = &g_wifi_default_mesh_crypto_funcs;
// Инициализация MeshCore
if (mesh.init(&cfg)) {
Serial.println("✅ MeshCore инициализирован");
} else {
Serial.println("❌ Ошибка инициализации MeshCore");
return;
}
// Установка callback-функций
mesh.onReceive(onReceive);
mesh.onEvent(onMeshEvent);
// Запуск mesh-сети
mesh.start();
Serial.println("🚀 Mesh-сеть запущена, ожидание подключения...");
}
void loop() {
// Обработка mesh-событий
mesh.loop();
// Периодическая отправка статуса
uint32_t now = millis();
if (now - lastSendTime > SEND_INTERVAL) {
lastSendTime = now;
char status[128];
snprintf(status, sizeof(status),
"Heartbeat from %s | Layer: %d | RSSI: %d dBm | FreeMem: %d",
WiFi.macAddress().c_str(),
nodeLayer,
WiFi.RSSI(),
ESP.getFreeHeap()
);
if (mesh.send((const uint8_t*)status, strlen(status))) {
Serial.printf("📤 Отправлен heartbeat\n");
}
}
delay(100);
}
MeshCore с ESP-IDF (продвинутый)
/*
* ESP-MESH — Продвинутый пример с ESP-IDF
*
* Демонстрирует:
* - Создание mesh-сети с ESP-MESH
* - Обработку событий сети
* - Передачу данных между узлами
* - OTA-обновления через mesh
* - Мониторинг состояния сети
*/
#include <string.h>
#include <esp_log.h>
#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_mesh.h>
#include <esp_mesh_internal.h>
#include <nvs_flash.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
static const char* TAG = "mesh_main";
// Конфигурация
#define MESH_SSID "ESP-MESH-NET"
#define MESH_PASSWD "meshpassword"
#define MESH_PORT 5555
#define RX_SIZE 1500
#define TX_SIZE 1500
#define MESH_CHANNEL 6
// Глобальные переменные
static bool is_mesh_connected = false;
static mesh_addr_t mesh_parent_addr;
static int mesh_layer = -1;
static esp_netif_t* netif_sta = NULL;
static esp_netif_t* netif_ap = NULL;
// Очередь сообщений
static QueueHandle_t mesh_msg_queue;
// Структура сообщения
typedef struct {
mesh_addr_t from;
uint8_t data[RX_SIZE];
int len;
mesh_data_t mesh_data;
} mesh_message_t;
// ============================================
// WiFi Event Handler
// ============================================
static void wifi_event_handler(void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
ESP_LOGI(TAG, "WiFi STA started");
} else if (event_base == WIFI_EVENT &&
event_id == WIFI_EVENT_STA_DISCONNECTED) {
ESP_LOGI(TAG, "WiFi STA disconnected");
is_mesh_connected = false;
}
}
// ============================================
// IP Event Handler
// ============================================
static void ip_event_handler(void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data)
{
if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
ESP_LOGI(TAG, "Got IP: " IPSTR,
IP2STR(&event->ip_info.ip));
}
}
// ============================================
// Mesh Event Handler
// ============================================
static void mesh_event_handler(void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data)
{
mesh_event_t* event = (mesh_event_t*)event_data;
switch (event_id) {
case MESH_EVENT_STARTED: {
ESP_LOGI(TAG,
"MESH_EVENT_STARTED: Mesh network initialized");
break;
}
case MESH_EVENT_STOPPED: {
ESP_LOGI(TAG,
"MESH_EVENT_STOPPED: Mesh network stopped");
is_mesh_connected = false;
break;
}
case MESH_EVENT_CHILD_CONNECTED: {
mesh_event_child_connected_t* child_connected =
(mesh_event_child_connected_t*)event_data;
ESP_LOGI(TAG,
"MESH_EVENT_CHILD_CONNECTED: child " MACSTR
" at layer %d",
MAC2STR(child_connected->mac),
child_connected->self_layer);
break;
}
case MESH_EVENT_CHILD_DISCONNECTED: {
mesh_event_child_disconnected_t* child_disconnected =
(mesh_event_child_disconnected_t*)event_data;
ESP_LOGI(TAG,
"MESH_EVENT_CHILD_DISCONNECTED: child " MACSTR,
MAC2STR(child_disconnected->mac));
break;
}
case MESH_EVENT_ROUTING_TABLE_CHANGE: {
mesh_event_routing_table_change_t* routing =
(mesh_event_routing_table_change_t*)event_data;
ESP_LOGI(TAG,
"MESH_EVENT_ROUTING_TABLE_CHANGE: size %d, capacity %d",
routing->rt_size, routing->rt_capacity);
break;
}
case MESH_EVENT_PARENT_CONNECTED: {
mesh_event_connected_t* connected =
(mesh_event_connected_t*)event_data;
is_mesh_connected = true;
mesh_layer = connected->self_layer;
memcpy(&mesh_parent_addr, &connected->parent_bssid,
sizeof(mesh_addr_t));
ESP_LOGI(TAG,
"MESH_EVENT_PARENT_CONNECTED: layer %d, parent " MACSTR,
mesh_layer, MAC2STR(connected->parent_bssid));
break;
}
case MESH_EVENT_PARENT_DISCONNECTED: {
ESP_LOGI(TAG,
"MESH_EVENT_PARENT_DISCONNECTED");
is_mesh_connected = false;
mesh_layer = -1;
break;
}
case MESH_EVENT_NO_PARENT_FOUND: {
ESP_LOGI(TAG,
"MESH_EVENT_NO_PARENT_FOUND: scan done, %d APs found",
event->info.no_parent.scan_size);
break;
}
case MESH_EVENT_ROOT_ADDRESS: {
mesh_event_root_address_t* root_addr =
(mesh_event_root_address_t*)event_data;
ESP_LOGI(TAG,
"MESH_EVENT_ROOT_ADDRESS: root " MACSTR,
MAC2STR(root_addr->addr));
break;
}
case MESH_EVENT_TODS_STATE: {
mesh_event_toDS_state_t* toDs_state =
(mesh_event_toDS_state_t*)event_data;
ESP_LOGI(TAG,
"MESH_EVENT_TODS_STATE: %d", *toDs_state);
break;
}
default:
ESP_LOGI(TAG, "Mesh event: %ld", event_id);
break;
}
}
// ============================================
// TX Task — отправка данных
// ============================================
static void mesh_tx_task(void* arg)
{
mesh_data_t data;
uint8_t tx_buf[TX_SIZE] = {0};
int tx_count = 0;
data.data = tx_buf;
data.size = 0;
data.proto = MESH_PROTO_BIN;
data.tos = MESH_TOS_P2P;
while (1) {
// Формирование сообщения
int len = snprintf((char*)tx_buf, TX_SIZE,
"{"
"\"node\":\"%s\","
"\"layer\":%d,"
"\"seq\":%d,"
"\"rssi\":%d,"
"\"heap\":%lu,"
"\"uptime\":%lu"
"}",
"ESP32-Node",
mesh_layer,
tx_count++,
esp_wifi_sta_get_ap_info(NULL) == ESP_OK ?
esp_wifi_get_rssi() : 0,
(unsigned long)esp_get_free_heap_size(),
(unsigned long)(xTaskGetTickCount() * portTICK_PERIOD_MS / 1000)
);
data.size = len;
// Отправка родительскому узлу (вверх по дереву)
esp_err_t err = esp_mesh_send(
NULL, // NULL = отправить родителю
&data,
MESH_DATA_TODS, // Направление: к корню
NULL,
0
);
if (err == ESP_OK) {
ESP_LOGI(TAG, "TX: sent %d bytes, seq=%d", len, tx_count);
} else {
ESP_LOGW(TAG, "TX: failed, err=%d", err);
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
}
// ============================================
// RX Task — приём данных
// ============================================
static void mesh_rx_task(void* arg)
{
mesh_data_t data;
uint8_t rx_buf[RX_SIZE] = {0};
int flag = 0;
data.data = rx_buf;
data.size = RX_SIZE;
while (1) {
mesh_addr_t from;
data.size = RX_SIZE;
esp_err_t err = esp_mesh_recv(
&from,
&data,
portMAX_DELAY,
&flag,
NULL,
0
);
if (err == ESP_OK) {
// Обработка полученного сообщения
ESP_LOGI(TAG,
"RX: %d bytes from " MACSTR ", flag=0x%04x",
data.size, MAC2STR(from.addr), flag);
// Парсинг JSON-сообщения
if (data.proto == MESH_PROTO_BIN) {
char msg_str[RX_SIZE] = {0};
memcpy(msg_str, data.data,
min(data.size, RX_SIZE - 1));
ESP_LOGI(TAG, "RX data: %s", msg_str);
// Отправка в очередь для дальнейшей обработки
mesh_message_t msg;
msg.from = from;
msg.len = data.size;
memcpy(msg.data, data.data, data.size);
xQueueSend(mesh_msg_queue, &msg, 0);
}
} else {
ESP_LOGW(TAG, "RX: error=%d", err);
}
}
}
// ============================================
// Network Monitor Task
// ============================================
static void mesh_monitor_task(void* arg)
{
while (1) {
int layer = -1;
int node_count = 0;
mesh_assoc_t assoc;
// Получение текущего уровня
esp_mesh_get_layer(&layer);
// Получение количества дочерних узлов
esp_mesh_get_routing_table_size(&node_count);
// Получение информации о родителе
esp_mesh_get_parent_bssid(&assoc);
ESP_LOGI(TAG,
"📊 Network: layer=%d, nodes=%d, parent_rssi=%d, heap=%lu",
layer,
node_count,
assoc.rssi,
(unsigned long)esp_get_free_heap_size()
);
vTaskDelay(10000 / portTICK_PERIOD_MS);
}
}
// ============================================
// Main — app_main
// ============================================
void app_main(void)
{
// Инициализация NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
// Инициализация сетевого стека
esp_netif_init();
esp_event_loop_create_default();
// Создание сетевых интерфейсов
netif_sta = esp_netif_create_default_wifi_sta();
netif_ap = esp_netif_create_default_wifi_ap();
// Инициализация WiFi
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
esp_wifi_init(&cfg);
// Регистрация обработчиков событий
esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL);
esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&ip_event_handler, NULL);
esp_event_handler_register(MESH_EVENT, ESP_EVENT_ANY_ID,
&mesh_event_handler, NULL);
// Конфигурация mesh
mesh_cfg_t mesh_cfg = {0};
uint8_t mesh_id[6] = {0x77, 0x77, 0x77,
0x77, 0x77, 0x77};
mesh_cfg.channel = MESH_CHANNEL;
mesh_cfg.router.ssid = MESH_SSID;
mesh_cfg.router.password = MESH_PASSWD;
memcpy(&mesh_cfg.mesh_id, mesh_id, 6);
mesh_cfg.mesh_ap.max_connection = 10;
// Запуск mesh
esp_mesh_init();
esp_mesh_set_config(&mesh_cfg);
esp_mesh_start();
// Создание очереди сообщений
mesh_msg_queue = xQueueCreate(32, sizeof(mesh_message_t));
// Запуск задач
xTaskCreate(mesh_tx_task, "mesh_tx",
4096, NULL, 5, NULL);
xTaskCreate(mesh_rx_task, "mesh_rx",
4096, NULL, 5, NULL);
xTaskCreate(mesh_monitor_task, "mesh_mon",
4096, NULL, 3, NULL);
ESP_LOGI(TAG, "🚀 MeshCore node started!");
}
Производительность MeshCore
Зависимость скорости от количества прыжков
Обратите внимание: Скорость падает с каждым прыжком, так как каждый промежуточный узел должен получить и переслать пакет. Для максимальной производительности старайтесь минимизировать количество hops между узлами.
Reticulum
Универсальный сетевой стек для создания зашифрованных mesh-сетей поверх любых сред передачи данных. Поддержка LXMF, Sideband и множества интерфейсов.
Что такое Reticulum?
Reticulum Network Stack — это криптографически защищённый сетевой стек, разработанный Марком Квистом (markqvist). Он предназначен для создания надёжных mesh-сетей поверх практически любых физических сред передачи: LoRa, WiFi, Ethernet, serial, packet radio, I2P и других.
Reticulum не привязан к конкретной технологии связи. Вместо этого он предоставляет абстрактный сетевой слой, который может работать поверх любого интерфейса (Interface). Это делает Reticulum невероятно гибким: вы можете создать сеть, в которой одни узлы связаны через LoRa, другие через WiFi, а третьи через Ethernet — и все они будут частью единой mesh-сети.
Криптография: Reticulum использует X25519 для обмена ключами, Ed25519 для цифровых подписей и AES-256 для шифрования данных. Каждый узел имеет уникальный 256-битный адрес, производный от его публичного ключа.
Архитектура Reticulum
Ключевые концепции
Identity (Идентификация)
Каждый узел имеет криптографическую идентичность — пару ключей X25519/Ed25519. Адрес узла — это хеш его публичного ключа. Это обеспечивает уникальность и аутентификацию без центрального органа.
Announce (Объявление)
Узлы периодически отправляют announce-пакеты, содержащие их публичный ключ и аспекты (имена сервисов). Это позволяет другим узлам обнаруживать сервисы и строить маршруты.
Link (Связь)
Link — это зашифрованное соединение между двумя узлами. Устанавливается через обмен ключами Диффи-Хеллмана. Обеспечивает надёжную доставку с подтверждениями.
LXMF (Сообщения)
Lightweight Extensible Message Format — протокол обмена сообщениями поверх Reticulum. Поддерживает асинхронную доставку через propagation-узлы (аналог почтовых серверов).
Установка и настройка Reticulum
# Установка Reticulum через pip
$ pip install rns
# Установка LXMF (протокол сообщений)
$ pip install lxmf
# Установка Sideband (CLI-клиент)
$ pip install sideband
# Установка Nomad Network (mesh-браузер)
$ pip install nomadnet
# Установка Reticulum Utilities
$ pip install rnsh
# Проверка установки
$ rns --version
# Reticulum Network Stack 0.7.2
# Инициализация (создаёт конфигурацию и ключи)
$ rns --init
# Запуск Reticulum в фоновом режиме
$ rnsd
# Запуск с выводом логов
$ rnsd -v
# Запуск с максимальным уровнем логирования
$ rnsd -vvv
Конфигурационный файл
# ============================================
# Reticulum Network Stack Configuration
# ============================================
# Основные настройки
[reticulum]
# Включить режим transport-узла (ретрансляция)
enable_transport = true
# Share instance statistics
share_instance = true
# Порт TCP-интерфейса (для входящих подключений)
shared_instance_port = 37428
# ============================================
# Интерфейсы
# ============================================
# --- LoRa через RNode (Serial) ---
[[interfaces]]
[[rnode_lora]]
# Тип интерфейса
type = RNodeInterface
# Serial-порт устройства RNode
port = /dev/ttyUSB0
# Скорость serial-соединения
speed = 115200
# Частота LoRa (868 МГц для Европы)
frequency = 868500000
# Ширина полосы
bandwidth = 125000
# Spreading Factor
txpower = 17
# Spreading Factor
spreadingfactor = 10
# Coding Rate
codingrate = 5
# Airtime limit (для соблюдения duty cycle)
airtime_limit = 1.0
# --- WiFi (TCP/IP Server) ---
[[interfaces]]
[[tcp_server]]
type = TCPServerInterface
listen_port = 4242
# --- WiFi (TCP/IP Client к другому узлу) ---
[[interfaces]]
[[tcp_client]]
type = TCPClientInterface
target_host = 192.168.1.100
target_port = 4242
# --- I2P Interface (для анонимной маршрутизации) ---
[[interfaces]]
[[i2p_interface]]
type = I2PInterface
peers = "i2p_address_1.b32.i2p,i2p_address_2.b32.i2p"
# --- Serial Interface (прямое подключение) ---
[[interfaces]]
[[serial_interface]]
type = SerialInterface
port = /dev/ttyUSB1
speed = 9600
# --- UDP Interface ---
[[interfaces]]
[[udp_interface]]
type = UDPInterface
listen_port = 2971
forward_port = 2971
# ============================================
# LXMF Configuration
# ============================================
[lxmf]
# Propagation node settings
propagation_node = false
# Если true, этот узел будет хранить и
# пересылать LXMF-сообщения для других
offer_propagation = false
# Максимальный размер хранилища (в байтах)
propagation_storage_limit = 104857600 # 100 MB
# Интервал синхронизации (секунды)
propagation_sync_interval = 3600
Reticulum Python API
"""
Reticulum — Базовое приложение для обмена сообщениями
Этот пример демонстрирует:
- Создание Reticulum-идентичности
- Объявление сервиса (announce)
- Установление зашифрованного линка
- Отправку и приём сообщений
- Обработку входящих соединений
"""
import RNS
import time
import sys
import threading
class MeshMessenger:
"""
Простой мессенджер поверх Reticulum Network Stack.
Поддерживает обнаружение пиров, зашифрованную связь
и асинхронный обмен сообщениями.
"""
# Аспект приложения (имя сервиса в сети Reticulum)
APP_NAME = "meshmessenger"
APP_ASPECT = "messenger.example"
def __init__(self, identity_path=None):
"""
Инициализация мессенджера.
Args:
identity_path: Путь к файлу идентичности.
Если None, создаётся новая.
"""
self.reticulum = None
self.identity = None
self.destination = None
self.active_links = {}
self.message_history = []
self.known_peers = {}
# Инициализация Reticulum
self._init_reticulum(identity_path)
def _init_reticulum(self, identity_path):
"""Инициализация Reticulum Network Stack."""
try:
# Создание или загрузка идентичности
if identity_path:
self.identity = RNS.Identity.from_file(identity_path)
RNS.log("Загружена существующая идентичность")
else:
self.identity = RNS.Identity()
RNS.log("Создана новая идентичность")
# Инициализация Reticulum
self.reticulum = RNS.Reticulum()
# Создание Destination (адреса в сети)
self.destination = RNS.Destination(
self.identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
self.APP_NAME,
self.APP_ASPECT
)
# Установка обработчика входящих линков
self.destination.set_link_established_callback(
self.on_link_established
)
# Установка обработчика входящих данных
self.destination.set_packet_callback(
self.on_packet
)
# Объявление сервиса в сети
self.destination.announce()
RNS.log(f"🟢 Мессенджер запущен")
RNS.log(f"📡 Адрес: {self.destination.hash.hex()}")
RNS.log(f"🔑 Публичный ключ: {self.identity.hash.hex()[:16]}...")
except Exception as e:
RNS.log(f"❌ Ошибка инициализации: {e}")
raise
def announce(self):
"""Отправка объявления в сеть."""
self.destination.announce(
app_data="MeshMessenger Node Online".encode("utf-8")
)
RNS.log("📢 Announce отправлен")
def discover_peers(self):
"""
Обнаружение пиров в сети.
Сканирует сеть на наличие других узлов мессенджера.
"""
RNS.log("🔍 Поиск пиров...")
# Получение списка известных announce
for dest_hash, announce_data in \
RNS.Transport.destination_table().items():
if announce_data["aspect"] == self.APP_ASPECT:
peer_hash = dest_hash.hex()
self.known_peers[peer_hash] = announce_data
RNS.log(f" 👤 Найден пир: {peer_hash[:16]}...")
RNS.log(f"✅ Найдено {len(self.known_peers)} пиров")
return self.known_peers
def send_message(self, dest_hash, message):
"""
Отправка зашифрованного сообщения.
Args:
dest_hash: Хеш адреса получателя (hex string)
message: Текст сообщения
"""
try:
# Создание destination получателя
dest_bytes = bytes.fromhex(dest_hash)
peer_identity = RNS.Identity.recall(dest_bytes)
if peer_identity is None:
RNS.log("❌ Идентичность получателя неизвестна")
RNS.log(" Ожидание announce от получателя...")
return False
# Создание destination получателя
peer_dest = RNS.Destination(
peer_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
self.APP_NAME,
self.APP_ASPECT
)
# Установление зашифрованного линка
link = RNS.Link(peer_dest)
link.set_link_established_callback(
self.on_link_established
)
RNS.log(f"🔗 Установление линка с {dest_hash[:16]}...")
# Ожидание установления линка
timeout = 15
while not link.status() == RNS.Link.ACTIVE:
time.sleep(0.5)
timeout -= 0.5
if timeout <= 0:
RNS.log("❌ Таймаут установления линка")
return False
RNS.log("✅ Линк установлен (зашифрован)")
# Отправка сообщения через линк
packet = RNS.Packet(
link,
message.encode("utf-8")
)
packet.send()
RNS.log(f"📤 Отправлено: {message}")
# Сохранение в истории
self.message_history.append({
"direction": "out",
"peer": dest_hash,
"message": message,
"time": time.time()
})
return True
except Exception as e:
RNS.log(f"❌ Ошибка отправки: {e}")
return False
def on_packet(self, message, packet):
"""Обработчик входящих пакетов."""
try:
text = message.decode("utf-8")
sender = packet.link.destination.hash.hex()
RNS.log(f"💬 Сообщение от {sender[:16]}: {text}")
self.message_history.append({
"direction": "in",
"peer": sender,
"message": text,
"time": time.time()
})
except Exception as e:
RNS.log(f"❌ Ошибка обработки пакета: {e}")
def on_link_established(self, link):
"""Обработчик установления линка."""
link_hash = link.link_id.hex()
self.active_links[link_hash] = link
RNS.log(f"🔗 Линк установлен: {link_hash[:16]}")
# Установка обработчика входящих данных для линка
link.set_packet_callback(self.on_packet)
link.set_link_closed_callback(self.on_link_closed)
def on_link_closed(self, link):
"""Обработчик закрытия линка."""
link_hash = link.link_id.hex()
if link_hash in self.active_links:
del self.active_links[link_hash]
RNS.log(f"🔌 Линк закрыт: {link_hash[:16]}")
def get_status(self):
"""Получение статуса узла."""
return {
"address": self.destination.hash.hex(),
"known_peers": len(self.known_peers),
"active_links": len(self.active_links),
"messages": len(self.message_history),
"interfaces": len(self.reticulum.interfaces)
}
# === Пример использования ===
if __name__ == "__main__":
# Создание мессенджера
messenger = MeshMessenger()
# Периодическое объявление
def announce_loop():
while True:
messenger.announce()
time.sleep(300) # каждые 5 минут
announce_thread = threading.Thread(
target=announce_loop, daemon=True
)
announce_thread.start()
# Основной цикл
try:
while True:
# Обнаружение пиров
peers = messenger.discover_peers()
# Отправка тестового сообщения первому найденному пиру
if peers:
first_peer = list(peers.keys())[0]
messenger.send_message(
first_peer,
"Привет из Reticulum! 🔐"
)
# Вывод статуса
status = messenger.get_status()
RNS.log(f"📊 Статус: {status}")
time.sleep(60)
except KeyboardInterrupt:
RNS.log("\n👋 Завершение работы...")
sys.exit(0)
LXMF — Lightweight Extensible Message Format
LXMF — это протокол обмена сообщениями, работающий поверх Reticulum. Он обеспечивает асинхронную доставку сообщений, подобно электронной почте, но в децентрализованной mesh-сети.
"""
LXMF — Пример отправки и получения сообщений
LXMF (Lightweight Extensible Message Format) — протокол
асинхронного обмена сообщениями поверх Reticulum.
"""
import RNS
import LXMF
import time
def lxmf_delivery_callback(message):
"""Обработчик доставленного сообщения."""
print(f"✅ Сообщение доставлено: {message.hash.hex()[:16]}")
def lxmf_failed_callback(message):
"""Обработчик недоставленного сообщения."""
print(f"❌ Не удалось доставить: {message.hash.hex()[:16]}")
def lxmf_incoming(message):
"""Обработчик входящего LXMF-сообщения."""
source = message.source_hash.hex()
content = message.content.decode("utf-8")
title = message.title.decode("utf-8")
timestamp = message.timestamp
print(f"\n📬 Новое сообщение!")
print(f" От: {source[:16]}...")
print(f" Тема: {title}")
print(f" Текст: {content}")
print(f" Время: {time.ctime(timestamp)}")
print(f" Размер: {len(content)} байт")
# Инициализация Reticulum
reticulum = RNS.Reticulum()
# Создание идентичности пользователя
user_identity = RNS.Identity()
# Создание LXMF-роутера
lxmf_router = LXMF.LXMRouter(
identity=user_identity,
storagepath="./lxmf_storage"
)
# Создание LXMF-адреса (destination)
lxmf_destination = lxmf_router.register_delivery_identity(
user_identity,
display_name="MyLXMFNode"
)
# Установка обработчика входящих сообщений
lxmf_router.register_delivery_callback(lxmf_incoming)
# Объявление LXMF-сервиса
lxmf_router.announce(lxmf_destination)
print(f"📡 LXMF-адрес: {lxmf_destination.hex()}")
print(f"🔑 Имя: MyLXMFNode")
# Отправка LXMF-сообщения
# (нужно знать LXMF-адрес получателя)
dest_lxmf_addr = "a1b2c3d4e5f6..." # адрес получателя
dest_bytes = bytes.fromhex(dest_lxmf_addr)
dest_identity = RNS.Identity.recall(dest_bytes)
if dest_identity:
# Создание сообщения
lxmessage = LXMF.LXMessage(
destination=dest_identity,
source=user_identity,
content="Привет! Это тестовое сообщение через LXMF 📨",
title="Тест LXMF",
desired_method=LXMF.LXMessage.DIRECT
)
# Установка callback-функций
lxmessage.set_delivery_callback(lxmf_delivery_callback)
lxmessage.set_failed_callback(lxmf_failed_callback)
# Отправка
lxmf_router.handle_outbound(lxmessage)
print("📤 Сообщение отправлено!")
else:
print("❌ Идентичность получателя неизвестна")
print(" Подождите announce от получателя")
# Ожидание сообщений
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\n👋 Завершение...")
Совет: Для удобной работы с LXMF используйте приложение Sideband — это полноценный клиент для обмена сообщениями через Reticulum/LXMF с графическим интерфейсом (доступен для Linux, Android).
Сравнение протоколов
Детальное сравнение Meshtastic, MeshCore и Reticulum по ключевым параметрам для выбора оптимального решения.
Детальное сравнение
| Параметр | 🟢 Meshtastic | 🔵 MeshCore | 🟣 Reticulum |
|---|---|---|---|
| Технология связи | LoRa (Sub-GHz) | WiFi 2.4 ГГц | Мульти-интерфейс |
| Дальность (1 hop) | до 20+ км | до 100-500 м | зависит от интерфейса |
| Скорость | 0.3 — 50 кбит/с | до 5 Мбит/с | зависит от интерфейса |
| Частота | 433/868/915 МГц | 2.4 ГГц | любая |
| Платформа | ESP32, nRF52, RP2040 | ESP32 | Python (любая ОС) |
| Шифрование | AES-256 (PSK) | WPA2 (WiFi) | X25519 + AES-256 |
| Маршрутизация | Controlled Flooding | Tree-based | Announce-based |
| Макс. узлов | ~100-200 | ~1000 | практически ∞ |
| Энергопотребление | Очень низкое | Среднее | зависит от интерфейса |
| GPS-трекинг | ✅ Встроенный | ❌ Нет | ⚠️ Через приложение |
| Мобильное приложение | ✅ iOS + Android | ❌ Нет | ⚠️ Sideband (Android) |
| MQTT-интеграция | ✅ Встроенная | ❌ Нет | ⚠️ Через rnsh |
| Язык разработки | C++ (Arduino) | C/C++ (ESP-IDF) | Python |
| Сложность настройки | 🟢 Низкая | 🟡 Средняя | 🟠 Высокая |
| Сообщество | 🟢 Большое | 🟡 Среднее | 🟡 Среднее |
| Стоимость узла | $15 — $50 | $5 — $15 | $5 — $100+ |
Когда что выбрать?
Выберите Meshtastic, если:
- Нужна связь на больших расстояниях
- Важна автономность (батарея)
- Нужен GPS-трекинг
- Хотите простое мобильное приложение
- Работаете в условиях без WiFi
- Нужна быстрая настройка
Выберите MeshCore, если:
- Нужна высокая скорость данных
- Работаете в помещении/городе
- Передаёте файлы или видео
- Нужна большая сеть (100+ узлов)
- Есть доступ к розетке
- Знаете C/C++ и ESP-IDF
Выберите Reticulum, если:
- Нужна максимальная гибкость
- Хотите комбинировать интерфейсы
- Важна криптографическая безопасность
- Нужна асинхронная доставка (LXMF)
- Знаете Python
- Строите сложную инфраструктуру
Примеры кода
Практические примеры для всех трёх технологий. От базовых до продвинутых.
Meshtastic: GPS-трекер
/*
* Meshtastic GPS-трекер
*
* Автоматически отправляет GPS-координаты в mesh-сеть
* с настраиваемым интервалом.
*
* Оборудование:
* - ESP32 + LoRa модуль (SX1276/SX1262)
* - GPS модуль (NEO-6M или аналог)
* - OLED дисплей (опционально)
*/
#include <Arduino.h>
#include <TinyGPS++.h>
#include <HardwareSerial.h>
#include <U8g2lib.h>
// GPS на Serial2
HardwareSerial gpsSerial(2);
TinyGPSPlus gps;
// OLED дисплей
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(
U8G2_R0, /* reset=*/ U8X8_PIN_NONE
);
// Настройки
const unsigned long GPS_UPDATE_INTERVAL = 1000; // 1 сек
const unsigned long POSITION_SEND_INTERVAL = 30000; // 30 сек
unsigned long lastGpsUpdate = 0;
unsigned long lastPositionSend = 0;
float currentLat = 0.0;
float currentLon = 0.0;
float currentAlt = 0.0;
int satellites = 0;
bool gpsValid = false;
void setup() {
Serial.begin(115200);
gpsSerial.begin(9600, SERIAL_8N1, 16, 17);
// Инициализация дисплея
display.begin();
display.setFont(u8g2_font_ncenB08_tr);
display.clearBuffer();
display.drawStr(10, 30, "GPS Tracker");
display.drawStr(10, 50, "Initializing...");
display.sendBuffer();
Serial.println("🛰️ GPS Tracker started");
}
void updateGPS() {
while (gpsSerial.available() > 0) {
char c = gpsSerial.read();
gps.encode(c);
}
if (gps.location.isUpdated()) {
currentLat = gps.location.lat();
currentLon = gps.location.lng();
currentAlt = gps.altitude.meters();
satellites = gps.satellites.value();
gpsValid = true;
Serial.printf(
"📍 GPS: %.6f, %.6f, %.1fm (%d sat)\n",
currentLat, currentLon, currentAlt, satellites
);
}
}
void updateDisplay() {
display.clearBuffer();
if (gpsValid) {
char latStr[20], lonStr[20], altStr[20], satStr[20];
snprintf(latStr, sizeof(latStr), "Lat: %.6f", currentLat);
snprintf(lonStr, sizeof(lonStr), "Lon: %.6f", currentLon);
snprintf(altStr, sizeof(altStr), "Alt: %.0fm", currentAlt);
snprintf(satStr, sizeof(satStr), "Sat: %d", satellites);
display.drawStr(5, 15, "🛰️ GPS Tracker");
display.drawStr(5, 30, latStr);
display.drawStr(5, 42, lonStr);
display.drawStr(5, 54, altStr);
display.drawStr(80, 54, satStr);
} else {
display.drawStr(5, 30, "Waiting for GPS...");
display.drawStr(5, 50, "Go outdoors!");
}
display.sendBuffer();
}
void loop() {
unsigned long now = millis();
// Обновление GPS
if (now - lastGpsUpdate > GPS_UPDATE_INTERVAL) {
lastGpsUpdate = now;
updateGPS();
updateDisplay();
}
// Отправка позиции через Meshtastic
if (gpsValid && now - lastPositionSend > POSITION_SEND_INTERVAL) {
lastPositionSend = now;
// Meshtastic автоматически отправляет позицию
// при настроенном position_broadcast_secs
Serial.println("📤 Position broadcast sent");
}
}
MeshCore: Сенсорная сеть
/*
* MeshCore Sensor Network
*
* Создаёт mesh-сеть из сенсорных узлов, которые собирают
* данные (температура, влажность) и передают их на
* корневой узел для агрегации.
*/
#include <WiFi.h>
#include <esp_mesh.h>
#include <ArduinoJson.h>
#include <DHT.h>
#define DHTPIN 4
#define DHTTYPE DHT22
#define MESH_SSID "SensorMesh"
#define MESH_PASS "sensor123"
DHT dht(DHTPIN, DHTTYPE);
struct SensorData {
float temperature;
float humidity;
float pressure;
uint32_t timestamp;
char nodeId[20];
};
void sendSensorData() {
SensorData data;
data.temperature = dht.readTemperature();
data.humidity = dht.readHumidity();
data.timestamp = millis();
strncpy(data.nodeId, WiFi.macAddress().c_str(), 19);
// Сериализация в JSON
StaticJsonDocument<256> doc;
doc["node"] = data.nodeId;
doc["temp"] = data.temperature;
doc["hum"] = data.humidity;
doc["ts"] = data.timestamp;
char jsonBuf[256];
serializeJson(doc, jsonBuf);
// Отправка через mesh
mesh_data_t meshData;
meshData.data = (uint8_t*)jsonBuf;
meshData.size = strlen(jsonBuf);
meshData.proto = MESH_PROTO_JSON;
esp_mesh_send(NULL, &meshData, MESH_DATA_TODS, NULL, 0);
}
void setup() {
Serial.begin(115200);
dht.begin();
// ... mesh init (см. MeshCore page)
}
void loop() {
static unsigned long lastSend = 0;
if (millis() - lastSend > 10000) {
lastSend = millis();
sendSensorData();
}
}
Reticulum: Чат-приложение
"""
Reticulum Mesh Chat — Интерактивный чат
Полноценное чат-приложение поверх Reticulum Network Stack
с поддержкой обнаружения пиров и зашифрованных сообщений.
"""
import RNS
import time
import threading
import sys
import os
class MeshChat:
"""Интерактивный mesh-чат поверх Reticulum."""
APP_NAME = "meshchat"
APP_ASPECT = "chat.mesh"
def __init__(self):
self.identity = RNS.Identity()
self.reticulum = RNS.Reticulum()
self.destination = RNS.Destination(
self.identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
self.APP_NAME,
self.APP_ASPECT
)
self.peers = {}
self.messages = []
self.current_peer = None
self.running = True
self.destination.set_packet_callback(self.on_message)
self.destination.announce()
print(f"🟢 MeshChat запущен")
print(f"📡 Адрес: {self.destination.hash.hex()}")
def on_message(self, data, packet):
"""Обработка входящего сообщения."""
msg = data.decode("utf-8", errors="ignore")
sender = packet.link.destination.hash.hex() if packet.link else "?"
timestamp = time.strftime("%H:%M:%S")
self.messages.append({
"from": sender,
"msg": msg,
"time": timestamp,
"dir": "in"
})
print(f"\n💬 [{timestamp}] {sender[:8]}: {msg}")
def scan_peers(self):
"""Сканирование сети на наличие пиров."""
self.peers.clear()
for dh, info in RNS.Transport.destination_table().items():
if info.get("aspect") == self.APP_ASPECT:
self.peers[dh.hex()] = info
return self.peers
def send(self, peer_hash, message):
"""Отправка сообщения пиру."""
peer_id = RNS.Identity.recall(bytes.fromhex(peer_hash))
if not peer_id:
print("❌ Пир неизвестен")
return
peer_dest = RNS.Destination(
peer_id, RNS.Destination.OUT,
RNS.Destination.SINGLE,
self.APP_NAME, self.APP_ASPECT
)
link = RNS.Link(peer_dest)
while link.status() != RNS.Link.ACTIVE:
time.sleep(0.2)
pkt = RNS.Packet(link, message.encode("utf-8"))
pkt.send()
timestamp = time.strftime("%H:%M:%S")
self.messages.append({
"to": peer_hash,
"msg": message,
"time": timestamp,
"dir": "out"
})
def announce_loop(self):
"""Периодическая отправка announce."""
while self.running:
self.destination.announce()
time.sleep(180)
def interactive_loop(self):
"""Интерактивный цикл ввода."""
announce_thread = threading.Thread(
target=self.announce_loop, daemon=True
)
announce_thread.start()
while self.running:
try:
cmd = input("\n> ").strip()
if cmd == "/scan":
peers = self.scan_peers()
print(f"🔍 Найдено {len(peers)} пиров")
for h in peers:
print(f" {h[:16]}...")
elif cmd.startswith("/msg "):
parts = cmd[5:].split(" ", 1)
if len(parts) == 2:
self.send(parts[0], parts[1])
print("📤 Отправлено")
elif cmd == "/quit":
self.running = False
print("👋 Bye!")
elif cmd == "/help":
print("Команды: /scan, /msg <hash> <text>, /quit")
except EOFError:
break
except KeyboardInterrupt:
self.running = False
print("\n👋 Bye!")
if __name__ == "__main__":
chat = MeshChat()
chat.interactive_loop()
Продвинутый: Гибридная сеть (Reticulum + LoRa)
"""
Reticulum + RNode (LoRa) — Конфигурация гибридного узла
Этот скрипт настраивает Reticulum-узел с несколькими
интерфейсами: LoRa (через RNode), WiFi (TCP) и Serial.
Узел действует как transport-node, ретранслируя трафик
между разными средами передачи.
"""
import RNS
import time
import sys
def setup_hybrid_node():
"""
Настройка гибридного Reticulum-узла с:
- LoRa через RNode (дальняя связь)
- WiFi TCP (высокоскоростная локальная связь)
- Serial (проводное подключение к датчикам)
"""
# Инициализация Reticulum с кастомным конфигом
reticulum = RNS.Reticulum(
configdir="./reticulum_config"
)
# Создание идентичности transport-узла
transport_identity = RNS.Identity()
# Создание destination для transport-узла
transport_dest = RNS.Destination(
transport_identity,
RNS.Destination.IN,
RNS.Destination.SINGLE,
"transport",
"hybrid.gateway"
)
# Объявление transport-сервиса
transport_dest.announce()
RNS.log("🟢 Hybrid transport node started")
RNS.log(f"📡 LoRa: RNode on /dev/ttyUSB0")
RNS.log(f"📶 WiFi: TCP server on port 4242")
RNS.log(f"🔌 Serial: /dev/ttyUSB1 @ 9600")
# Мониторинг интерфейсов
while True:
for iface in reticulum.interfaces:
stats = iface.get_stats()
RNS.log(
f"📊 {iface.name}: "
f"RX={stats.get('rx_bytes', 0)} "
f"TX={stats.get('tx_bytes', 0)} "
f"pkts={stats.get('rx_packets', 0)}"
)
time.sleep(30)
if __name__ == "__main__":
try:
setup_hybrid_node()
except KeyboardInterrupt:
RNS.log("\n👋 Shutting down...")
sys.exit(0)
Продвинутый: Meshtastic MQTT-шлюз
"""
Meshtastic MQTT Gateway
Мост между локальной Meshtastic mesh-сетью и MQTT-брокером.
Позволяет объединять разрозненные mesh-сети через интернет
и интегрировать их с другими системами (Home Assistant,
Node-RED, Grafana и т.д.).
"""
import meshtastic
import meshtastic.serial_interface
import paho.mqtt.client as mqtt
import json
import time
import base64
import logging
from datetime import datetime
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
class MeshtasticMQTTGateway:
"""
Шлюз между Meshtastic и MQTT.
Функции:
- Пересылка сообщений из mesh в MQTT
- Пересылка команд из MQTT в mesh
- Публикация телеметрии в MQTT-топики
- Интеграция с Home Assistant
- Логирование всех событий
"""
def __init__(self, serial_port="/dev/ttyUSB0",
mqtt_host="localhost", mqtt_port=1883,
mqtt_user=None, mqtt_pass=None):
# Meshtastic
self.serial_port = serial_port
self.interface = None
# MQTT
self.mqtt_host = mqtt_host
self.mqtt_port = mqtt_port
self.mqtt_user = mqtt_user
self.mqtt_pass = mqtt_pass
self.mqtt_client = None
self.mqtt_base_topic = "meshtastic"
# Статистика
self.stats = {
"messages_rx": 0,
"messages_tx": 0,
"telemetry_rx": 0,
"position_rx": 0,
"start_time": time.time()
}
def connect_meshtastic(self):
"""Подключение к Meshtastic-устройству."""
try:
self.interface = meshtastic.serial_interface.SerialInterface(
devPath=self.serial_port
)
my_node = self.interface.getMyNodeInfo()
logger.info(f"✅ Meshtastic подключён: "
f"{my_node.get('user', {}).get('longName')}")
return True
except Exception as e:
logger.error(f"❌ Meshtastic ошибка: {e}")
return False
def connect_mqtt(self):
"""Подключение к MQTT-брокеру."""
self.mqtt_client = mqtt.Client(
client_id="meshtastic_gateway",
clean_session=True
)
self.mqtt_client.on_connect = self._on_mqtt_connect
self.mqtt_client.on_message = self._on_mqtt_message
if self.mqtt_user:
self.mqtt_client.username_pw_set(
self.mqtt_user, self.mqtt_pass
)
try:
self.mqtt_client.connect(
self.mqtt_host, self.mqtt_port, 60
)
self.mqtt_client.loop_start()
return True
except Exception as e:
logger.error(f"❌ MQTT ошибка: {e}")
return False
def _on_mqtt_connect(self, client, userdata, flags, rc):
"""Обработчик подключения MQTT."""
logger.info(f"✅ MQTT подключён (rc={rc})")
# Подписка на топики для управления mesh
client.subscribe(f"{self.mqtt_base_topic}/send/#")
client.subscribe(f"{self.mqtt_base_topic}/config/#")
client.subscribe(f"{self.mqtt_base_topic}/cmd/#")
# Публикация Home Assistant discovery
self._publish_ha_discovery()
def _on_mqtt_message(self, client, userdata, msg):
"""Обработчик входящих MQTT-сообщений."""
topic = msg.topic
payload = msg.payload.decode('utf-8')
logger.info(f"📥 MQTT: {topic} = {payload}")
if topic.startswith(f"{self.mqtt_base_topic}/send/"):
# Отправка сообщения в mesh
dest = topic.split("/")[-1]
if dest == "broadcast":
dest = "^all"
self.interface.sendText(
text=payload,
destinationId=dest,
wantAck=True
)
self.stats["messages_tx"] += 1
logger.info(f"📤 Отправлено в mesh: {payload}")
elif topic.startswith(f"{self.mqtt_base_topic}/cmd/"):
# Выполнение команды
cmd = topic.split("/")[-1]
if cmd == "reboot":
self.interface.sendPacket(
bytes([0x04]), "^all"
)
elif cmd == "stats":
self._publish_stats()
def on_meshtastic_receive(self, packet, interface):
"""Обработчик входящих Meshtastic-пакетов."""
decoded = packet.get('decoded', {})
portnum = decoded.get('portnum', '')
from_id = packet.get('fromId', 'unknown')
rx_time = packet.get('rxTime', 0)
rssi = packet.get('rxRssi', 0)
snr = packet.get('rxSnr', 0.0)
if portnum == 'TEXT_MESSAGE_APP':
text = decoded.get('payload', b'').decode('utf-8')
self.stats["messages_rx"] += 1
# Публикация в MQTT
self._mqtt_publish(
f"{self.mqtt_base_topic}/message/{from_id}",
json.dumps({
"text": text,
"from": from_id,
"rssi": rssi,
"snr": snr,
"timestamp": rx_time,
"datetime": datetime.fromtimestamp(
rx_time
).isoformat() if rx_time else None
})
)
logger.info(f"💬 {from_id}: {text}")
elif portnum == 'TELEMETRY_APP':
self.stats["telemetry_rx"] += 1
telemetry = decoded.get('payload', {})
# Публикация телеметрии
self._mqtt_publish(
f"{self.mqtt_base_topic}/telemetry/{from_id}",
json.dumps({
"from": from_id,
"data": telemetry,
"rssi": rssi,
"snr": snr,
"timestamp": rx_time
})
)
elif portnum == 'POSITION_APP':
self.stats["position_rx"] += 1
position = decoded.get('payload', {})
self._mqtt_publish(
f"{self.mqtt_base_topic}/position/{from_id}",
json.dumps({
"from": from_id,
"position": position,
"rssi": rssi,
"snr": snr,
"timestamp": rx_time
})
)
def _mqtt_publish(self, topic, payload, retain=True):
"""Публикация в MQTT."""
if self.mqtt_client:
self.mqtt_client.publish(
topic, payload, qos=1, retain=retain
)
def _publish_ha_discovery(self):
"""Публикация Home Assistant MQTT Discovery."""
nodes = self.interface.nodes
for node_id, node in nodes.items():
user = node.get('user', {})
name = user.get('longName', node_id)
# Battery sensor
config = {
"name": f"{name} Battery",
"state_topic": (
f"{self.mqtt_base_topic}/telemetry/{node_id}"
),
"value_template": (
"{{ value_json.data.device_metrics."
"battery_level | default(0) }}"
),
"unit_of_measurement": "%",
"device_class": "battery",
"unique_id": f"meshtastic_{node_id}_battery"
}
self._mqtt_publish(
f"homeassistant/sensor/meshtastic/"
f"{node_id}_battery/config",
json.dumps(config)
)
def _publish_stats(self):
"""Публикация статистики шлюза."""
uptime = time.time() - self.stats["start_time"]
self._mqtt_publish(
f"{self.mqtt_base_topic}/gateway/stats",
json.dumps({
**self.stats,
"uptime": uptime,
"nodes": len(self.interface.nodes)
})
)
def start(self):
"""Запуск шлюза."""
logger.info("🚀 Запуск Meshtastic MQTT Gateway...")
if not self.connect_meshtastic():
return False
if not self.connect_mqtt():
return False
# Установка обработчика Meshtastic
self.interface.receiveCallback = (
self.on_meshtastic_receive
)
logger.info("✅ Шлюз запущен и работает!")
# Периодическая публикация статистики
try:
while True:
self._publish_stats()
time.sleep(60)
except KeyboardInterrupt:
logger.info("👋 Остановка шлюза...")
self.mqtt_client.loop_stop()
self.interface.close()
# === Запуск ===
if __name__ == "__main__":
gateway = MeshtasticMQTTGateway(
serial_port="/dev/ttyUSB0",
mqtt_host="192.168.1.50",
mqtt_port=1883,
mqtt_user="mesh",
mqtt_pass="gateway"
)
gateway.start()
Оборудование
Полный гид по выбору оборудования для mesh-сетей. От бюджетных решений до профессиональных устройств.
Оборудование для Meshtastic
Heltec V3
Самое популярное устройство для Meshtastic. Встроенный OLED-дисплей, LoRa SX1262, ESP32-S3.
TTGO T-Beam
Со встроенным GPS-модулем NEO-6M. Идеален для трекеров. LoRa SX1276, ESP32.
RAK Wireless WisBlock
Модульная система на nRF52840. Очень низкое энергопотребление. Профессиональное качество.
Station G2
Готовое устройство в корпусе. Встроенная батарея, GPS, OLED. Plug-and-play решение.
DIY: ESP32 + SX1276
Самый бюджетный вариант. ESP32 DevKit + LoRa модуль SX1276 на макетной плате.
nRF52840 Dongle
Компактный USB-донгл для Meshtastic. Подключается к телефону или ПК через USB.
Гид по антеннам
Антенна — критически важный компонент. Правильная антенна может увеличить дальность в 5-10 раз.
| Тип антенны | Усиление | Дальность | Цена | Применение |
|---|---|---|---|---|
| Штыревая (stock) | 2 dBi | 1-3 км | $1-3 | Базовое, в комплекте |
| Штыревая 5 dBi | 5 dBi | 3-8 км | $3-8 | Улучшенная городская |
| Штыревая 8 dBi | 8 dBi | 5-15 км | $5-12 | Открытая местность |
| Яги (направленная) | 10-14 dBi | 15-30+ км | $15-40 | Стационарные линки |
| Коллинеарная | 6-9 dBi | 8-20 км | $10-25 | Базовые станции |
| Патч (патч) | 6-8 dBi | 5-12 км | $8-20 | Направленная, компактная |
Важно: Убедитесь, что антенна соответствует частоте вашего региона! Антенна 915 МГц будет плохо работать на 868 МГц и наоборот. Также проверяйте разъём: большинство модулей используют IPEX/U.FL.
Оборудование для MeshCore
ESP32 DevKit V1
Стандартная плата разработки ESP32. WiFi + Bluetooth, 240 МГц, 520 КБ SRAM.
ESP32-S3 DevKit
Улучшенная версия с AI-инструкциями, больше GPIO, USB OTG. Рекомендуется для MeshCore.
Оборудование для Reticulum
RNode (LoRa)
Открытый LoRa-модем для Reticulum. Подключается через USB. Поддерживает все частоты.
Raspberry Pi
Идеальная платформа для Reticulum-узла. Работает 24/7, поддерживает все интерфейсы.
Любой Linux-ПК
Reticulum работает на любом устройстве с Python: ноутбуки, серверы, VM, контейнеры.
Протоколы
Глубокое погружение в протоколы mesh-сетей. Как работают маршрутизация, шифрование и обнаружение узлов.
Алгоритмы маршрутизации
Controlled Flooding (Meshtastic)
Controlled Flooding — простейший, но эффективный алгоритм маршрутизации. Каждый узел, получивший новый пакет, ретранслирует его всем соседям. Для предотвращения бесконечных петель используется:
- Hop Limit (TTL) — счётчик прыжков, уменьшается на 1 при каждой ретрансляции. При достижении 0 пакет отбрасывается.
- Packet ID Cache — каждый узел хранит ID недавно полученных пакетов. Дубликаты отбрасываются.
- Random Delay — случайная задержка перед ретрансляцией для предотвращения коллизий.
- SNR-based filtering — узел может решить не ретранслировать, если сигнал слишком слабый.
import time
import random
from collections import OrderedDict
class FloodingRouter:
"""
Реализация алгоритма Controlled Flooding.
Используется в Meshtastic для маршрутизации пакетов.
"""
def __init__(self, node_id, max_hop_limit=3,
cache_size=100, cache_ttl=300):
self.node_id = node_id
self.max_hop_limit = max_hop_limit
self.cache_size = cache_size
self.cache_ttl = cache_ttl # секунды
# Кэш полученных пакетов (ID -> timestamp)
self.seen_packets = OrderedDict()
# Таблица соседей
self.neighbors = {}
# Статистика
self.stats = {
"rx": 0,
"tx": 0,
"relayed": 0,
"dropped": 0,
"duplicates": 0
}
def receive_packet(self, packet):
"""
Обработка входящего пакета.
Args:
packet: dict с полями:
- id: уникальный ID пакета
- from: ID отправителя
- to: ID получателя (0 = broadcast)
- hop_limit: оставшиеся прыжки
- hop_start: начальные прыжки
- payload: данные
- snr: SNR приёма
"""
self.stats["rx"] += 1
packet_id = packet["id"]
now = time.time()
# 1. Проверка дубликатов
if packet_id in self.seen_packets:
self.stats["duplicates"] += 1
return None # Дубликат, игнорируем
# 2. Регистрация пакета в кэше
self.seen_packets[packet_id] = now
self._cleanup_cache()
# 3. Обновление таблицы соседей
sender = packet["from"]
self.neighbors[sender] = {
"last_seen": now,
"snr": packet.get("snr", 0),
"hops": packet["hop_start"] - packet["hop_limit"]
}
# 4. Проверка — пакет для нас?
if packet["to"] == self.node_id or packet["to"] == 0:
# Пакет доставлен!
if packet["to"] == self.node_id:
return {"action": "deliver", "packet": packet}
# 5. Решение о ретрансляции
hop_limit = packet["hop_limit"]
if hop_limit <= 0:
self.stats["dropped"] += 1
return {"action": "drop", "reason": "hop_limit"}
# 6. SNR-based filtering
snr = packet.get("snr", 0)
if snr < -15:
self.stats["dropped"] += 1
return {"action": "drop", "reason": "low_snr"}
# 7. Ретрансляция с случайной задержкой
delay = random.uniform(0.1, 2.0)
relay_packet = dict(packet)
relay_packet["hop_limit"] = hop_limit - 1
self.stats["relayed"] += 1
self.stats["tx"] += 1
return {
"action": "relay",
"packet": relay_packet,
"delay": delay
}
def _cleanup_cache(self):
"""Очистка кэша от старых записей."""
now = time.time()
expired = [
pid for pid, ts in self.seen_packets.items()
if now - ts > self.cache_ttl
]
for pid in expired:
del self.seen_packets[pid]
# Ограничение размера кэша
while len(self.seen_packets) > self.cache_size:
self.seen_packets.popitem(last=False)
def create_packet(self, destination, payload, is_broadcast=False):
"""Создание нового пакета для отправки."""
return {
"id": random.randint(1, 2**32),
"from": self.node_id,
"to": 0 if is_broadcast else destination,
"hop_limit": self.max_hop_limit,
"hop_start": self.max_hop_limit,
"payload": payload,
"snr": 0
}
# === Пример использования ===
router = FloodingRouter(node_id=42, max_hop_limit=3)
# Создание и отправка broadcast-сообщения
msg = router.create_packet(
destination=0,
payload="Hello Mesh!",
is_broadcast=True
)
# Симуляция приёма пакета от другого узла
incoming = {
"id": 12345,
"from": 100,
"to": 0,
"hop_limit": 2,
"hop_start": 3,
"payload": "Relay test",
"snr": 5.5
}
result = router.receive_packet(incoming)
print(f"Результат: {result}")
print(f"Статистика: {router.stats}")
Tree Routing (MeshCore / ESP-MESH)
Tree Routing организует узлы в древовидную структуру с корневым узлом на вершине. Данные передаются вверх к корню и вниз к листьям. Каждый узел знает своего родителя и своих детей.
class TreeNode:
"""Узел дерева маршрутизации (ESP-MESH style)."""
def __init__(self, node_id):
self.node_id = node_id
self.parent = None
self.children = []
self.layer = 0
self.routing_table = {}
def set_parent(self, parent):
self.parent = parent
self.layer = parent.layer + 1
parent.children.append(self)
def route_to(self, dest_id, payload):
"""Маршрутизация пакета к целевому узлу."""
if self.node_id == dest_id:
return [f"DELIVERED to {dest_id}"]
# Проверяем детей
for child in self.children:
if child.node_id == dest_id or \
child.has_descendant(dest_id):
path = [f"{self.node_id} → {child.node_id}"]
path.extend(child.route_to(dest_id, payload))
return path
# Отправляем родителю
if self.parent:
path = [f"{self.node_id} → {self.parent.node_id} (up)"]
path.extend(self.parent.route_to(dest_id, payload))
return path
return ["NO ROUTE"]
def has_descendant(self, node_id):
"""Проверка, есть ли узел среди потомков."""
for child in self.children:
if child.node_id == node_id:
return True
if child.has_descendant(node_id):
return True
return False
# Построение дерева
root = TreeNode("ROOT")
n1 = TreeNode("Node-1"); n1.set_parent(root)
n2 = TreeNode("Node-2"); n2.set_parent(root)
n3 = TreeNode("Node-3"); n3.set_parent(n1)
n4 = TreeNode("Node-4"); n4.set_parent(n1)
n5 = TreeNode("Node-5"); n5.set_parent(n2)
n6 = TreeNode("Node-6"); n6.set_parent(n3)
# Маршрутизация
path = n6.route_to("Node-5", "Hello!")
print("Маршрут Node-6 → Node-5:")
for step in path:
print(f" {step}")
# Node-6 → Node-3 (up)
# Node-3 → Node-1 (up)
# Node-1 → ROOT (up)
# ROOT → Node-2
# Node-2 → Node-5
# DELIVERED to Node-5
Table-Based Routing
Каждый узел поддерживает таблицу маршрутизации с информацией о том, через какой соседний узел можно добраться до каждого известного узла в сети. Таблицы обновляются через периодический обмен маршрутной информацией.
class RoutingTable:
"""Таблица маршрутизации для mesh-узла."""
def __init__(self, node_id):
self.node_id = node_id
self.routes = {} # dest -> (next_hop, cost, timestamp)
self.neighbors = {} # neighbor -> (link_quality, timestamp)
def add_route(self, dest, next_hop, cost):
"""Добавление/обновление маршрута."""
import time
if dest not in self.routes or \
cost < self.routes[dest][1]:
self.routes[dest] = (next_hop, cost, time.time())
return True
return False
def get_route(self, dest):
"""Получение маршрута к узлу."""
if dest in self.routes:
return self.routes[dest]
return None
def update_from_neighbor(self, neighbor_id, their_table):
"""Обновление таблицы от соседа (Distance Vector)."""
updated = 0
link_cost = self.neighbors.get(neighbor_id, (1, 0))[0]
for dest, (nh, cost, ts) in their_table.items():
if dest == self.node_id:
continue
new_cost = cost + link_cost
if self.add_route(dest, neighbor_id, new_cost):
updated += 1
return updated
def __str__(self):
lines = [f"Routing Table for {self.node_id}:"]
lines.append(f" {'Dest':10} {'Next Hop':10} {'Cost':6}")
for dest, (nh, cost, ts) in self.routes.items():
lines.append(f" {dest:10} {nh:10} {cost:6}")
return "\n".join(lines)
Announce-Based Routing (Reticulum)
Reticulum использует уникальный подход: узлы периодически отправляют announce-пакеты, которые распространяются по сети. Каждый transport-узел, получив announce, запоминает путь к отправителю. Когда нужно отправить сообщение, узел использует запомненный маршрут.
import time
import hashlib
class AnnounceRouter:
"""
Announce-based маршрутизация (стиль Reticulum).
Принцип:
1. Узлы отправляют announce с своим публичным ключом
2. Transport-узлы запоминают путь к announce
3. Для отправки — используем запомненный путь
4. Путь обновляется при каждом новом announce
"""
def __init__(self, node_id, is_transport=False):
self.node_id = node_id
self.is_transport = is_transport
# Таблица announce: dest_hash -> (next_hop, hops, timestamp, data)
self.announce_table = {}
# Соседи
self.neighbors = set()
# Счётчик announce
self.announce_seq = 0
def create_announce(self, app_data=b""):
"""Создание announce-пакета."""
self.announce_seq += 1
return {
"type": "announce",
"origin": self.node_id,
"seq": self.announce_seq,
"hops": 0,
"app_data": app_data,
"timestamp": time.time()
}
def receive_announce(self, announce, from_neighbor):
"""
Обработка announce-пакета.
Returns:
True если announce нужно ретранслировать
"""
origin = announce["origin"]
seq = announce["seq"]
hops = announce["hops"]
# Не обрабатываем собственные announce
if origin == self.node_id:
return False
# Проверяем, видели ли мы уже этот announce
key = f"{origin}:{seq}"
if key in self.announce_table:
existing = self.announce_table[key]
if existing[1] <= hops:
return False # Уже видели более короткий путь
# Сохраняем маршрут
self.announce_table[key] = (
from_neighbor, # next_hop
hops, # hop count
time.time(), # timestamp
announce.get("app_data", b"")
)
# Обновляем лучший маршрут к origin
best_key = self._get_best_announce(origin)
# Ретрансляция (только transport-узлы)
if self.is_transport:
announce["hops"] = hops + 1
return True
return False
def get_route_to(self, dest_id):
"""Получение лучшего маршрута к узлу."""
best = self._get_best_announce(dest_id)
if best and best in self.announce_table:
entry = self.announce_table[best]
return {
"next_hop": entry[0],
"hops": entry[1],
"age": time.time() - entry[2]
}
return None
def _get_best_announce(self, origin):
"""Поиск лучшего (самого свежего и короткого) announce."""
best_key = None
best_hops = float('inf')
best_time = 0
for key, (nh, hops, ts, data) in self.announce_table.items():
if key.startswith(f"{origin}:"):
if hops < best_hops or \
(hops == best_hops and ts > best_time):
best_key = key
best_hops = hops
best_time = ts
return best_key
# === Пример ===
# Сеть: A -- T1 -- T2 -- B
# где T1, T2 — transport-узлы
node_a = AnnounceRouter("Alice")
node_t1 = AnnounceRouter("Transport-1", is_transport=True)
node_t2 = AnnounceRouter("Transport-2", is_transport=True)
node_b = AnnounceRouter("Bob")
# Alice отправляет announce
ann = node_a.create_announce(b"Alice's Chat Node")
# Распространение announce по сети
if node_t1.receive_announce(ann, "Alice"):
ann["hops"] = 1
if node_t2.receive_announce(ann, "Transport-1"):
ann["hops"] = 2
node_b.receive_announce(ann, "Transport-2")
# Bob теперь знает маршрут к Alice
route = node_b.get_route_to("Alice")
print(f"Bob → Alice: {route}")
# {'next_hop': 'Transport-2', 'hops': 2, 'age': 0.001}
Шифрование в Mesh-сетях
Meshtastic: PSK
Pre-Shared Key (AES-256). Все узлы на канале используют общий ключ. Простота настройки, но компрометация одного узла = компрометация канала.
MeshCore: WPA2
Стандартное WiFi-шифрование WPA2. Защищает канал между узлами, но не обеспечивает сквозное шифрование на уровне приложения.
Reticulum: E2E
Сквозное шифрование: X25519 (обмен ключами) + Ed25519 (подписи) + AES-256 (данные). Каждый линк уникально зашифрован. Максимальная безопасность.
Топологии сетей
Визуализация и описание сетевых топологий, используемых в mesh-сетях.
Типы топологий
Звезда (Star)
Все узлы подключены к центральному хабу. Простая, но уязвимая — отказ хаба = отказ всей сети. Не является mesh.
Единая точка отказа
Дерево (Tree)
Иерархическая структура с корневым узлом. Используется в MeshCore/ESP-MESH. Данные идут вверх к корню и вниз к листьям.
Отказ родительского узла изолирует ветку
Полная Mesh (Full Mesh)
Каждый узел связан с каждым. Максимальная надёжность и избыточность. Но требует O(n²) соединений — непрактично для больших сетей.
Максимальная отказоустойчивость
Частичная Mesh (Partial Mesh)
Узлы связаны с несколькими соседями. Баланс между надёжностью и сложностью. Наиболее распространённая топология в реальных mesh-сетях.
Оптимальный баланс для Meshtastic и Reticulum
Интерактивная визуализация
Анимированная mesh-сеть с узлами и соединениями.
Интерактив: Анимация выше показывает mesh-сеть с пульсирующими узлами и активными соединениями. Зелёные линии — активные линки, жёлтые точки — передаваемые пакеты.
Часто задаваемые вопросы
Ответы на самые популярные вопросы о mesh-сетях, Meshtastic, MeshCore и Reticulum.
Зависит от ваших задач:
- Meshtastic — для дальней связи (км), GPS-трекинга, простоты. Идеален для походов и ЧС.
- MeshCore — для высокой скорости (Мбит/с) на средних расстояниях. Идеален для IoT и локальных сетей.
- Reticulum — для максимальной гибкости и безопасности. Комбинирует любые интерфейсы, поддерживает LXMF.
Нет «лучшего» — есть «лучший для вашей задачи».
Реальная дальность зависит от множества факторов:
| Условия | Дальность |
|---|---|
| Город, застройка | 1-3 км |
| Пригород, мало зданий | 3-8 км |
| Открытая местность | 8-15 км |
| Прямая видимость (холмы) | 15-30+ км |
| Рекорд (с направленными антеннами) | 200+ км |
Ключевые факторы: высота антенны, мощность, SF, антенна, помехи.
Уровень безопасности различается:
- Meshtastic: AES-256 с общим ключом. Достаточно для большинства задач, но все узлы на канале могут читать сообщения.
- MeshCore: WPA2 на уровне WiFi. Нет сквозного шифрования на уровне приложения.
- Reticulum: Сквозное шифрование X25519 + AES-256. Каждый линк уникально зашифрован. Максимальная безопасность.
Помните: радиосигнал можно перехватить. Всегда используйте шифрование для конфиденциальных данных.
Минимальная стоимость для 2 узлов:
- Meshtastic: от $20-30 за узел (Heltec V3). Итого ~$40-60 для сети из 2 узлов.
- MeshCore: от $5-8 за узел (ESP32 DevKit). Итого ~$10-16.
- Reticulum: от $0 (если есть ПК) до $50+ (с RNode LoRa).
Это одни из самых доступных сетевых технологий в мире!
Да! Reticulum может выступать как «клей» между разными технологиями:
- Reticulum + LoRa (RNode) = дальняя связь, совместимая с концепцией Meshtastic
- Reticulum + WiFi = высокоскоростная локальная сеть (аналог MeshCore)
- Reticulum может одновременно использовать LoRa, WiFi, Serial, Ethernet, I2P
Однако Meshtastic и MeshCore напрямую не совместимы — они используют разные протоколы и частоты.
- Meshtastic: Отличные приложения для iOS и Android. Поддержка чата, карты, GPS-трекинга.
- MeshCore: Нет официального мобильного приложения. Требуется разработка собственного клиента.
- Reticulum: Sideband для Android. Nomad Network — mesh-браузер для Linux.
| Технология | Передача | Приём | Сон | Автономность |
|---|---|---|---|---|
| Meshtastic (LoRa) | ~120 мА | ~12 мА | ~0.01 мА | Недели/месяцы |
| MeshCore (WiFi) | ~200 мА | ~80 мА | ~1 мА | Часы/дни |
| Reticulum (LoRa) | ~120 мА | ~12 мА | — | Зависит от IF |
| Reticulum (WiFi) | ~200 мА | ~80 мА | — | Зависит от IF |
Пошаговый план:
- Планирование: Определите цели, покрытие, количество участников.
- Выбор технологии: Meshtastic для дальности, MeshCore для скорости, Reticulum для гибкости.
- Закупка оборудования: Минимум 3-5 узлов для начала. Включая ретрансляторы на возвышенностях.
- Установка ретрансляторов: Разместите router-узлы на крышах, холмах, мачтах.
- Настройка каналов: Единый канал с общим PSK для всех участников.
- Тестирование: Проверьте покрытие, дальность, качество связи.
- Обучение: Обучите участников пользоваться приложениями.
- Мониторинг: Используйте MQTT + Grafana для мониторинга сети.
Глоссарий
Словарь терминов и определений для mesh-сетей.
Контакты и ресурсы
Полезные ссылки, сообщества и документация.