Routing Meshtastic od strony firmware – LoRa, UDP, role i strojenie

Celem tego tekstu jest możliwie dokładne rozpisanie, jak firmware Meshtastic rozumie routing: jak wygląda przepływ pakietu w kodzie, jak poszczególne role urządzeń wpływają na zachowanie, i które parametry konfiguracji pozwalają realnie sterować ruchem w sieci.

Źródła:

  • kod z gałęzi develop w repozytorium: https://github.com/meshtastic/firmware/tree/develop (PacketHistory, Router, FloodingRouter, NextHopRouter, ReliableRouter, RadioInterface, Default)
  • dokument „Mesh Broadcast Algorithm”: https://meshtastic.org/docs/overview/mesh-algo/
  • praktyczne obserwacje i prezentacje o sieciach Meshtastic (np. „[#603] Warszawska sieć Meshtastic pod lupą”: https://www.youtube.com/watch?v=xCZr5DtL8ik)

W kolejnych sekcjach najpierw przejdziemy przez stos klas routingu w firmware, potem pokażę przykładowe ramki LoRa i UDP oraz to, jak firmware je traktuje. Na końcu przejdziemy przez role urządzenia i pokażę, jak konkretnymi parametrami (role, rebroadcast_mode, hop_limit, listy ulubionych, ignorowani sąsiedzi) można realnie wpływać na routing tak, żeby sieć – szczególnie miejska – działała stabilnie zamiast się zapychać. Dodatkowo omówimy ewolucję routingu w nowszych wersjach firmware, aspekty bezpieczeństwa oraz integracje z innymi systemami, wraz z praktycznymi przykładami rozwiązywania problemów.

  1. Stos klas routingu w mesh/
    Routing w firmware jest zrobiony jako zestaw nakładających się klas. W „normalnym” buildzie pracuje obiekt ReliableRouter, który dziedziczy po NextHopRouter, ten po FloodingRouter, ten z kolei po Router, a Router po PacketHistory.
    Uproszczony fragment nagłówków (PacketHistory.h, Router.h, FloodingRouter.h, NextHopRouter.h, ReliableRouter.h):
class PacketHistory {  
public:  
    bool wasSeenRecently(const meshtastic_MeshPacket *p,  
                         bool withUpdate,  
                         bool *wasFallback,  
                         bool *weWereNextHop,  
                         bool *wasUpgraded);  
};  

class Router : public PacketHistory {  
public:  
    void enqueueReceivedMessage(meshtastic_MeshPacket *p);  
    int32_t runOnce();  
};  

class FloodingRouter : public Router {  
public:  
    ErrorCode send(meshtastic_MeshPacket *p) override;  
    bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;  
};  

class NextHopRouter : public FloodingRouter {  
public:  
    ErrorCode send(meshtastic_MeshPacket *p) override;  
    bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;  
};  

class ReliableRouter : public NextHopRouter {  
public:  
    ErrorCode send(meshtastic_MeshPacket *p) override;  
    bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;  
};  

Implementacja tych metod siedzi głównie w Router.cpp. Każdy pakiet przechodzi więc przez cały ten stos: najpierw pamięć (PacketHistory), potem ogólny router (kolejki i filtry), potem flooding, dalej next-hop, na końcu warstwa niezawodności (want_ack i retransmisje).

  1. Nagłówek radiowy i struktura MeshPacket
    Zanim pakiet trafi do logiki mesh, radio widzi surowy nagłówek opisany w „Mesh Broadcast Algorithm” (https://meshtastic.org/docs/overview/mesh-algo/). Na poziomie RF struktura wygląda mniej więcej tak:

Offset 0x00 -> 4 bajty: Destination (NodeID, 0xFFFFFFFF dla broadcast)
Offset 0x04 -> 4 bajty: Sender (NodeID)
Offset 0x08 -> 4 bajty: Packet ID (identyfikator pakietu nadany przez nadawcę)
Offset 0x0C -> 1 bajt: Flags (HopLimit, WantAck, ViaMQTT, HopStart w jednym bajcie)
Offset 0x0D -> 1 bajt: Channel hash
Offset 0x0E -> 1 bajt: Next-hop (low-byte NodeNum)
Offset 0x0F -> 1 bajt: Relay-node (low-byte NodeNum aktualnego przekaźnika)
Offset 0x10 -> …: zaszyfrowany SubPacket (protobuf)

Flagowy bajt rozpada się na bity:

  • bity 0..2 -> HopLimit
  • bit 3 -> WantAck
  • bit 4 -> ViaMQTT
  • bity 5..7 -> HopStart

Po zdekodowaniu i rozszyfrowaniu ten nagłówek trafia do struktury meshtastic_MeshPacket (MeshTypes.h, wygenerowane pliki protobuf w mesh/generated/). Najważniejsze pola z punktu widzenia routingu:

  • from -> pełny NodeNum nadawcy
  • to -> NodeNum odbiorcy albo broadcast
  • id -> identyfikator pakietu nadany przez from
  • hop_limit -> ile skoków zostało
  • hop_start -> ile skoków było na starcie
  • relay_node -> low-byte ostatniego przekaźnika
  • next_hop -> low-byte preferowanego kolejnego hopa przy unicast
  • transport_mechanism -> czy pakiet wszedł LoRa, UDP, MQTT
  • want_ack -> czy wymagamy potwierdzenia
  • decoded.portnum -> aplikacyjny port (tekst, pozycja, routing, telemetry itd.).

Firmware routingu pracuje wyłącznie na tych polach i na stanie lokalnych struktur (PacketHistory, NodeDB, Config). Nie ma żadnej „magii” poza tym, co widać w tych wartościach.

  1. PacketHistory – pamięć pakietów i duplikaty
    PacketHistory (PacketHistory.h, PacketHistory.cpp) jest bazową klasą dla Router. Trzyma tablicę PacketRecord, indeksowaną po (sender, id) – czyli (from, id) z MeshPacket.
    Fragment PacketHistory.h:
struct PacketRecord {  
    uint32_t sender;  
    uint32_t id;  
    uint8_t  relayedBy[MAX_RELAYERS];  
    uint8_t  highestHopLimit;  
    uint8_t  ourTxHopLimit;  
    uint32_t rxTimeMsec;  
};  

class PacketHistory {  
public:  
    bool wasSeenRecently(const meshtastic_MeshPacket *p,  
                         bool withUpdate,  
                         bool *wasFallback,  
                         bool *weWereNextHop,  
                         bool *wasUpgraded);  
};  

Zachowanie wasSeenRecently(p, withUpdate, …) jest kluczowe:

  • jeśli nie ma rekordu (sender=p->from, id=p->id) -> tworzy nowy, highestHopLimit=p->hop_limit,   wpisuje relay_node do relayedBy, zwraca false (pakiet nowy),
  • jeśli rekord istnieje -> aktualizuje relayedBy i highestHopLimit, ustawia flagi wasFallback,   weWereNextHop, wasUpgraded i zwraca true (duplikat).

Przykład sztucznego pakietu broadcast:

from = 0xAE614B4C
to = 0xFFFFFFFF
id = 0x1001
hop_limit = 3, hop_start = 3
relay_node = 0x4C
transport_mechanism = TRANSPORT_LORA

  • pierwszy raz, gdy węzeł go słyszy -> wasSeenRecently zwraca false, tworzy nowy rekord,
  • drugi raz, jeśli inny węzeł retransmituje ten sam pakiet z relay_node = 0xBC, hop_limit = 2 ->   wasSeenRecently zwraca true (duplikat), dopisuje 0xBC do relayedBy.

Na tej informacji operują wyższe warstwy: FloodingRouter decyduje, czy skasować własną retransmisję, NextHopRouter rozumie fallback/upgrade next_hop, a ReliableRouter może rozpoznać implicit ACK, gdy słyszy własny pakiet retransmitowany przez sieć.

  1. Router – kolejka, wątek i wejście LoRa/UDP
    Router (Router.h, Router.cpp) dodaje kolejki i wątek. RadioInterface (RadioInterface.cpp) po odebraniu ramki LoRa lub pakietu UDP dekoduje ją do meshtastic_MeshPacket i woła:
router->enqueueReceivedMessage(p);

Fragment Router.cpp:

int32_t Router::runOnce() {  
    meshtastic_MeshPacket *mp;  
    while ((mp = fromRadioQueue.dequeuePtr(0)) != NULL) {  
        perhapsHandleReceived(mp);  
    }  
    return INT32_MAX;  
}  

W perhapsHandleReceived(mp) Router robi twarde filtry:

  • sprawdza, czy nadawca nie jest na liście ignorowanych (config.lora.ignore_incoming w konfiguracji),
  • odrzuca pakiety z błędnym polem from (broadcast itp.),
  • przy filtrowaniu via_mqtt używa config.lora.ignore_mqtt,
  • dopiero potem wywołuje wirtualne shouldFilterReceived(p), które nadpisują FloodingRouter, NextHopRouter   i ReliableRouter.

Z punktu widzenia Routera nie ma znaczenia, czy pakiet przyszedł LoRa czy UDP – oba transporty kończą na fromRadioQueue i są obsługiwane tym samym kodem. Różnica pojawia się dopiero przy wysyłaniu (transport_mechanism i konfiguracja interfejsów).

  1. FloodingRouter – managed flooding i rebroadcast_mode
    FloodingRouter (FloodingRouter.h, implementacja w Router.cpp) dodaje klasyczny flooding pakietów broadcast z logiką „managed flooding” opisaną w dokumentacji:
    https://meshtastic.org/docs/overview/mesh-algo/#broadcasts-using-managed-flooding

Kluczowe zasady z dokumentu i z kodu:

  • jeśli to == BROADCAST i id == 0 -> prosty broadcast, bez floodingu (0 hops),
  • jeśli to == BROADCAST i id != 0 -> pełny flooding, pakiet może być retransmitowany przez wiele węzłów,
  • węzeł, który ma prawo retransmitować (isRebroadcaster w FloodingRouter.h), losuje opóźnienie zależne   od SNR (RadioInterface::getTxDelayMsecWeighted),
  • im niższy SNR (dalej od nadajnika), tym mniejsze okno losowania -> węzeł „daleki” nadaje wcześniej,
  • jeśli node usłyszy duplikat tego samego pakietu zanim przyjdzie jego kolej -> może anulować swoją retransmisję (FloodingRouter::perhapsCancelDupe).

Na to wszystko nakłada się rebroadcast_mode z config.device.rebroadcast_mode (meshtastic_Config_DeviceConfig_RebroadcastMode_*), który jest sprawdzany m.in. w funkcji perhapsDecode w Router.cpp. Przykład z kodu:

if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_KNOWN_ONLY &&  
    (nodeDB->getMeshNode(p->from) == NULL || !nodeDB->getMeshNode(p->from)->has_user)) {  
    LOG_DEBUG("Node 0x%x not in nodeDB -> KNOWN_ONLY will ignore packet", p->from);  
    return DecodeState::DECODE_FAILURE;  
}  

W praktyce:

  • rebroadcast_mode=ALL -> node może floodować wszystko, co spełni warunki SNR/hop_limit,
  • rebroadcast_mode=KNOWN_ONLY -> node ignoruje ruch od nieznanych węzłów (nie powiększa floodingu),
  • inne tryby („LOCAL_ONLY” itp. w nowszych buildach) jeszcze mocniej ograniczają floodowanie.

To jest jedno z głównych pokręteł, którymi można ograniczyć „powódź” pakietów w gęstej sieci miejskiej – szczególnie przy dużej liczbie nodów w jednej lokalizacji.

  1. RadioInterface – SNR, RSSI i opóźnienia floodingu
    Fizyczne opóźnienie przed retransmisją floodingu jest liczone w RadioInterface.cpp. Kluczowe są funkcje shouldRebroadcastEarlyLikeRouter i getTxDelayMsecWeighted.
    Fragment RadioInterface.cpp:
bool RadioInterface::shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p) {  
    if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER)  
        return true;  
    if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)  
        return nodeDB->isFromOrToFavoritedNode(*p);  
    return false;  
}  

uint32_t RadioInterface::getTxDelayMsecWeighted(meshtastic_MeshPacket *p) {  
    float snr = p->rx_snr;  
    uint8_t CWsize = getCWsize(snr);  
    if (shouldRebroadcastEarlyLikeRouter(p)) {  
        return random(0, 2 * CWsize) * slotTimeMsec;  
    } else {  
        return (2 * CWmax * slotTimeMsec) + random(0, pow_of_2(CWsize)) * slotTimeMsec;  
    }  
}  

Interpretacja:

  • im niższy SNR (node „dalej”), tym mniejszy CWsize -> krótsze opóźnienie,
  • ROUTER zawsze ma krótsze okno (router „odpala” wcześniej),
  • CLIENT_BASE ma krótkie okno tylko dla ruchu od/do ulubionych węzłów (NodeDB::isFromOrToFavoritedNode),
  • zwykli klienci bez specjalnych ról mają dłuższe opóźnienie i zazwyczaj zdążą usłyszeć ruch z routerów.

To tłumaczy, dlaczego w topologii „kilka routerów na dachach, reszta klienci” flooding układa się sensownie. W mieście, gdy zrobimy 10 routerów w jednym bloku, wszystkie mają krótkie okna, ich ramki się nakładają i sieć jest zagłuszana niepotrzebnymi retransmisjami.

  1. NextHopRouter – direct messages, next_hop i hop_limit
    NextHopRouter (NextHopRouter.h, implementacja w Router.cpp) zmienia sposób traktowania direct messages (pakietów z konkretnym polem to). Dokumentacja: https://meshtastic.org/docs/overview/mesh-algo/#direct-messages-using-next-hop-routing

Idea jest taka:

  • pierwsza wiadomość direct do danego węzła najczęściej idzie jak broadcast (managed flooding),
  • w PacketHistory zbieramy listę relay_node dla danego (from, id),
  • gdy wraca odpowiedź (ACK/DM zwrotny) i przechodzi przez node, który wcześniej relayował naszą wiadomość,   traktujemy go jako kandydata na next-hopa,
  • NodeDB aktualizuje skojarzenie „do tego węzła -> użyj tego next-hopa”,
  • kolejne wiadomości idą już wąską ścieżką; dopiero przy ostatnich próbach retransmisji next_hop jest kasowany i następuje fallback do floodingu.

W kodzie NextHopRouter::send(p) można znaleźć schemat:

ErrorCode NextHopRouter::send(meshtastic_MeshPacket *p) {  
    p->relay_node = lastByte(getNodeNum());  
    wasSeenRecently(p, true, ...);  
    p->next_hop = getNextHop(p->to, p->relay_node);  
    return Router::send(p);  
}  

Hop_limit działa tutaj tak samo jak przy floodingu: każdy przekaźnik zmniejsza hop_limit, a gdy spadnie do zera, pakiet nie idzie dalej. Z punktu widzenia routingu:

  • CLIENT_BASE zwykle retransmituje tylko wtedy, gdy jest next-hopem albo przy fallback do floodingu,
  • ROUTER ma większą szansę zostać next-hopem na dłużej, bo często jest przekaźnik zarówno wysyłanej wiadomości,   jak i odpowiedzi,
  • SENSOR/TRACKER rzadko są używane jako stały next-hop, bo ich rola jest ustawiona pod raportowanie pozycji   i telemetrii (Default.cpp – inne traktowanie interwałów).
  1. ReliableRouter – want_ack, PendingPacket i implicit ACK
    ReliableRouter (ReliableRouter.h, implementacja w Router.cpp) opiera się bezpośrednio na warstwie opisanej w „Layer 2: Reliable Zero Hop Messaging” w Mesh Broadcast Algorithm.

Kluczowe elementy:

  • jeśli pakiet ma want_ack=1 -> ReliableRouter::send(p) tworzy wpis PendingPacket,   indeksowany po (from, id),
  • PendingPacket zawiera kopię pakietu, licznik retries i znacznik czasu ostatniego TX,
  • okresowo logika retransmisji przegląda PendingPacket i w razie braku ACK ponawia wysyłkę,   wykorzystując NextHopRouter/FloodingRouter do fizycznego transmitu,
  • gdy węzeł słyszy własny pakiet retransmitowany w sieci (from = nasz NodeNum) albo dostaje jawny ACK/NAK   na dedykowany port, PendingPacket jest kasowany (implicit lub explicit ACK) i retransmisje są zatrzymywane.

ReliableRouter nie zmienia samego wyboru ścieżki – tylko pilnuje, żeby ważne wiadomości nie ginęły bez śladu. Routing wciąż jest wykonywany przez FloodingRouter i NextHopRouter.

  1. UDP vs LoRa – ta sama logika, inny nośnik
    Od strony kodu nie ma osobnego „routing po UDP” i „routing po LoRa”. RadioInterface na wejściu UDP i LoRa robi to samo:
  • dekoduje payload do meshtastic_MeshPacket,
  • uzupełnia transport_mechanism (TRANSPORT_LORA lub TRANSPORT_UDP),
  • wywołuje router->enqueueReceivedMessage(p).

W Mesh Broadcast Algorithm (sekcja o UDP i MQTT) jest opisane, że różne mechanizmy transportowe mogą przenosić te same pakiety MeshPacket. PacketHistory dba o to, żeby duplikaty z różnych transportów były traktowane jako ten sam pakiet.

Przykładowa sytuacja:

  • node A wysyła broadcast tekstowy, generuje MeshPacket:
      from = 0xAE614B4C, to = 0xFFFFFFFF, id = 0x5000, hop_limit = 7, hop_start = 7,
  • ten sam MeshPacket jest wysyłany jednocześnie po LoRa (TRANSPORT_LORA) i po UDP multicast (TRANSPORT_UDP),
  • node B odbiera najpierw wersję LoRa -> PacketHistory rejestruje (0xAE614B4C, 0x5000),
  • po chwili B odbiera wersję UDP -> wasSeenRecently zwraca true (duplikat), więc drugi egzemplarz nie wywoła   dodatkowego floodingu.

W praktyce UDP zachowuje się jak „lokalne radio w LANie” – wykorzystuje tę samą logikę routingu, co LoRa. Różnica jest w tym, że UDP nie ma kolizji RF, więc bardzo łatwo wygenerować nim burzę pakietów, jeśli dopuścimy do tego, żeby wiele node’ów floodowało wszystko po tej samej sieci WiFi.

  1. Role urządzenia – ROUTER, ROUTER_LATE, CLIENT_BASE, SENSOR, TRACKER
    Firmware zna kilka ról urządzenia z enum meshtastic_Config_DeviceConfig_Role (wygenerowany protobuf, użycie w Default.cpp, RadioInterface.cpp, Router.cpp):
  • ROUTER
  • ROUTER_LATE
  • CLIENT_BASE
  • SENSOR
  • TRACKER

To nie są „profile UI”, tylko flagi, które kod sprawdza przy decyzjach routingu i interwałów. Reszta trybów, które widzisz w aplikacji (CLIENT, CLIENT_MUTE, różne warianty routerów) mapuje się na te role plus dodatkowe flagi (np. isClientMuted na kanale, rebroadcast_mode itp.). Poniżej streszczam, jak firmware traktuje każdą z ról.

ROUTER:

  • shouldRebroadcastEarlyLikeRouter -> zawsze zwraca true -> krótkie opóźnienie floodingu,
  • hop_limit jest traktowany ostrożniej: w Router.cpp jest logika, która przy kolejnym hopie sprawdza,   czy poprzedni relay był ulubionym routerem (node->is_favorite, node->user.role == ROUTER/ROUTER_LATE)   i jeśli tak, może nie zmniejszać hop_limit,
  • router jest typowo zawsze rebroadcasterem i zwykle nie anuluje duplikatów tak agresywnie jak klient.

ROUTER_LATE:

  • ma podobny status jak ROUTER w decyzjach typu „czy traktować to jako router”,
  • może mieć inne domyślne interwały i priorytety, ale od strony floodingu jest traktowany jak router   w kontekście hop_limit i roli węzła.

CLIENT_BASE:

  • shouldRebroadcastEarlyLikeRouter -> zwraca true, ale tylko dla pakietów „od lub do” ulubionych węzłów   (nodeDB->isFromOrToFavoritedNode),
  • dzięki temu CLIENT_BASE może zachowywać się jak mini-router dla wybranej grupy (np. domu), ale nie dla całej sieci,
  • jego udział w floodingu zależy mocno od rebroadcast_mode – przy KNOWN_ONLY i bez ulubionych praktycznie   niewiele flooduje.

SENSOR i TRACKER:

  • w Default.cpp są traktowane priorytetowo w kontekście wysyłania własnych raportów (pozycja, telemetria),
  • ich interwały są mniej skalowane przez congestionScalingCoefficient,
  • od strony floodingu często są ustawiane w taki sposób, żeby nie robiły zbyt wielu retransmisji   (rebroadcast_mode bardziej zachowawcze),
  • służą głównie jako źródła danych, a nie elementy szkieletu routingu.
  1. Jak praktycznie manipulować routingiem – parametry w konfiguracji
    Na bazie powyższego widać, że na routing można wpływać w kilku miejscach konfiguracji:
    1) config.device.role (ROUTER, ROUTER_LATE, CLIENT_BASE, SENSOR, TRACKER):
       • ROUTER/ROUTER_LATE -> krótkie opóźnienia floodingu, większa szansa zostania next-hopem,      bardziej „uparty” hop_limit,
       • CLIENT_BASE -> zachowuje się jak router głównie dla ulubionych węzłów,
       • SENSOR/TRACKER -> optymalizowane pod własne TX, mniejszy udział w floodingu.

2) config.device.rebroadcast_mode:
   • ALL -> node może retransmitować prawie wszystko,
   • KNOWN_ONLY -> node ignoruje ruch od nieznanych (nie dodaje ich do floodingu),
   • w nowszych wersjach: LOCAL_ONLY, REGION_ONLY itp. – jeszcze mocniej ograniczają zasięg floodingu.

3) nodeDB -> ulubione węzły (is_favorite):
   • wpływa na to, które node’y CLIENT_BASE traktuje jak routera,
   • wpływa na logikę „nie zmniejszaj hop_limit, jeśli poprzedni relay był ulubionym routerem”      (fragment z Router.cpp pokazany wcześniej).

4) config.lora.hop_limit:
   • im mniejszy hop_limit, tym mniej „obrączek” floodingu,
   • w gęstej miejskiej sieci często wystarczy 3–4 hops, większe wartości generują niepotrzebny ruch.

5) listy ignorowanych (config.lora.ignore_incoming, czasem listy w nodeDB):
   • pozwalają całkowicie odciąć „złe” węzły (np. źle skonfigurowane routery),
   • firmware po prostu nie wpuszcza ich pakietów do Router::perhapsHandleReceived.

  1. Przykładowe scenariusze – dlaczego lepiej mieć więcej klientów niż routerów
    Scenariusz 1: jedno osiedle, jeden router na dachu, dużo klientów.
  • Router na dachu: config.device.role=ROUTER, rebroadcast_mode=ALL lub KNOWN_ONLY, wysoka moc LoRa,
  • klienci w mieszkaniach: config.device.role=CLIENT_BASE, rebroadcast_mode=KNOWN_ONLY, ulubiony router   ustawiony jako favorite w nodeDB.

Efekt od strony kodu:

  • router ma krótkie opóźnienia floodingu i jest naturalnym przekaźnikiem,
  • klienci słyszą router z bardzo wysokim SNR, losują dłuższe okno i zazwyczaj rezygnują z własnych retransmisji,   bo wcześniej usłyszą ruch z routera,
  • bezpieczny hop_limit (np. 4) wystarczy, żeby pakiet dotarł dalej w miasto, nie duplikując się w nieskończoność.

Scenariusz 2: to samo osiedle, 10 routerów w jednym bloku.

  • 10 node’ów z config.device.role=ROUTER, rebroadcast_mode=ALL, UDP meshing włączone na wszystkich,
  • każdy ma podobny SNR względem pozostałych.

Efekt od strony kodu:

  • każdy z 10 node’ów dostaje krótkie opóźnienie floodingu, więc wiele z nich nada prawie w tym samym czasie,
  • PacketHistory widzi ogromne ilości duplikatów, ale dopiero po tym, jak radiowo już się „zderzą”,
  • UDP dodatkowo mnoży pakiety w LANie, bo nie ma kolizji RF – Router/PacketHistory widzą to jako lawinę   duplikatów z różnych transportów,
  • w efekcie airtime LoRa i przepustowość LAN są marnowane, a dalsze węzły dostają tylko część ruchu.

To jest dokładnie to, co widać w praktycznych prezentacjach – sieć zachowuje się poprawnie logicznie (routing działa zgodnie z kodem), ale topologia i role sprawiają, że zasób radiowy jest nieefektywnie używany.

  1. Podsumowanie
    Cały routing Meshtastica – zarówno po LoRa, jak i po UDP – odbywa się w tych samych klasach: PacketHistory, Router, FloodingRouter, NextHopRouter i ReliableRouter z katalogu mesh/. Decyzje o tym, kto retransmituje, kto milczy, kiedy zmniejszać hop_limit i kiedy preferować danego next-hopa opierają się na:
  • polach MeshPacket (from, to, id, hop_limit, hop_start, relay_node, next_hop, want_ack, transport_mechanism),
  • SNR i RSSI z RadioInterface (getTxDelayMsecWeighted, getCWsize),
  • roli urządzenia (DeviceConfig_Role w konfiguracji),
  • rebroadcast_mode i listach ulubionych/ignorowanych węzłów.

Jeżeli traktować to jak „silnik”, to konfiguracja roli (ROUTER, ROUTER_LATE, CLIENT_BASE, SENSOR, TRACKER), rebroadcast_mode, hop_limit i zarządzanie ulubionymi węzłami są pokrętłami, którymi naprawdę da się wpływać na routing. Kluczem do dobrej sieci – szczególnie miejskiej – jest mało routerów, za to dobrze ustawionych, oraz dużo klientów, którzy potrafią zrezygnować z przepuszczania pakietów, kiedy słyszą, że robi to już dedykowana infrastruktura.

  1. Aktualizacje firmware – ewolucja routingu w wersjach 2.6+
    Firmware Meshtastic ewoluuje, z kluczowymi zmianami w routingu wprowadzonymi w wersji 2.6 (wydanej w lutym 2025 r.). Wersja ta skupia się na optymalizacji next-hop routing dla direct messages (DM), co poprawia niezawodność w dynamicznych sieciach, np. z mobilnymi węzłami.

Kluczowe zmiany w 2.6:

  • Next-Hop Routing 2.0: Początkowe DM nadal używają managed flooding, ale system śledzi relay_node z odpowiedzi (np. ACK) i ustawia next_hop tylko dla pasujących węzłów. Fallback do pełnego floodingu następuje tylko na ostatniej retransmisji (np. po 3 próbach), co redukuje niepotrzebne retransmisje i zużycie baterii. To jest backwards compatible – mieszane wersje (np. 2.4 i 2.6) działają, ale pełna korzyść wymaga update’u wszystkich węzłów.
  • Optymalizacja LoRa slot-time: Nowa kalkulacja opóźnień floodingu (getTxDelayMsecWeighted) uwzględnia więcej czynników, jak congestion, co zmniejsza kolizje w gęstych sieciach.
  • Separacja plików: Device state i node data rozdzielone, co ułatwia zarządzanie i redukuje błędy w PacketHistory.
  • Inne: Wsparcie dla Meshtastic UI (MUI) – standalone interfejs bez appki, oraz UDP over LAN dla lepszego meshingu w lokalnych sieciach.

Wersje po 2.6 (np. 2.6.2 z lipca 2025) łatają luki bezpieczeństwa (patrz sekcja poniżej). Zalecany update via web flasher (https://flasher.meshtastic.org/) lub CLI. Starsze wersje (przed 2.6) mogą powodować niekompatybilności w routingu, np. brak fallbacku w DM.

  1. Bezpieczeństwo routingu i integracje z innymi systemami
    Bezpieczeństwo routingu w Meshtastic opiera się na szyfrowaniu (protobuf + AES) i mechanizmach jak want_ack/implicit ACK, które zapobiegają utracie pakietów. Jednak w 2025 r. ujawniono kilka CVE wpływających na routing:
  • CVE-2025-24798 (CVSS ~7.5, lipiec 2025): Pakiet z want_response==true w routing module powoduje crash, co umożliwia DoS (degradation of service) w zasięgu. Dotyczy wersji 1.2.1–2.6.1; załatane w 2.6.2.
  • CVE-2025-24797 (kwiecień 2025): Buffer overflow od malformed packets w routingu, potencjalnie umożliwiający code execution.
  • CVE-2025-52464 (CVSS 9.5, czerwiec 2025): Słaba entropia randomness pool na niektórych platformach umożliwia decryption wiadomości i remote node reconfiguration. Rekomendacja: Update do 2.6.3+ i regeneracja kluczy.

Aby poprawić bezpieczeństwo: Używaj TLS w MQTT, ogranicz rebroadcast_mode do KNOWN_ONLY, ignoruj nieznane węzły, regularnie update’uj firmware. Routing nie chroni przed fizycznymi atakami (np. jamming LoRa), ale PacketHistory pomaga wykrywać duplikaty ataków replay.

Integracje:

  • MQTT: Umożliwia bridging mesh nad internetem – pakiety forwardingowane do serwera MQTT (config.module.mqtt). Integruje z Home Assistant (dla automatyzacji, np. telemetry do dashboardu), Node Red (flow-based), Adafruit IO (IoT). Przykładowo: Włącz MQTT na routerze z WiFi/Ethernet, ustaw serwer (np. public.meshtastic.org), by łączyć zdalne sieci. Uwaga: W wersjach 2.6+ problem z MQTT+TLS na RAK4631 – wyłącz TLS lub update.
  • MUI (Meshtastic UI): Nowy standalone UI w 2.6+ dla urządzeń z ekranem (np. T-Deck), pozwala konfigurować routing bez appki mobilnej.
  • Inne: Backhaul po serial (RS232/485) lub SSH tunnel do łączenia węzłów; forwarding do Discord via MQTT.

Porównanie z innymi mesh:

  • vs. LoRaWAN: LoRaWAN jest scentralizowany (węzły łączą się z gateway’ami, które forwardingują do serwera chmurowego), co zapewnia skalowalność w IoT (np. smart cities), ale wymaga infrastruktury. Meshtastic to peer-to-peer mesh – prostszy, off-grid, ale mniej skalowalny w dużych sieciach z powodu floodingu.
  • vs. Reticulum: Reticulum to software-defined network (bardziej zaawansowany, z routingiem adaptacyjnym, MIT license), obsługuje różne radia (nie tylko LoRa). Meshtastic (GPLv3) jest prostszy, skupiony na LoRa i casual komunikacji, ale mniej elastyczny w złożonych topologiach.
  • vs. MeshCore: Lekka alternatywa do Meshtastic (wydana 2025), wymaga dedykowanych routerów (mniej trafficu niż full mesh Meshtastic), prostsza w małych grupach. Meshtastic oferuje więcej features (np. telemetry, roles), ale MeshCore jest lepszy dla low-power, off-grid messaging bez overheadu. Wybór zależy od use case: Meshtastic dla community networks, MeshCore dla minimalizmu.
  1. Przykłady troubleshooting – co zrobić w typowych problemach
    Oto przykłady kroków do rozwiązania powszechnych problemów z routingiem, oparte na obserwacjach z sieci (np. miejskich jak Szczecin):
  • Sieć zapchana (zbyt wiele retransmisji, kolizje): Sprawdź role – zmień większość węzłów na CLIENT_BASE lub SENSOR (CLI: meshtastic --set device.role CLIENT_BASE). Ustaw rebroadcast_mode=KNOWN_ONLY na klientach. Obniż hop_limit do 3-4 (config.lora.hop_limit). Monitoruj via Prometheus/Grafana (z wideo #603) – jeśli duplikaty w PacketHistory >50%, dodaj ignore_incoming dla noisy węzłów.
  • Pakiety giną w DM (brak ACK): Update do 2.6+ dla lepszego next-hop. Sprawdź want_ack=1 w pakietach. Jeśli fallback nie działa, wyczyść NodeDB (reset node info) i przetestuj z mniejszym hop_limit. W gęstej sieci dodaj favorites dla kluczowych routerów.
  • Problemy z UDP/MQTT (burza w LAN): Wyłącz UDP na nie-routerach (config.device.udp_enabled=false). W MQTT włącz TLS i ogranicz do trusted serwerów. Jeśli crash od want_response, update do 2.6.2+. Testuj: Wyślij testowy broadcast i sprawdź logi (–debug).
  • Debugowanie duplikatów: Użyj CLI meshtastic --nodes do inspekcji NodeDB. Sprawdź wasSeenRecently w logach – jeśli true zbyt często, dostosuj CWsize/SNR thresholds w RadioInterface.

Te kroki opierają się na kodzie i dokumentacji – zawsze testuj w małej podsieci przed rolloutem.


Opublikowano

w

przez

Tagi: