LoRa MQTT Bridge

Collection of articles for working with Multitech devices in LoRaWAN networks.

LoRa MQTT Bridge

Overview

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.

Features

Architecture

┌──────────────────┐     ┌─────────────────┐     ┌──────────────────┐
│   LoRa Gateway   │────▶│  MQTT Bridge    │────▶│ Remote Broker 1  │
│  (Local Broker)  │     │                 │────▶│ Remote Broker 2  │
│                  │◀────│                 │────▶│ Remote Broker N  │
└──────────────────┘     └─────────────────┘     └──────────────────┘

Getting Started

Prerequisites

Installation

From Source

# 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 .

Using pip

pip install lora-mqtt-bridge

Basic Configuration

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
      }
    }
  ]
}

Running the Bridge

# 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

Verifying Operation

  1. Check the logs for successful connection messages:
    2024-01-15 10:00:00 - INFO - Connected to local broker
    2024-01-15 10:00:01 - INFO - Connected to remote broker: cloud
    
  2. Send a test uplink from a device and verify it appears on your remote broker.

Configuration Reference

Complete Configuration Example

{
  "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
}

Local Broker Configuration

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

Remote Broker Configuration

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

TLS Configuration

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

Topic Configuration

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.

Topic Formats

LoRa Format (Default)

Standard topic format used by Multitech gateways:

Local Subscriptions:

Local Publishes:

SCADA Format

Alternative topic format for SCADA systems with decoded payload data:

Local Subscriptions:

Local Publishes:

Note: SCADA topics contain JSON-decoded payload data instead of base64-encoded data.

Topic Pattern Variables

Available variables for format strings:

Topic Examples

Standard LoRa Topics

{
  "topics": {
    "format": "lora",
    "uplink_pattern": "lora/+/+/up",
    "downlink_pattern": "lora/%s/down"
  }
}

Topic examples:

Cloud-Style Topics

{
  "topics": {
    "uplink_pattern": "lorawan/%(appeui)s/%(deveui)s/up",
    "downlink_pattern": "lorawan/%(deveui)s/down"
  }
}

Topic examples:

Including Gateway UUID

The 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

Source Topic Format Selection

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

Message filtering allows you to control which devices’ messages are forwarded to each remote broker based on device identifiers.

Filter Types

DevEUI Filtering

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"]
  }
}

JoinEUI Filtering

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": []
  }
}

AppEUI Filtering

Filter messages based on the AppEUI.

{
  "message_filter": {
    "appeui_whitelist": ["aa-bb-cc-dd-ee-ff-00-11"],
    "appeui_blacklist": []
  }
}

Filter Rules

  1. Blacklist takes precedence: If a device is in both whitelist and blacklist, it will be blocked.

  2. Empty whitelist allows all: If the whitelist is empty, all devices are allowed (subject to blacklist).

  3. Non-empty whitelist restricts: If the whitelist has entries, only those devices are allowed.

  4. Multiple filter types combine with AND: A message must pass all filter types (DevEUI, JoinEUI, AppEUI).

Filter Examples

Allow Only Specific Devices

{
  "message_filter": {
    "deveui_whitelist": [
      "00-11-22-33-44-55-66-77",
      "00-11-22-33-44-55-66-88"
    ]
  }
}

Block Specific Devices

{
  "message_filter": {
    "deveui_blacklist": [
      "ff-ff-ff-ff-ff-ff-ff-ff"
    ]
  }
}

Filter by Application

Forward only messages from devices belonging to a specific application:

{
  "message_filter": {
    "appeui_whitelist": ["aa-bb-cc-dd-ee-ff-00-11"]
  }
}

Different Filters for Different Brokers

{
  "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

Field filtering allows you to control which fields are included in forwarded messages.

Filter Options

Include Fields

Only include specified fields (plus always-include fields):

{
  "field_filter": {
    "include_fields": ["deveui", "port", "data", "fcnt"]
  }
}

Exclude Fields

Exclude specific fields from messages:

{
  "field_filter": {
    "exclude_fields": ["rssi", "snr", "freq", "dr"]
  }
}

Always Include Fields

Fields that are always included regardless of other filters:

{
  "field_filter": {
    "always_include": ["deveui", "appeui", "time"]
  }
}

Field Filter Rules

  1. Always-include takes precedence: Fields in always_include are never filtered out.

  2. Include mode: If include_fields is non-empty, only those fields (plus always-include) are forwarded.

  3. Exclude mode: If include_fields is empty, all fields except those in exclude_fields are forwarded.

Field Filter Example

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="
}

EUI Format Normalization

All EUI values are automatically normalized to lowercase with dashes:

This ensures consistent matching regardless of input format.

Environment Variables

All configuration can be provided via environment variables with the prefix LORA_MQTT_BRIDGE_.

Local Broker Variables

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

Single Remote Broker Variables

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

Multiple Remote Brokers (JSON)

LORA_MQTT_BRIDGE_REMOTE_BROKERS='[
  {"name": "broker1", "host": "broker1.example.com", "port": 8883},
  {"name": "broker2", "host": "broker2.example.com", "port": 8884}
]'

Command Line Options

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

Deployment

Running as a Systemd Service (Linux)

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

Docker Deployment

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

Docker Compose

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"

Security Best Practices

TLS Configuration

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
      }
    }
  ]
}

Credential Management

sudo chmod 600 /etc/lora-mqtt-bridge/config.json
sudo chown lora:lora /etc/lora-mqtt-bridge/config.json

Network Security

Troubleshooting

Common Issues

  1. Connection refused to local broker
    • Check that the local broker is running
    • Verify the host and port configuration
    • Check firewall rules
  2. TLS handshake failures
    • Verify certificate validity
    • Check hostname verification settings
    • Ensure CA certificate is correct
  3. Messages not forwarding
    • Check filter configurations
    • Verify topic patterns match incoming messages
    • Enable DEBUG logging to see filter decisions
  4. High memory usage
    • Check message queue sizes
    • Monitor for connection issues causing queue buildup
    • Consider reducing max queue size

Debug Mode

Enable debug logging:

lora-mqtt-bridge -c config.json --log-level DEBUG

Or in configuration:

{
  "log": {
    "level": "DEBUG"
  }
}

API Reference

MQTTBridge Class

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)

Methods

start() -> None

Start the bridge and connect to all configured brokers.

bridge.start()
stop() -> None

Stop the bridge and disconnect from all brokers.

bridge.stop()
run() -> None

Run the bridge in the foreground until interrupted.

bridge.run()  # Blocks until SIGINT/SIGTERM
get_status() -> dict

Get 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) -> None

Add 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) -> bool

Remove a remote broker.

removed = bridge.remove_remote_broker("cloud")

License

This project is licensed under the Apache License 2.0.