Skip to content

LoRa Gateway Tags

Mathison edited this page Jun 19, 2026 · 2 revisions

LoRa Gateway Tags

Crow uses short gateway tags when forwarding gateway-originated cleartext messages OUT to local LoRa networks (Meshtastic or MeshCore). The outbound LoRa payload looks like:

CALLSIGN@TAG> message

Examples:

KJ6DZB@MCGW> Hello local MeshCore
KJ6DZB@MCG2> Same message via the second MeshCore backend
KJ6DZB@MTGW> Hello Meshtastic
KJ6DZB@MTG1> Same message via the second Meshtastic backend

Why this exists

LoRa-side users need to know that a message came through a gateway and which gateway/backend emitted it. Crow keeps the original sender's callsign at the front of the LoRa text payload and adds a compact backend tag.

This complements the inbound-side Strict Gatekeeper [SENDER via GATEWAY] annotation:

  • Inbound (LoRa → AREDN): gatekeeper rewrites text as [KJ6DZB via W6XYZ] hello so AREDN readers see who sent it and which gateway carried it.
  • Outbound (AREDN → LoRa): the tagged wrapper rewrites text as W6XYZ@MCGW> hello so LoRa readers see who sent it and which gateway emitted it.

Tagging happens at the outbound backend layer, not globally in the router, because the backend knows the actual target path.

What happens when you flip it on

Tagging is implemented as a wrapper module that wraps the production backend. The wrapper:

  1. Receives the outbound message from the router via send(msg).
  2. Runs lora_outbound_text.prepare(msg, target_transport, gateway_index, max_payload) to build the tagged payload.
  3. Replaces msg.data.text_message with the tagged text (the original message object is cloned, not mutated).
  4. Hands the tagged message to the underlying production backend for actual transmission.

Inbound traffic (recv()) is passed through unchanged. Tagging only affects outbound LoRa text. Non-text messages and messages with no text_message field are passed through unchanged.

If the tagged payload exceeds the backend's payload budget, the original message text is truncated to fit and ... is appended. The callsign and tag are always preserved.

Tag scheme

The helper module is lora_outbound_text.uc. The tag family is:

Target backend family Primary gateway Additional gateways
MeshCore MCGW MCG2, MCG3, MCG4, ...
Meshtastic MTGW MTG1, MTG2, MTG3, ...

Numbering preserves the short primary gateway names:

  • MCGW is the primary MeshCore gateway.
  • MCG2 is the second MeshCore gateway/backend.
  • MTGW is the primary Meshtastic gateway.
  • MTG1 is the first additional Meshtastic gateway/backend.

The index is set via gateway_index in the matching backend config block.

How to enable tagging

Gateway tagging is off by default. Production routing uses the raw backends:

import * as meshtastic from "meshtastic";    // raw — no outbound tag
import * as meshcore   from "meshcore";       // raw — no outbound tag

To turn tagging on, swap the import lines in router.uc to the tagged wrappers:

import * as meshtastic from "meshtastic_tagged";
import * as meshcore   from "meshcore_tagged";

You can enable just one side if you only want tags on one transport.

Enable Meshtastic tagging

In router.uc:

import * as meshtastic from "meshtastic_tagged";

In config:

{
  "meshtastic": {
    "enabled": true,
    "gateway_index": 0,
    "gateway_tag_max_payload": 200
  }
}
gateway_index Tag
0 MTGW
1 MTG1
2 MTG2

Enable MeshCore tagging

In router.uc:

import * as meshcore from "meshcore_tagged";

In config:

{
  "meshcore": {
    "enabled": true,
    "gateway_index": 0,
    "gateway_tag_max_payload": 150
  }
}
gateway_index Tag
0 MCGW
1 MCG2
2 MCG3

Disable / rollback

Swap the import lines back to the raw modules. No message-schema changes are required and no config keys need to be removed (extra config fields are simply ignored by the raw backends).

Formatter behavior

The helper signature is:

prepare(msg, target_transport, gateway_index, max_payload)

Returns a safely formatted string:

CALLSIGN@TAG> original text

Callsign lookup order:

msg.originating_callsign
msg.callsign
msg.from_callsign
msg.data.callsign
UNKNOWN

If the formatted payload exceeds max_payload, the original text is truncated and ... is appended. The header (CALLSIGN@TAG> ) is preserved; if even the header exceeds the budget, the header itself is truncated and the payload is dropped.

MTU and backend limits

Default budget: 255 bytes.

Backends should pass a smaller budget if their packet format adds overhead or has a tighter text limit. This avoids hidden truncation deeper in the encoder.

Recommended budgets:

Backend Suggested call
MeshCore (UDP) prepare(msg, "meshcore", gateway_index, 150)
Meshtastic (UDP) prepare(msg, "meshtastic", gateway_index, 200)

Backend ownership

Gateway tagging happens immediately before the backend packet encoder.

Do not inject the tag globally in the router because msg.transport describes where the message came from, not where it is going. The router has a single send call but the tagging needs to know the actual outbound transport.

Correct placement:

AREDN/native message
  → router.uc decides outbound backend
  → meshtastic_tagged.send() or meshcore_tagged.send()
  → lora_outbound_text.prepare(...)
  → underlying meshtastic.send() or meshcore.send()
  → encoder / UDP multicast

Debug logging

The formatter emits DEBUG1 lines:

lora_outbound_text: formatted outbound target=meshcore tag=MCGW callsign=KJ6DZB total=42 max=150
lora_outbound_text: truncated outbound target=meshtastic tag=MTG1 callsign=KJ6DZB original=320 final=200 max=200
lora_outbound_text: header exceeds payload budget callsign=KJ6DZB tag=MCGW header_len=14 max=10

Test cases

Run from the repo root:

node tests/run_formatter_tests.js
ucode -R -L tests/test_outbound_formatter.uc       # on a node with ucode

20 cases covering:

Input Backend Index Expected prefix
Hello meshcore 0 CALLSIGN@MCGW> Hello
Hello meshcore 1 CALLSIGN@MCG2> Hello
Hello meshcore 2 CALLSIGN@MCG3> Hello
Hello meshtastic 0 CALLSIGN@MTGW> Hello
Hello meshtastic 1 CALLSIGN@MTG1> Hello
Hello meshtastic 2 CALLSIGN@MTG2> Hello

Plus truncation behavior, missing-text passthrough, callsign fallback chain, and exact-fit boundary.

File layout

Module Role
lora_outbound_text.uc Shared formatter. gatewayTag() returns the tag string; prepare() returns the full tagged payload.
meshtastic_tagged.uc Wrapper around meshtastic.uc (production UDP backend) that calls prepare() on every outbound send(msg).
meshcore_tagged.uc Wrapper around meshcore.uc (production UDP backend) that calls prepare() on every outbound send(msg).
meshtastic.uc Raw Meshtastic UDP backend. Untagged.
meshcore.uc Raw MeshCore UDP backend. Untagged.

The wrappers re-export enabled, setup, shutdown, handle, recv, send, tick, and process. They are drop-in replacements for the raw backends in router.uc.

What's not yet tagged

Transport Tagging status
meshtastic (UDP, production) Wrapper exists (meshtastic_tagged.uc). Opt-in via import swap.
meshcore (UDP, production) Wrapper exists (meshcore_tagged.uc). Opt-in via import swap.
meshtastic_API (TCP Port-API, experimental) No tagged wrapper. Backend isn't wired into the router yet.
meshcore_tcp_api (TCP Companion API, experimental) No tagged wrapper. Backend only receives TXT_MSG/GRP_TXT today; outbound is stubbed and outbound LoRa still goes via meshcore.uc.

When the experimental TCP backends are promoted to production, equivalent tagged wrappers should be added before they are wired into router.uc.

Related

  • Strict Gatekeeper — inbound-side annotation that pairs with outbound tagging
  • Bridges — supported transports and what they do

Crow Wiki

Pages

Markdown files

  • APRS.md
  • Backend-Selection-and-Deployment.md
  • Change-Log.md
  • Command-Reference.md
  • Configuration.md
  • Configuring-Channels.md
  • Home.md
  • LoRa-Gateway-Tags.md
  • Meshtastic-API.md
  • Memory-Use.md
  • Strict-Gatekeeper.md
  • USB-Storage.md
  • Winlink.md
  • _Sidebar.md

Maintenance

  • Keep every .md wiki page linked here.
  • Keep Home.md and _Sidebar.md in sync.
  • When a wiki page is removed, remove it from both the Home page inventory and this sidebar.

Clone this wiki locally