From 31a1c148314c92c344489fcf6ca132666dc5d54a Mon Sep 17 00:00:00 2001 From: Alexis Fourmaux Date: Thu, 7 May 2026 00:13:17 +0200 Subject: [PATCH] feat: add basic app that consumes mqtt uplinks from chirpstack --- app/Dockerfile | 6 + app/main.py | 30 ++ app/requirements.txt | 1 + ...ack-gateway-bridge-basicstation-eu868.toml | 45 +++ .../chirpstack-gateway-bridge.toml | 7 + configuration/chirpstack/chirpstack.toml | 105 +++++++ configuration/chirpstack/region_eu868.toml | 280 ++++++++++++++++++ configuration/mosquitto/config/mosquitto.conf | 2 + .../initdb/001-chirpstack_extensions.sh | 7 + docker-compose.yml | 87 ++++++ 10 files changed, 570 insertions(+) create mode 100644 app/Dockerfile create mode 100644 app/main.py create mode 100644 app/requirements.txt create mode 100644 configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml create mode 100644 configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml create mode 100644 configuration/chirpstack/chirpstack.toml create mode 100644 configuration/chirpstack/region_eu868.toml create mode 100644 configuration/mosquitto/config/mosquitto.conf create mode 100644 configuration/postgresql/initdb/001-chirpstack_extensions.sh create mode 100644 docker-compose.yml diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..b8c8907 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.13-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY main.py . +CMD ["python", "main.py"] \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..53fb907 --- /dev/null +++ b/app/main.py @@ -0,0 +1,30 @@ +import paho.mqtt.client as mqtt +import logging +import sys + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", + stream=sys.stdout, # Docker lit stdout par défaut + force=True +) + +log = logging.getLogger(__name__) + + +def on_connect(client, userdata, flags, reason_code, properties): + log.info(f"Connected with result code {reason_code}") + client.subscribe("application/+/device/+/event/up") + +def on_message(client, userdata, msg): + log.info(msg.topic+" "+str(msg.payload)) + + +if __name__ == "__main__": + mqttc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + mqttc.on_connect = on_connect + mqttc.on_message = on_message + + mqttc.connect("mosquitto", 1883, 60) + mqttc.loop_forever() \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..79791a2 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1 @@ +paho-mqtt==v2.1.0 \ No newline at end of file diff --git a/configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml b/configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml new file mode 100644 index 0000000..4c5b086 --- /dev/null +++ b/configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml @@ -0,0 +1,45 @@ +[integration.mqtt.auth.generic] +servers=["tcp://mosquitto:1883"] +username="" +password="" + +[integration.mqtt] +event_topic_template="eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}" +state_topic_template="eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}" +command_topic_template="eu868/gateway/{{ .GatewayID }}/command/#" + +[backend] +type="basic_station" + + [backend.basic_station] + bind=":3001" + tls_cert="" + tls_key="" + ca_cert="" + + region="EU868" + frequency_min=863000000 + frequency_max=870000000 + + + [[backend.basic_station.concentrators]] + + [backend.basic_station.concentrators.multi_sf] + frequencies=[ + 868100000, + 868300000, + 868500000, + 867100000, + 867300000, + 867500000, + 867700000, + 867900000, + ] + + [backend.basic_station.concentrators.lora_std] + frequency=868300000 + bandwidth=250000 + spreading_factor=7 + + [backend.basic_station.concentrators.fsk] + frequency=868800000 diff --git a/configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml b/configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml new file mode 100644 index 0000000..894de95 --- /dev/null +++ b/configuration/chirpstack-gateway-bridge/chirpstack-gateway-bridge.toml @@ -0,0 +1,7 @@ +# See https://www.chirpstack.io/gateway-bridge/install/config/ for a full +# configuration example and documentation. + +[integration.mqtt.auth.generic] +servers=["tcp://mosquitto:1883"] +username="" +password="" diff --git a/configuration/chirpstack/chirpstack.toml b/configuration/chirpstack/chirpstack.toml new file mode 100644 index 0000000..5ac43c2 --- /dev/null +++ b/configuration/chirpstack/chirpstack.toml @@ -0,0 +1,105 @@ +# Logging. +[logging] + + # Log level. + # + # Options are: trace, debug, info, warn error. + level="info" + + +# PostgreSQL configuration. +[postgresql] + + # PostgreSQL DSN. + # + # Format example: postgres://:@/?sslmode=. + # + # SSL mode options: + # * disable - Do not use TLS + # * prefer - Attempt to connect with TLS but allow sessions without + # * require - Require the use of TLS + dsn="postgres://chirpstack:chirpstack@$POSTGRESQL_HOST/chirpstack?sslmode=disable" + + # Max open connections. + # + # This sets the max. number of open connections that are allowed in the + # PostgreSQL connection pool. + max_open_connections=10 + + # Min idle connections. + # + # This sets the min. number of idle connections in the PostgreSQL connection + # pool (0 = equal to max_open_connections). + min_idle_connections=0 + + +# Redis configuration. +[redis] + + # Server address or addresses. + # + # Set multiple addresses when connecting to a cluster. + servers=[ + "redis://$REDIS_HOST/", + ] + + # TLS enabled. + tls_enabled=false + + # Redis Cluster. + # + # Set this to true when the provided URLs are pointing to a Redis Cluster + # instance. + cluster=false + + +# Network related configuration. +[network] + + # Network identifier (NetID, 3 bytes) encoded as HEX (e.g. 010203). + net_id="000000" + + # Enabled regions. + # + # Multiple regions can be enabled simultaneously. Each region must match + # the 'name' parameter of the region configuration in '[[regions]]'. + enabled_regions=[ + "as923", + "as923_2", + "as923_3", + "as923_4", + "au915_0", + "cn470_10", + "cn779", + "eu433", + "eu868", + "in865", + "ism2400", + "kr920", + "ru864", + "us915_0", + "us915_1", + ] + + +# API interface configuration. +[api] + + # interface:port to bind the API interface to. + bind="0.0.0.0:8080" + + # Secret. + # + # This secret is used for generating login and API tokens, make sure this + # is never exposed. Changing this secret will invalidate all login and API + # tokens. The following command can be used to generate a random secret: + # openssl rand -base64 32 + secret="you-must-replace-this" + + +[integration] + enabled=["mqtt"] + + [integration.mqtt] + server="tcp://$MQTT_BROKER_HOST:1883/" + json=true diff --git a/configuration/chirpstack/region_eu868.toml b/configuration/chirpstack/region_eu868.toml new file mode 100644 index 0000000..c87f268 --- /dev/null +++ b/configuration/chirpstack/region_eu868.toml @@ -0,0 +1,280 @@ +# This file contains an example EU868 configuration. +[[regions]] + + # ID is an user-defined identifier for this region. + id="eu868" + + # Description is a short description for this region. + description="EU868" + + # Common-name refers to the common-name of this region as defined by + # the LoRa Alliance. + common_name="EU868" + + + # Gateway configuration. + [regions.gateway] + + # Force gateways as private. + # + # If enabled, gateways can only be used by devices under the same tenant. + force_gws_private=false + + + # Gateway backend configuration. + [regions.gateway.backend] + + # The enabled backend type. + enabled="mqtt" + + # MQTT configuration. + [regions.gateway.backend.mqtt] + + # Topic prefix. + # + # The topic prefix can be used to define the region of the gateway. + # Note, there is no need to add a trailing '/' to the prefix. The trailing + # '/' is automatically added to the prefix if it is configured. + topic_prefix="eu868" + + # MQTT server (e.g. scheme://host:port where scheme is tcp, ssl or ws) + server="tcp://$MQTT_BROKER_HOST:1883" + + # Connect with the given username (optional) + username="" + + # Connect with the given password (optional) + password="" + + # Quality of service level + # + # 0: at most once + # 1: at least once + # 2: exactly once + # + # Note: an increase of this value will decrease the performance. + # For more information: https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels + qos=0 + + # Clean session + # + # Set the "clean session" flag in the connect message when this client + # connects to an MQTT broker. By setting this flag you are indicating + # that no messages saved by the broker for this client should be delivered. + clean_session=false + + # Client ID + # + # Set the client id to be used by this client when connecting to the MQTT + # broker. A client id must be no longer than 23 characters. If left blank, + # a random id will be generated by ChirpStack. + client_id="" + + # Keep alive interval. + # + # This defines the maximum time that that should pass without communication + # between the client and server. + keep_alive_interval="30s" + + # CA certificate file (optional) + # + # Use this when setting up a secure connection (when server uses ssl://...) + # but the certificate used by the server is not trusted by any CA certificate + # on the server (e.g. when self generated). + ca_cert="" + + # TLS certificate file (optional) + tls_cert="" + + # TLS key file (optional) + tls_key="" + + + # Gateway channel configuration. + # + # Note: this configuration is only used in case the gateway is using the + # ChirpStack Concentratord daemon. In any other case, this configuration + # is ignored. + [[regions.gateway.channels]] + frequency=868100000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=868300000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=868500000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=867100000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=867300000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=867500000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=867700000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=867900000 + bandwidth=125000 + modulation="LORA" + spreading_factors=[7, 8, 9, 10, 11, 12] + + [[regions.gateway.channels]] + frequency=868300000 + bandwidth=250000 + modulation="LORA" + spreading_factors=[7] + + [[regions.gateway.channels]] + frequency=868800000 + bandwidth=125000 + modulation="FSK" + datarate=50000 + + + # Region specific network configuration. + [regions.network] + + # Installation margin (dB) used by the ADR engine. + # + # A higher number means that the network-server will keep more margin, + # resulting in a lower data-rate but decreasing the chance that the + # device gets disconnected because it is unable to reach one of the + # surrounded gateways. + installation_margin=10 + + # RX window (Class-A). + # + # Set this to: + # 0: RX1 / RX2 + # 1: RX1 only + # 2: RX2 only + rx_window=0 + + # RX1 delay (1 - 15 seconds). + rx1_delay=1 + + # RX1 data-rate offset + rx1_dr_offset=0 + + # RX2 data-rate + rx2_dr=0 + + # RX2 frequency (Hz) + rx2_frequency=869525000 + + # Prefer RX2 on RX1 data-rate less than. + # + # Prefer RX2 over RX1 based on the RX1 data-rate. When the RX1 data-rate + # is smaller than the configured value, then the Network Server will + # first try to schedule the downlink for RX2, failing that (e.g. the gateway + # has already a payload scheduled at the RX2 timing) it will try RX1. + rx2_prefer_on_rx1_dr_lt=0 + + # Prefer RX2 on link budget. + # + # When the link-budget is better for RX2 than for RX1, the Network Server will first + # try to schedule the downlink in RX2, failing that it will try RX1. + rx2_prefer_on_link_budget=false + + # Downlink TX Power (dBm) + # + # When set to -1, the downlink TX Power from the configured band will + # be used. + # + # Please consult the LoRaWAN Regional Parameters and local regulations + # for valid and legal options. Note that the configured TX Power must be + # supported by your gateway(s). + downlink_tx_power=-1 + + # ADR is disabled. + adr_disabled=false + + # Minimum data-rate. + min_dr=0 + + # Maximum data-rate. + max_dr=5 + + + # Rejoin-request configuration (LoRaWAN 1.1) + [regions.network.rejoin_request] + + # Request devices to periodically send rejoin-requests. + enabled=false + + # The device must send a rejoin-request type 0 at least every 2^(max_count_n + 4) + # uplink messages. Valid values are 0 to 15. + max_count_n=0 + + # The device must send a rejoin-request type 0 at least every 2^(max_time_n + 10) + # seconds. Valid values are 0 to 15. + # + # 0 = roughly 17 minutes + # 15 = about 1 year + max_time_n=0 + + + # Class-B configuration. + [regions.network.class_b] + + # Ping-slot data-rate. + ping_slot_dr=3 + + # Ping-slot frequency (Hz) + # + # set this to 0 to use the default frequency plan for the configured region + # (which could be frequency hopping). + ping_slot_frequency=0 + + + # Below is the common set of extra channels. Please make sure that these + # channels are also supported by the gateways. + [[regions.network.extra_channels]] + frequency=867100000 + min_dr=0 + max_dr=5 + + [[regions.network.extra_channels]] + frequency=867300000 + min_dr=0 + max_dr=5 + + [[regions.network.extra_channels]] + frequency=867500000 + min_dr=0 + max_dr=5 + + [[regions.network.extra_channels]] + frequency=867700000 + min_dr=0 + max_dr=5 + + [[regions.network.extra_channels]] + frequency=867900000 + min_dr=0 + max_dr=5 diff --git a/configuration/mosquitto/config/mosquitto.conf b/configuration/mosquitto/config/mosquitto.conf new file mode 100644 index 0000000..c8348ac --- /dev/null +++ b/configuration/mosquitto/config/mosquitto.conf @@ -0,0 +1,2 @@ +listener 1883 +allow_anonymous true diff --git a/configuration/postgresql/initdb/001-chirpstack_extensions.sh b/configuration/postgresql/initdb/001-chirpstack_extensions.sh new file mode 100644 index 0000000..4028a2d --- /dev/null +++ b/configuration/postgresql/initdb/001-chirpstack_extensions.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname="$POSTGRES_DB" <<-EOSQL + create extension pg_trgm; + create extension hstore; +EOSQL diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9d72e8b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,87 @@ +services: + chirpstack: + image: chirpstack/chirpstack:4 + command: -c /etc/chirpstack + restart: unless-stopped + volumes: + - ./configuration/chirpstack:/etc/chirpstack + depends_on: + - postgres + - mosquitto + - redis + environment: + - MQTT_BROKER_HOST=mosquitto + - REDIS_HOST=redis + - POSTGRESQL_HOST=postgres + ports: + - "8080:8080" + + chirpstack-gateway-bridge: + image: chirpstack/chirpstack-gateway-bridge:4 + restart: unless-stopped + ports: + - "1700:1700/udp" + volumes: + - ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge + environment: + - INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }} + - INTEGRATION__MQTT__STATE_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }} + - INTEGRATION__MQTT__COMMAND_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/command/# + depends_on: + - mosquitto + + chirpstack-gateway-bridge-basicstation: + image: chirpstack/chirpstack-gateway-bridge:4 + restart: unless-stopped + command: -c /etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml + ports: + - "3001:3001" + volumes: + - ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge + depends_on: + - mosquitto + + chirpstack-rest-api: + image: chirpstack/chirpstack-rest-api:4 + restart: unless-stopped + command: --server chirpstack:8080 --bind 0.0.0.0:8090 --insecure + ports: + - "8090:8090" + depends_on: + - chirpstack + + postgres: + image: postgres:14-alpine + restart: unless-stopped + volumes: + - ./configuration/postgresql/initdb:/docker-entrypoint-initdb.d + - postgresqldata:/var/lib/postgresql/data + environment: + - POSTGRES_USER=chirpstack + - POSTGRES_PASSWORD=chirpstack + - POSTGRES_DB=chirpstack + + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --save 300 1 --save 60 100 --appendonly no + volumes: + - redisdata:/data + + mosquitto: + image: eclipse-mosquitto:2 + restart: unless-stopped + ports: + - "1883:1883" + volumes: + - ./configuration/mosquitto/config/:/mosquitto/config/ + + application: + build: ./app + restart: unless-stopped + depends_on: + - mosquitto + +volumes: + postgresqldata: + redisdata: