Collection of articles for working with Multitech devices in LoRaWAN networks.
The LoRa MQTT Bridge is a Python application designed to bridge MQTT messages from a local LoRaWAN gateway broker to multiple remote MQTT brokers. It provides powerful filtering capabilities based on device identifiers and message fields.
┌──────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ LoRa Gateway │────▶│ MQTT Bridge │────▶│ Remote Broker 1 │
│ (Local Broker) │ │ │────▶│ Remote Broker 2 │
│ │◀────│ │────▶│ Remote Broker N │
└──────────────────┘ └─────────────────┘ └──────────────────┘
# Clone the repository
git clone https://github.com/MultiTechSystems/lora-mqtt-bridge.git
cd lora-mqtt-bridge
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Install the package
pip install -e .
pip install lora-mqtt-bridge
Create a config.json file:
{
"local_broker": {
"host": "127.0.0.1",
"port": 1883
},
"remote_brokers": [
{
"name": "cloud",
"host": "mqtt.example.com",
"port": 8883,
"username": "your-username",
"password": "your-password",
"tls": {
"enabled": true
}
}
]
}
# Using configuration file
lora-mqtt-bridge -c config.json
# Or using the Python module
python -m lora_mqtt_bridge -c config.json
# Or run with environment variables
export LORA_MQTT_BRIDGE_LOCAL_HOST=127.0.0.1
export LORA_MQTT_BRIDGE_REMOTE_HOST=cloud.example.com
python -m lora_mqtt_bridge --env
2024-01-15 10:00:00 - INFO - Connected to local broker
2024-01-15 10:00:01 - INFO - Connected to remote broker: cloud
{
"local_broker": {
"host": "127.0.0.1",
"port": 1883,
"username": null,
"password": null,
"client_id": "lora-mqtt-bridge-local",
"topics": {
"format": "lora",
"uplink_pattern": "lora/+/+/up",
"downlink_pattern": "lora/%s/down"
},
"keepalive": 60
},
"remote_brokers": [
{
"name": "cloud-primary",
"enabled": true,
"host": "mqtt.cloud.com",
"port": 8883,
"username": "user",
"password": "pass",
"client_id": "gateway-001",
"tls": {
"enabled": true,
"ca_cert": "/path/to/ca.pem",
"client_cert": "/path/to/client.pem",
"client_key": "/path/to/client.key",
"verify_hostname": true,
"insecure": false
},
"source_topic_format": ["lora", "scada"],
"topics": {
"uplink_pattern": "lorawan/%(gwuuid)s/%(appeui)s/%(deveui)s/up",
"downlink_pattern": "lorawan/%(deveui)s/down"
},
"message_filter": {
"deveui_whitelist": [],
"deveui_blacklist": [],
"joineui_whitelist": [],
"joineui_blacklist": [],
"appeui_whitelist": [],
"appeui_blacklist": []
},
"field_filter": {
"include_fields": [],
"exclude_fields": ["rssi", "snr"],
"always_include": ["deveui", "appeui", "time"]
},
"keepalive": 60,
"clean_session": false,
"qos": 1,
"retain": true
}
],
"log": {
"level": "INFO",
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
"file": "/var/log/lora-mqtt-bridge.log"
},
"reconnect_delay": 1.0,
"max_reconnect_delay": 60.0
}
| Field | Type | Default | Description |
|---|---|---|---|
| host | string | “127.0.0.1” | Local broker hostname |
| port | integer | 1883 | Local broker port |
| username | string | null | Authentication username |
| password | string | null | Authentication password |
| client_id | string | “lora-mqtt-bridge-local” | MQTT client ID |
| topics | object | - | Topic configuration |
| keepalive | integer | 60 | Keepalive interval in seconds |
Each remote broker can have the following configuration:
| Field | Type | Default | Description |
|---|---|---|---|
| name | string | required | Unique broker identifier |
| enabled | boolean | true | Enable/disable this broker |
| host | string | required | Broker hostname |
| port | integer | 1883 | Broker port |
| username | string | null | Authentication username |
| password | string | null | Authentication password |
| client_id | string | auto-generated | MQTT client ID |
| tls | object | - | TLS configuration |
| source_topic_format | array | [“lora”] | Local topic formats to forward: “lora”, “scada”, or both |
| topics | object | - | Topic configuration |
| message_filter | object | - | Message filtering rules |
| field_filter | object | - | Field filtering rules |
| keepalive | integer | 60 | Keepalive interval |
| clean_session | boolean | false | MQTT clean session flag |
| qos | integer | 1 | Default QoS level (0-2) |
| retain | boolean | true | Retain published messages |
| Field | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | false | Enable TLS |
| ca_cert | string | null | CA certificate path or content |
| client_cert | string | null | Client certificate path or content |
| client_key | string | null | Client key path or content |
| verify_hostname | boolean | true | Verify server hostname |
| insecure | boolean | false | Allow insecure connections |
| Field | Type | Default | Description |
|---|---|---|---|
| format | string | “lora” | Topic format: “lora” or “scada” |
| uplink_pattern | string | “lora/+/+/up” | Uplink topic pattern (supports %(gwuuid)s, %(deveui)s, %(appeui)s, %(joineui)s, %(gweui)s) |
| downlink_pattern | string | “lora/%s/down” | Downlink topic pattern |
Note: The %(gwuuid)s variable is automatically populated with the gateway’s UUID at runtime.
Standard topic format used by Multitech gateways:
Local Subscriptions:
lora/+/+/up - Device uplinkslora/+/joined - Join eventslora/+/+/moved - Device moved eventsLocal Publishes:
lora/{deveui}/down - Downlinks to deviceslora/{deveui}/clear - Clear downlink queueAlternative topic format for SCADA systems with decoded payload data:
Local Subscriptions:
scada/+/+/up - Device uplinks (e.g., scada/lorawan/{deveui}/up)Local Publishes:
scada/{deveui}/down - Downlinks to devicesNote: SCADA topics contain JSON-decoded payload data instead of base64-encoded data.
Available variables for format strings:
%(deveui)s - Device EUI (e.g., 00-80-00-00-0a-00-11-ba)%(appeui)s - Application EUI (e.g., 16-ea-76-f6-ab-66-3d-80)%(joineui)s - Join EUI (e.g., 16-ea-76-f6-ab-66-3d-80)%(gweui)s - Gateway EUI (e.g., 00-80-00-00-d0-00-42-6e)%(gwuuid)s - Gateway UUID, automatically retrieved at runtime (e.g., 244ab1fb-b08d-1dcc-d02d-bee6f5236ced)%s - Device EUI (simple format)%(deveui)s - Device EUI (named format){
"topics": {
"format": "lora",
"uplink_pattern": "lora/+/+/up",
"downlink_pattern": "lora/%s/down"
}
}
Topic examples:
lora/aa-bb-cc-dd/00-11-22-33/uplora/00-11-22-33/down{
"topics": {
"uplink_pattern": "lorawan/%(appeui)s/%(deveui)s/up",
"downlink_pattern": "lorawan/%(deveui)s/down"
}
}
Topic examples:
lorawan/aa-bb-cc-dd-ee-ff-00-11/00-11-22-33-44-55-66-77/uplorawan/00-11-22-33-44-55-66-77/downThe gateway UUID is automatically retrieved from the system at runtime (from /sys/devices/platform/mts-io/uuid on MultiTech gateways):
{
"topics": {
"uplink_pattern": "lorawan/%(gwuuid)s/%(appeui)s/%(deveui)s/up"
}
}
Example output topic: lorawan/244ab1fb-b08d-1dcc-d02d-bee6f5236ced/16-ea-76-f6-ab-66-3d-80/00-80-00-00-0a-00-11-ba/up
Each remote broker can specify which local topic formats to forward using source_topic_format. This allows you to selectively forward LoRa messages, SCADA messages, or both:
{
"remote_brokers": [
{
"name": "lora-only",
"host": "lora.example.com",
"source_topic_format": ["lora"],
"topics": {
"uplink_pattern": "lorawan/%(gwuuid)s/%(appeui)s/%(deveui)s/up"
}
},
{
"name": "scada-only",
"host": "scada.example.com",
"source_topic_format": ["scada"],
"topics": {
"uplink_pattern": "scada/%(deveui)s/up"
}
},
{
"name": "all-data",
"host": "cloud.example.com",
"source_topic_format": ["lora", "scada"],
"topics": {
"uplink_pattern": "devices/%(gwuuid)s/%(deveui)s/up"
}
}
]
}
Note: If source_topic_format is not specified, it defaults to ["lora"].
Message filtering allows you to control which devices’ messages are forwarded to each remote broker based on device identifiers.
Filter messages based on the device’s DevEUI (Device Extended Unique Identifier).
{
"message_filter": {
"deveui_whitelist": ["00-11-22-33-44-55-66-77"],
"deveui_blacklist": ["ff-ff-ff-ff-ff-ff-ff-ff"]
}
}
Filter messages based on the JoinEUI (also known as AppEUI in LoRaWAN 1.0.x).
{
"message_filter": {
"joineui_whitelist": ["aa-bb-cc-dd-ee-ff-00-11"],
"joineui_blacklist": []
}
}
Filter messages based on the AppEUI.
{
"message_filter": {
"appeui_whitelist": ["aa-bb-cc-dd-ee-ff-00-11"],
"appeui_blacklist": []
}
}
Blacklist takes precedence: If a device is in both whitelist and blacklist, it will be blocked.
Empty whitelist allows all: If the whitelist is empty, all devices are allowed (subject to blacklist).
Non-empty whitelist restricts: If the whitelist has entries, only those devices are allowed.
Multiple filter types combine with AND: A message must pass all filter types (DevEUI, JoinEUI, AppEUI).
{
"message_filter": {
"deveui_whitelist": [
"00-11-22-33-44-55-66-77",
"00-11-22-33-44-55-66-88"
]
}
}
{
"message_filter": {
"deveui_blacklist": [
"ff-ff-ff-ff-ff-ff-ff-ff"
]
}
}
Forward only messages from devices belonging to a specific application:
{
"message_filter": {
"appeui_whitelist": ["aa-bb-cc-dd-ee-ff-00-11"]
}
}
{
"remote_brokers": [
{
"name": "app1-broker",
"host": "app1.example.com",
"message_filter": {
"appeui_whitelist": ["aa-bb-cc-dd-ee-ff-00-11"]
}
},
{
"name": "app2-broker",
"host": "app2.example.com",
"message_filter": {
"appeui_whitelist": ["11-22-33-44-55-66-77-88"]
}
}
]
}
Field filtering allows you to control which fields are included in forwarded messages.
Only include specified fields (plus always-include fields):
{
"field_filter": {
"include_fields": ["deveui", "port", "data", "fcnt"]
}
}
Exclude specific fields from messages:
{
"field_filter": {
"exclude_fields": ["rssi", "snr", "freq", "dr"]
}
}
Fields that are always included regardless of other filters:
{
"field_filter": {
"always_include": ["deveui", "appeui", "time"]
}
}
Always-include takes precedence: Fields in always_include are never filtered out.
Include mode: If include_fields is non-empty, only those fields (plus always-include) are forwarded.
Exclude mode: If include_fields is empty, all fields except those in exclude_fields are forwarded.
Forward only essential fields to reduce bandwidth:
{
"field_filter": {
"include_fields": ["deveui", "port", "data"],
"always_include": ["deveui", "time"]
}
}
Input message:
{
"deveui": "00-11-22-33-44-55-66-77",
"appeui": "aa-bb-cc-dd-ee-ff-00-11",
"time": "2024-01-15T10:00:00Z",
"port": 1,
"data": "SGVsbG8=",
"rssi": -85,
"snr": 7.5,
"freq": 868.1,
"fcnt": 42
}
Output message:
{
"deveui": "00-11-22-33-44-55-66-77",
"time": "2024-01-15T10:00:00Z",
"port": 1,
"data": "SGVsbG8="
}
All EUI values are automatically normalized to lowercase with dashes:
0011223344556677 → 00-11-22-33-44-55-66-7700:11:22:33:44:55:66:77 → 00-11-22-33-44-55-66-77AA-BB-CC-DD-EE-FF-00-11 → aa-bb-cc-dd-ee-ff-00-11This ensures consistent matching regardless of input format.
All configuration can be provided via environment variables with the prefix LORA_MQTT_BRIDGE_.
LORA_MQTT_BRIDGE_LOCAL_HOST=127.0.0.1
LORA_MQTT_BRIDGE_LOCAL_PORT=1883
LORA_MQTT_BRIDGE_LOCAL_USERNAME=user
LORA_MQTT_BRIDGE_LOCAL_PASSWORD=pass
LORA_MQTT_BRIDGE_LOCAL_CLIENT_ID=my-client
LORA_MQTT_BRIDGE_LOCAL_TOPIC_FORMAT=lora
LORA_MQTT_BRIDGE_LOCAL_UPLINK_PATTERN=lora/+/+/up
LORA_MQTT_BRIDGE_LOCAL_DOWNLINK_PATTERN=lora/%s/down
LORA_MQTT_BRIDGE_REMOTE_NAME=cloud
LORA_MQTT_BRIDGE_REMOTE_HOST=mqtt.example.com
LORA_MQTT_BRIDGE_REMOTE_PORT=8883
LORA_MQTT_BRIDGE_REMOTE_USERNAME=user
LORA_MQTT_BRIDGE_REMOTE_PASSWORD=pass
LORA_MQTT_BRIDGE_REMOTE_TLS_ENABLED=true
LORA_MQTT_BRIDGE_REMOTE_TLS_VERIFY_HOSTNAME=true
LORA_MQTT_BRIDGE_REMOTE_DEVEUI_WHITELIST=00-11-22-33-44-55-66-77,aa-bb-cc-dd-ee-ff-00-11
LORA_MQTT_BRIDGE_REMOTE_EXCLUDE_FIELDS=rssi,snr,freq
LORA_MQTT_BRIDGE_REMOTE_BROKERS='[
{"name": "broker1", "host": "broker1.example.com", "port": 8883},
{"name": "broker2", "host": "broker2.example.com", "port": 8884}
]'
usage: lora-mqtt-bridge [-h] [-c CONFIG] [--env] [--log-level LEVEL] [--log-file FILE] [-v]
Options:
-h, --help Show help message
-c, --config FILE Path to configuration file (JSON)
--env Load configuration from environment variables
--log-level LEVEL Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
--log-file FILE Path to log file
-v, --version Show version
Create a systemd service file /etc/systemd/system/lora-mqtt-bridge.service:
[Unit]
Description=LoRa MQTT Bridge
After=network.target mosquitto.service
Wants=network.target
[Service]
Type=simple
User=lora
Group=lora
WorkingDirectory=/opt/lora-mqtt-bridge
ExecStart=/opt/lora-mqtt-bridge/venv/bin/python -m lora_mqtt_bridge -c /etc/lora-mqtt-bridge/config.json
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=lora-mqtt-bridge
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable lora-mqtt-bridge
sudo systemctl start lora-mqtt-bridge
sudo systemctl status lora-mqtt-bridge
Create a Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
COPY pyproject.toml .
RUN pip install --no-cache-dir -e .
CMD ["lora-mqtt-bridge", "--env"]
Build and run:
docker build -t lora-mqtt-bridge .
docker run -d \
--name lora-bridge \
--restart unless-stopped \
-e LORA_MQTT_BRIDGE_LOCAL_HOST=host.docker.internal \
-e LORA_MQTT_BRIDGE_REMOTE_HOST=mqtt.example.com \
lora-mqtt-bridge
version: '3.8'
services:
lora-mqtt-bridge:
build: .
restart: unless-stopped
environment:
- LORA_MQTT_BRIDGE_LOCAL_HOST=${LOCAL_MQTT_HOST:-localhost}
- LORA_MQTT_BRIDGE_LOCAL_PORT=1883
- LORA_MQTT_BRIDGE_REMOTE_HOST=${REMOTE_MQTT_HOST}
- LORA_MQTT_BRIDGE_REMOTE_PORT=${REMOTE_MQTT_PORT:-8883}
- LORA_MQTT_BRIDGE_REMOTE_USERNAME=${REMOTE_MQTT_USER}
- LORA_MQTT_BRIDGE_REMOTE_PASSWORD=${REMOTE_MQTT_PASS}
- LORA_MQTT_BRIDGE_REMOTE_TLS_ENABLED=true
volumes:
- ./config.json:/app/config.json:ro
- ./certs:/app/certs:ro
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Always use TLS for remote connections:
{
"remote_brokers": [
{
"name": "cloud",
"host": "mqtt.example.com",
"port": 8883,
"tls": {
"enabled": true,
"ca_cert": "/etc/lora-mqtt-bridge/certs/ca.pem",
"client_cert": "/etc/lora-mqtt-bridge/certs/client.pem",
"client_key": "/etc/lora-mqtt-bridge/certs/client.key",
"verify_hostname": true
}
}
]
}
sudo chmod 600 /etc/lora-mqtt-bridge/config.json
sudo chown lora:lora /etc/lora-mqtt-bridge/config.json
Enable debug logging:
lora-mqtt-bridge -c config.json --log-level DEBUG
Or in configuration:
{
"log": {
"level": "DEBUG"
}
}
The main bridge manager class that orchestrates message forwarding.
from lora_mqtt_bridge import MQTTBridge
from lora_mqtt_bridge.models.config import BridgeConfig
config = BridgeConfig.from_dict(config_dict)
bridge = MQTTBridge(config)
start() -> NoneStart the bridge and connect to all configured brokers.
bridge.start()
stop() -> NoneStop the bridge and disconnect from all brokers.
bridge.stop()
run() -> NoneRun the bridge in the foreground until interrupted.
bridge.run() # Blocks until SIGINT/SIGTERM
get_status() -> dictGet the current status of the bridge.
status = bridge.get_status()
# Returns:
# {
# "running": True,
# "local_broker": {"connected": True, "host": "127.0.0.1", "port": 1883},
# "remote_brokers": {
# "cloud": {"connected": True, "queue_size": 0}
# }
# }
add_remote_broker(config: RemoteBrokerConfig) -> NoneAdd a new remote broker dynamically.
from lora_mqtt_bridge.models.config import RemoteBrokerConfig
new_broker = RemoteBrokerConfig(name="new", host="new.example.com")
bridge.add_remote_broker(new_broker)
remove_remote_broker(name: str) -> boolRemove a remote broker.
removed = bridge.remove_remote_broker("cloud")
This project is licensed under the Apache License 2.0.