From b318c3248526d8f29881127670bb0e2b7ec345b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniele=20Vigan=C3=B2?= Date: Sun, 21 Jun 2020 15:21:00 +0200 Subject: [PATCH] Implement MQTT over WebSocket support (#19) * Update README.md * Change default port in EEPROM * Add more docs * Implement MQTT over WS * Update diagrams * Update README.md and fix travis * Fix simulator.py * Fix a warning in .travis.yml * Sync travis with simulator.py * Fix serial in EDGE composer --- .travis.yml | 15 +- README.md | 67 +- arduino/README.md | 58 ++ arduino/eeprom_prog/eeprom_prog.ino | 2 +- docker/docker-compose.dev.yml | 5 + docker/docker-compose.yml | 2 + docker/edge/docker-compose.modules.yml | 14 + docker/ingress/nginx.conf | 19 +- docker/mqtt/mosquitto/mosquitto.conf | 988 +++++++++++++++++++++++++ docker/simulator/device_simulator.py | 10 +- docs/application_chart.odg | Bin 146282 -> 147411 bytes docs/application_chart.svg | 860 +++++++++++---------- 12 files changed, 1630 insertions(+), 410 deletions(-) create mode 100644 arduino/README.md create mode 100644 docker/mqtt/mosquitto/mosquitto.conf diff --git a/.travis.yml b/.travis.yml index 7f592ce..8236818 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ -language: python +os: linux dist: bionic +language: python services: - docker @@ -7,15 +8,15 @@ services: before_install: - pip -q install -U docker-compose -simulator: &simulator +_iot-simulator: &iot-simulator stage: simulator install: - docker-compose -f docker/docker-compose.yml pull - docker-compose -f docker/docker-compose.yml build before_script: - docker-compose -f docker/docker-compose.yml -f docker/edge/docker-compose.edge.yml up -d - - DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/docker-compose.yml -f docker/edge/docker-compose.edge.yml pull - - DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/docker-compose.yml -f docker/edge/docker-compose.edge.yml build + - DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/edge/docker-compose.modules.yml pull + - DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker/edge/docker-compose.modules.yml build script: - sleep 5 # warm-up - sed -i 's/# IOT_SERIAL/IOT_SERIAL/g' docker/edge/docker-compose.modules.yml @@ -41,7 +42,9 @@ jobs: - docker-compose -f docker/docker-compose.yml up -d script: - docker-compose -f docker/docker-compose.yml exec bite python manage.py test - - <<: *simulator + - <<: *iot-simulator env: IOT_TL=http - - <<: *simulator + - <<: *iot-simulator env: IOT_TL=mqtt + - <<: *iot-simulator + env: IOT_TL=ws diff --git a/README.md b/README.md index 916fba5..b3d7303 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,17 @@ Playing with IoT -This project is for educational purposes only. It does not implement any authentication and/or encryption protocol, so it is not suitable for real production. +This project is for educational purposes only. It does not implement any +authentication and/or encryption protocol, so it is not suitable for real +production. ![Application Schema](./docs/application_chart.svg) +### Future implementations + +- Broker HA via [Nginx stream module](http://nginx.org/en/docs/stream/ngx_stream_core_module.html) +- Stream analytics via [Apache Spark](https://spark.apache.org/) + ## Installation ### Requirements @@ -13,17 +20,23 @@ This project is for educational purposes only. It does not implement any authent - `docker-ce` or `moby` - `docker-compose` -The project is compatible with Docker for Windows (using Linux executors), but it is advised to directly use a minimal Linux VM instead (via the preferred hypervisor). +The project is compatible with Docker for Windows (using Linux executors), +but it is advised to directly use a minimal Linux VM instead +(via the preferred hypervisor). The application stack is composed by the following components: -- [Django](https://www.djangoproject.com/) with [Django REST framework](https://www.django-rest-framework.org/) web application (running via `gunicorn` in production mode) +- [Django](https://www.djangoproject.com/) with +[Django REST framework](https://www.django-rest-framework.org/) +web application (running via `gunicorn` in production mode) - `mqtt-to-db` custom daemon to dump telemetry into the timeseries database - telemetry payload is stored as json object (via PostgreSQL JSON data type) -- [Timescale](https://www.timescale.com/) DB, a [PostgreSQL](https://www.postgresql.org/) database with a timeseries extension +- [Timescale](https://www.timescale.com/) DB, +a [PostgreSQL](https://www.postgresql.org/) database with a timeseries extension - [Mosquitto](https://mosquitto.org/) MQTT broker (see alternatives below) - [Nginx](http://nginx.org/) as ingress for HTTP (see alternative below) -- [Chrony](https://chrony.tuxfamily.org/) as NTP server (with optional `MD5` encryption) +- [Chrony](https://chrony.tuxfamily.org/) as NTP server +(with optional `MD5` encryption) ## Deployment @@ -33,7 +46,8 @@ The application stack is composed by the following components: docker-compose -f docker/docker-compose.yml up -d [--scale {bite,mqtt-to-db)=N] ``` It exposes: -- `http://localhost:80` (HTTP) + +- `http://localhost:80` (HTTP and MQTT over Websockets) - `tcp://localhost:1883` (MQTT) - `udp://localhost:123` (NTP) @@ -44,22 +58,26 @@ Django runs with `DEBUG = True` and `SKIP_WHITELIST = True` ```bash docker-compose -f docker/docker-compose.yml -f docker-compose.dev.yml up -d [--scale {bite,mqtt-to-db)=N] ``` + It exposes: -- `http://localhost:80` (HTTP) + +- `http://localhost:80` (HTTP and MQTT over Websockets) - `http://localhost:8080` (Django's `runserver`) - `tcp://localhost:1883` (MQTT) +- `tcp://localhost:9001` (MQTT over Websockets) - `udp://localhost:123` (NTP) - `tcp://localhost:5432` (PostgreSQL/Timescale) Django runs with `DEBUG = True` and `SKIP_WHITELIST = True` -### Production +### Production (kind of) ```bash docker-compose -f docker/docker-compose.yml -f docker-compose.prod.yml up -d [--scale {bite,mqtt-to-db)=N] ``` It exposes: -- `http://localhost:80` (HTTP) + +- `http://localhost:80` (HTTP and MQTT over Websockets) - `tcp://localhost:1883` (MQTT) - `udp://localhost:123` (NTP) @@ -80,24 +98,28 @@ docker-compose -f docker/docker-compose.yml up -f docker/ingress/docker-compose. A ~8x memory usage can be expected compared to Mosquitto. -To use [VerneMQ](https://vernemq.com/) instead of Mosquitto use: +To use [VerneMQ](https://vernemq.com/) instead of Mosquitto run: ```bash docker-compose -f docker/docker-compose.yml up -f docker/mqtt/docker-compose.vernemq.yml -d ``` ### RabbitMQ -RabbitMQ does provides AMQP protocol too, but ingestion on the application side is not implemented yet. +RabbitMQ does provide AMQP protocol too, but ingestion on the application side +is not implemented yet. A ~10x memory usage can be expected compared to Mosquitto. -To use [RabbitMQ](https://www.rabbitmq.com/) (with the MQTT plugin enabled) instead of Mosquitto use: +To use [RabbitMQ](https://www.rabbitmq.com/) (with the MQTT plugin enabled) + instead of Mosquitto run: + ```bash docker-compose -f docker/docker-compose.yml up -f docker/mqtt/docker-compose.rabbitmq.yml -d ``` ## EDGE gateway simulation (via dind) -An EDGE gateway, with containers as modules, may be simulated via dind (docker-in-docker). +An EDGE gateway, with containers as modules, may be simulated via dind +(docker-in-docker). ### Start the EDGE @@ -108,18 +130,23 @@ docker-compose -f docker/docker-compose.yml up -f docker/edge/docker-compose.edg ### Run the modules inside the EDGE ```bash -DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker-compose.modules.yml up -d [--scale {device-http,device-mqtt}=N] +DOCKER_HOST='127.0.0.1:22375' docker-compose -f docker-compose.modules.yml up -d [--scale {device-http,device-ws,device-mqtt}=N] ``` ## Arduino -A simple Arduino UNO sketch is provided in the `arduino/tempLightSensor` folder. The sketch reads temperature and light from sensors. The simple schematic is: +A simple Arduino UNO sketch is provided in the `arduino/tempLightSensor` folder. +The sketch reads temperature and light from sensors. -![tempLightSensor](./arduino/tempLightSensor/tempLightSensor.svg) +[Read more ...](./arduino/README.md) -The sketch does require an Ethernet shield and a bunch of libraries which are available as git submodules under `arduino/libraries`. -Be advised that some libraries (notably the NTP one) are customized. +## Testing -Configuration parameters are stored and retrieved from the EEPROM. An helper sketch to update the EEPROM is available under `arduino/eeprom_prog` +Application tests are part of the Django suite: -An `ESP32` board (or similar Arduino) may be used, with some adaptions, too. +```bash +python manage.py test +``` + +End-to-End tests are performed via Travis-CI. See [`.travis.yml`](.travis.yml) +for further explanations. \ No newline at end of file diff --git a/arduino/README.md b/arduino/README.md new file mode 100644 index 0000000..a93dac3 --- /dev/null +++ b/arduino/README.md @@ -0,0 +1,58 @@ +# Arduino IoT device + +A simple Arduino UNO sketch is provided in the `arduino/tempLightSensor` folder. +The sketch reads temperature and light from sensors. + +The simple schematic is: + +![tempLightSensor](./tempLightSensor/tempLightSensor.svg) + +The sketch does require an Ethernet shield and a bunch of libraries which are +available as git submodules under `arduino/libraries`. + +```bash +git submodule update --init +``` + +Be advised that some libraries (notably the `NTP` one) have been customized. + +An `ESP32` board (or similar Arduino) may be used, with some adaptions, too. + +## EEPROM + +Configuration parameters are stored and retrieved from the `EEPROM`. +An helper sketch to update the `EEPROM` is available under +`arduino/eeprom_prog`. + +The data stored in the `EEPROM` is: + +```c +// Ethernet MAC address +const byte mac[6]; + +// Device serial number +const char serial[]; + +// IoT platform address and port +struct netConfig { + IPAddress address; + unsigned int port; +}; + +``` + +The `EEPROM` can be completely erased setting the `ERASE_FIRST` macro to `1`. + +```c +#define ERASE_FIRST 0 +``` + +## Firmware options + +The following macros are available in the firmware (to be set at compile time): + +```c +#define DEBUG_TO_SERIAL 1 // debug on serial port +#define USE_MQTT 1 // use mqtt protocol instead of http post +#define USE_INTERNAL_NTP 0 // use default ntp server or the internal one +``` \ No newline at end of file diff --git a/arduino/eeprom_prog/eeprom_prog.ino b/arduino/eeprom_prog/eeprom_prog.ino index 7127ea0..2a482ee 100644 --- a/arduino/eeprom_prog/eeprom_prog.ino +++ b/arduino/eeprom_prog/eeprom_prog.ino @@ -15,7 +15,7 @@ struct netConfig { netConfig config = { {192, 168, 10, 123}, - 8000 + 80 }; void setup() { diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 17098b4..60e70b0 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -5,6 +5,11 @@ services: ports: - "5432:5432" + broker: + ports: + - "1883:1883" + - "9001:9001" + bite: volumes: - ../bite:/srv/app/bite diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1838361..845f169 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -36,6 +36,8 @@ services: broker: <<: *service_default image: eclipse-mosquitto + volumes: + - "./mqtt/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf" networks: - net ports: diff --git a/docker/edge/docker-compose.modules.yml b/docker/edge/docker-compose.modules.yml index 7157150..7965a4e 100644 --- a/docker/edge/docker-compose.modules.yml +++ b/docker/edge/docker-compose.modules.yml @@ -18,6 +18,20 @@ services: IOT_DEBUG: 1 network_mode: "host" + device-ws: + <<: *service_default + build: + context: ../simulator + image: daniviga/bite-device-simulator + environment: + IOT_HTTP: "http://ingress" + IOT_MQTT: "ingress:80" + # IOT_SERIAL: "ws1234" + # IOT_DELAY: 10 + IOT_DEBUG: 1 + command: ["/opt/bite/device_simulator.py", "-t", "ws"] + network_mode: "host" + device-mqtt: <<: *service_default build: diff --git a/docker/ingress/nginx.conf b/docker/ingress/nginx.conf index 0b8bef3..ae019e3 100644 --- a/docker/ingress/nginx.conf +++ b/docker/ingress/nginx.conf @@ -1,4 +1,3 @@ - user nginx; worker_processes auto; @@ -25,12 +24,23 @@ http { keepalive_timeout 65; gzip off; + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + upstream bite { # We point to the Docker 'service' instead of directly to the container # Docker does then a DNS round-robin internally server bite:8000; } + upstream broker { + # We point to the Docker 'service' instead of directly to the container + # Docker does then a DNS round-robin internally + server broker:9001; + } + server { listen 80 default_server; listen [::]:80 default_server; @@ -51,6 +61,13 @@ http { proxy_connect_timeout 300; } + location /mqtt { + proxy_pass http://broker; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + location /static/ { root /srv/appdata/bite; } diff --git a/docker/mqtt/mosquitto/mosquitto.conf b/docker/mqtt/mosquitto/mosquitto.conf new file mode 100644 index 0000000..20ef26f --- /dev/null +++ b/docker/mqtt/mosquitto/mosquitto.conf @@ -0,0 +1,988 @@ +# Config file for mosquitto +# +# See mosquitto.conf(5) for more information. +# +# Default values are shown, uncomment to change. +# +# Use the # character to indicate a comment, but only if it is the +# very first character on the line. + +# ================================================================= +# General configuration +# ================================================================= + +# Use per listener security settings. +# +# It is recommended this option be set before any other options. +# +# If this option is set to true, then all authentication and access control +# options are controlled on a per listener basis. The following options are +# affected: +# +# password_file acl_file psk_file auth_plugin auth_opt_* allow_anonymous +# auto_id_prefix allow_zero_length_clientid +# +# Note that if set to true, then a durable client (i.e. with clean session set +# to false) that has disconnected will use the ACL settings defined for the +# listener that it was most recently connected to. +# +# The default behaviour is for this to be set to false, which maintains the +# setting behaviour from previous versions of mosquitto. +#per_listener_settings false + + +# If a client is subscribed to multiple subscriptions that overlap, e.g. foo/# +# and foo/+/baz , then MQTT expects that when the broker receives a message on +# a topic that matches both subscriptions, such as foo/bar/baz, then the client +# should only receive the message once. +# Mosquitto keeps track of which clients a message has been sent to in order to +# meet this requirement. The allow_duplicate_messages option allows this +# behaviour to be disabled, which may be useful if you have a large number of +# clients subscribed to the same set of topics and are very concerned about +# minimising memory usage. +# It can be safely set to true if you know in advance that your clients will +# never have overlapping subscriptions, otherwise your clients must be able to +# correctly deal with duplicate messages even when then have QoS=2. +#allow_duplicate_messages false + +# This option controls whether a client is allowed to connect with a zero +# length client id or not. This option only affects clients using MQTT v3.1.1 +# and later. If set to false, clients connecting with a zero length client id +# are disconnected. If set to true, clients will be allocated a client id by +# the broker. This means it is only useful for clients with clean session set +# to true. +#allow_zero_length_clientid true + +# If allow_zero_length_clientid is true, this option allows you to set a prefix +# to automatically generated client ids to aid visibility in logs. +# Defaults to 'auto-' +#auto_id_prefix auto- + +# This option affects the scenario when a client subscribes to a topic that has +# retained messages. It is possible that the client that published the retained +# message to the topic had access at the time they published, but that access +# has been subsequently removed. If check_retain_source is set to true, the +# default, the source of a retained message will be checked for access rights +# before it is republished. When set to false, no check will be made and the +# retained message will always be published. This affects all listeners. +#check_retain_source true + +# QoS 1 and 2 messages will be allowed inflight per client until this limit +# is exceeded. Defaults to 0. (No maximum) +# See also max_inflight_messages +#max_inflight_bytes 0 + +# The maximum number of QoS 1 and 2 messages currently inflight per +# client. +# This includes messages that are partway through handshakes and +# those that are being retried. Defaults to 20. Set to 0 for no +# maximum. Setting to 1 will guarantee in-order delivery of QoS 1 +# and 2 messages. +#max_inflight_messages 20 + +# For MQTT v5 clients, it is possible to have the server send a "server +# keepalive" value that will override the keepalive value set by the client. +# This is intended to be used as a mechanism to say that the server will +# disconnect the client earlier than it anticipated, and that the client should +# use the new keepalive value. The max_keepalive option allows you to specify +# that clients may only connect with keepalive less than or equal to this +# value, otherwise they will be sent a server keepalive telling them to use +# max_keepalive. This only applies to MQTT v5 clients. The maximum value +# allowable is 65535. Do not set below 10. +#max_keepalive 65535 + +# For MQTT v5 clients, it is possible to have the server send a "maximum packet +# size" value that will instruct the client it will not accept MQTT packets +# with size greater than max_packet_size bytes. This applies to the full MQTT +# packet, not just the payload. Setting this option to a positive value will +# set the maximum packet size to that number of bytes. If a client sends a +# packet which is larger than this value, it will be disconnected. This applies +# to all clients regardless of the protocol version they are using, but v3.1.1 +# and earlier clients will of course not have received the maximum packet size +# information. Defaults to no limit. Setting below 20 bytes is forbidden +# because it is likely to interfere with ordinary client operation, even with +# very small payloads. +#max_packet_size 0 + +# QoS 1 and 2 messages above those currently in-flight will be queued per +# client until this limit is exceeded. Defaults to 0. (No maximum) +# See also max_queued_messages. +# If both max_queued_messages and max_queued_bytes are specified, packets will +# be queued until the first limit is reached. +#max_queued_bytes 0 + +# The maximum number of QoS 1 and 2 messages to hold in a queue per client +# above those that are currently in-flight. Defaults to 100. Set +# to 0 for no maximum (not recommended). +# See also queue_qos0_messages. +# See also max_queued_bytes. +#max_queued_messages 100 +# +# This option sets the maximum number of heap memory bytes that the broker will +# allocate, and hence sets a hard limit on memory use by the broker. Memory +# requests that exceed this value will be denied. The effect will vary +# depending on what has been denied. If an incoming message is being processed, +# then the message will be dropped and the publishing client will be +# disconnected. If an outgoing message is being sent, then the individual +# message will be dropped and the receiving client will be disconnected. +# Defaults to no limit. +#memory_limit 0 + +# This option sets the maximum publish payload size that the broker will allow. +# Received messages that exceed this size will not be accepted by the broker. +# The default value is 0, which means that all valid MQTT messages are +# accepted. MQTT imposes a maximum payload size of 268435455 bytes. +#message_size_limit 0 + +# This option allows persistent clients (those with clean session set to false) +# to be removed if they do not reconnect within a certain time frame. +# +# This is a non-standard option in MQTT V3.1 but allowed in MQTT v3.1.1. +# +# Badly designed clients may set clean session to false whilst using a randomly +# generated client id. This leads to persistent clients that will never +# reconnect. This option allows these clients to be removed. +# +# The expiration period should be an integer followed by one of h d w m y for +# hour, day, week, month and year respectively. For example +# +# persistent_client_expiration 2m +# persistent_client_expiration 14d +# persistent_client_expiration 1y +# +# The default if not set is to never expire persistent clients. +#persistent_client_expiration + +# Write process id to a file. Default is a blank string which means +# a pid file shouldn't be written. +# This should be set to /var/run/mosquitto.pid if mosquitto is +# being run automatically on boot with an init script and +# start-stop-daemon or similar. +#pid_file + +# Set to true to queue messages with QoS 0 when a persistent client is +# disconnected. These messages are included in the limit imposed by +# max_queued_messages and max_queued_bytes +# Defaults to false. +# This is a non-standard option for the MQTT v3.1 spec but is allowed in +# v3.1.1. +#queue_qos0_messages false + +# Set to false to disable retained message support. If a client publishes a +# message with the retain bit set, it will be disconnected if this is set to +# false. +#retain_available true + +# Disable Nagle's algorithm on client sockets. This has the effect of reducing +# latency of individual messages at the potential cost of increasing the number +# of packets being sent. +#set_tcp_nodelay false + +# Time in seconds between updates of the $SYS tree. +# Set to 0 to disable the publishing of the $SYS tree. +#sys_interval 10 + +# The MQTT specification requires that the QoS of a message delivered to a +# subscriber is never upgraded to match the QoS of the subscription. Enabling +# this option changes this behaviour. If upgrade_outgoing_qos is set true, +# messages sent to a subscriber will always match the QoS of its subscription. +# This is a non-standard option explicitly disallowed by the spec. +#upgrade_outgoing_qos false + +# When run as root, drop privileges to this user and its primary +# group. +# Set to root to stay as root, but this is not recommended. +# If run as a non-root user, this setting has no effect. +# Note that on Windows this has no effect and so mosquitto should +# be started by the user you wish it to run as. +#user mosquitto + +# ================================================================= +# Default listener +# ================================================================= + +# IP address/hostname to bind the default listener to. If not +# given, the default listener will not be bound to a specific +# address and so will be accessible to all network interfaces. +# bind_address ip-address/host name +#bind_address + +# Port to use for the default listener. +port 1883 + +# Bind the listener to a specific interface. This is similar to +# bind_address above but is useful when an interface has multiple addresses or +# the address may change. It is valid to use this with the bind_address option, +# but take care that the interface you are binding to contains the address you +# are binding to, otherwise you will not be able to connect. +# Example: bind_interface eth0 +#bind_interface + +# When a listener is using the websockets protocol, it is possible to serve +# http data as well. Set http_dir to a directory which contains the files you +# wish to serve. If this option is not specified, then no normal http +# connections will be possible. +#http_dir + +# The maximum number of client connections to allow. This is +# a per listener setting. +# Default is -1, which means unlimited connections. +# Note that other process limits mean that unlimited connections +# are not really possible. Typically the default maximum number of +# connections possible is around 1024. +#max_connections -1 + +# Choose the protocol to use when listening. +# This can be either mqtt or websockets. +# Websockets support is currently disabled by default at compile time. +# Certificate based TLS may be used with websockets, except that +# only the cafile, certfile, keyfile and ciphers options are supported. +protocol mqtt + +# Set use_username_as_clientid to true to replace the clientid that a client +# connected with with its username. This allows authentication to be tied to +# the clientid, which means that it is possible to prevent one client +# disconnecting another by using the same clientid. +# If a client connects with no username it will be disconnected as not +# authorised when this option is set to true. +# Do not use in conjunction with clientid_prefixes. +# See also use_identity_as_username. +#use_username_as_clientid + +# ----------------------------------------------------------------- +# Certificate based SSL/TLS support +# ----------------------------------------------------------------- +# The following options can be used to enable SSL/TLS support for +# this listener. Note that the recommended port for MQTT over TLS +# is 8883, but this must be set manually. +# +# See also the mosquitto-tls man page. + +# At least one of cafile or capath must be defined. They both +# define methods of accessing the PEM encoded Certificate +# Authority certificates that have signed your server certificate +# and that you wish to trust. +# cafile defines the path to a file containing the CA certificates. +# capath defines a directory that will be searched for files +# containing the CA certificates. For capath to work correctly, the +# certificate files must have ".crt" as the file ending and you must run +# "openssl rehash " each time you add/remove a certificate. +#cafile +#capath + +# Path to the PEM encoded server certificate. +#certfile + +# Path to the PEM encoded keyfile. +#keyfile + + +# If you have require_certificate set to true, you can create a certificate +# revocation list file to revoke access to particular client certificates. If +# you have done this, use crlfile to point to the PEM encoded revocation file. +#crlfile + +# If you wish to control which encryption ciphers are used, use the ciphers +# option. The list of available ciphers can be obtained using the "openssl +# ciphers" command and should be provided in the same format as the output of +# that command. +# If unset defaults to DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:@STRENGTH +#ciphers DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:@STRENGTH + +# To allow the use of ephemeral DH key exchange, which provides forward +# security, the listener must load DH parameters. This can be specified with +# the dhparamfile option. The dhparamfile can be generated with the command +# e.g. "openssl dhparam -out dhparam.pem 2048" +#dhparamfile + +# By default a TLS enabled listener will operate in a similar fashion to a +# https enabled web server, in that the server has a certificate signed by a CA +# and the client will verify that it is a trusted certificate. The overall aim +# is encryption of the network traffic. By setting require_certificate to true, +# the client must provide a valid certificate in order for the network +# connection to proceed. This allows access to the broker to be controlled +# outside of the mechanisms provided by MQTT. +#require_certificate false + +# This option defines the version of the TLS protocol to use for this listener. +# The default value allows all of v1.3, v1.2 and v1.1. The valid values are +# tlsv1.3 tlsv1.2 and tlsv1.1. +#tls_version + +# If require_certificate is true, you may set use_identity_as_username to true +# to use the CN value from the client certificate as a username. If this is +# true, the password_file option will not be used for this listener. +# This takes priority over use_subject_as_username. +# See also use_subject_as_username. +#use_identity_as_username false + +# If require_certificate is true, you may set use_subject_as_username to true +# to use the complete subject value from the client certificate as a username. +# If this is true, the password_file option will not be used for this listener. +# See also use_identity_as_username +#use_subject_as_username false + +# ----------------------------------------------------------------- +# Pre-shared-key based SSL/TLS support +# ----------------------------------------------------------------- +# The following options can be used to enable PSK based SSL/TLS support for +# this listener. Note that the recommended port for MQTT over TLS is 8883, but +# this must be set manually. +# +# See also the mosquitto-tls man page and the "Certificate based SSL/TLS +# support" section. Only one of certificate or PSK encryption support can be +# enabled for any listener. + +# The psk_hint option enables pre-shared-key support for this listener and also +# acts as an identifier for this listener. The hint is sent to clients and may +# be used locally to aid authentication. The hint is a free form string that +# doesn't have much meaning in itself, so feel free to be creative. +# If this option is provided, see psk_file to define the pre-shared keys to be +# used or create a security plugin to handle them. +#psk_hint + +# When using PSK, the encryption ciphers used will be chosen from the list of +# available PSK ciphers. If you want to control which ciphers are available, +# use the "ciphers" option. The list of available ciphers can be obtained +# using the "openssl ciphers" command and should be provided in the same format +# as the output of that command. +#ciphers + +# Set use_identity_as_username to have the psk identity sent by the client used +# as its username. Authentication will be carried out using the PSK rather than +# the MQTT username/password and so password_file will not be used for this +# listener. +#use_identity_as_username false + + +# ================================================================= +# Extra listeners +# ================================================================= + +# Listen on a port/ip address combination. By using this variable +# multiple times, mosquitto can listen on more than one port. If +# this variable is used and neither bind_address nor port given, +# then the default listener will not be started. +# The port number to listen on must be given. Optionally, an ip +# address or host name may be supplied as a second argument. In +# this case, mosquitto will attempt to bind the listener to that +# address and so restrict access to the associated network and +# interface. By default, mosquitto will listen on all interfaces. +# Note that for a websockets listener it is not possible to bind to a host +# name. +# listener port-number [ip address/host name] +listener 9001 + +# Bind the listener to a specific interface. This is similar to +# the [ip address/host name] part of the listener definition, but is useful +# when an interface has multiple addresses or the address may change. It is +# valid to use this with the [ip address/host name] part of the listener +# definition, but take care that the interface you are binding to contains the +# address you are binding to, otherwise you will not be able to connect. +# Only available on Linux and requires elevated privileges. +# +# Example: bind_interface eth0 +#bind_interface + +# When a listener is using the websockets protocol, it is possible to serve +# http data as well. Set http_dir to a directory which contains the files you +# wish to serve. If this option is not specified, then no normal http +# connections will be possible. +#http_dir + +# The maximum number of client connections to allow. This is +# a per listener setting. +# Default is -1, which means unlimited connections. +# Note that other process limits mean that unlimited connections +# are not really possible. Typically the default maximum number of +# connections possible is around 1024. +#max_connections -1 + +# The listener can be restricted to operating within a topic hierarchy using +# the mount_point option. This is achieved be prefixing the mount_point string +# to all topics for any clients connected to this listener. This prefixing only +# happens internally to the broker; the client will not see the prefix. +#mount_point + +# Choose the protocol to use when listening. +# This can be either mqtt or websockets. +# Certificate based TLS may be used with websockets, except that only the +# cafile, certfile, keyfile and ciphers options are supported. +protocol websockets + +# Set use_username_as_clientid to true to replace the clientid that a client +# connected with with its username. This allows authentication to be tied to +# the clientid, which means that it is possible to prevent one client +# disconnecting another by using the same clientid. +# If a client connects with no username it will be disconnected as not +# authorised when this option is set to true. +# Do not use in conjunction with clientid_prefixes. +# See also use_identity_as_username. +#use_username_as_clientid + +# Change the websockets headers size. This is a global option, it is not +# possible to set per listener. This option sets the size of the buffer used in +# the libwebsockets library when reading HTTP headers. If you are passing large +# header data such as cookies then you may need to increase this value. If left +# unset, or set to 0, then the default of 1024 bytes will be used. +#websockets_headers_size + +# ----------------------------------------------------------------- +# Certificate based SSL/TLS support +# ----------------------------------------------------------------- +# The following options can be used to enable certificate based SSL/TLS support +# for this listener. Note that the recommended port for MQTT over TLS is 8883, +# but this must be set manually. +# +# See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS +# support" section. Only one of certificate or PSK encryption support can be +# enabled for any listener. + +# At least one of cafile or capath must be defined to enable certificate based +# TLS encryption. They both define methods of accessing the PEM encoded +# Certificate Authority certificates that have signed your server certificate +# and that you wish to trust. +# cafile defines the path to a file containing the CA certificates. +# capath defines a directory that will be searched for files +# containing the CA certificates. For capath to work correctly, the +# certificate files must have ".crt" as the file ending and you must run +# "openssl rehash " each time you add/remove a certificate. +#cafile +#capath + +# Path to the PEM encoded server certificate. +#certfile + +# Path to the PEM encoded keyfile. +#keyfile + + +# If you wish to control which encryption ciphers are used, use the ciphers +# option. The list of available ciphers can be optained using the "openssl +# ciphers" command and should be provided in the same format as the output of +# that command. +#ciphers + +# If you have require_certificate set to true, you can create a certificate +# revocation list file to revoke access to particular client certificates. If +# you have done this, use crlfile to point to the PEM encoded revocation file. +#crlfile + +# To allow the use of ephemeral DH key exchange, which provides forward +# security, the listener must load DH parameters. This can be specified with +# the dhparamfile option. The dhparamfile can be generated with the command +# e.g. "openssl dhparam -out dhparam.pem 2048" +#dhparamfile + +# By default an TLS enabled listener will operate in a similar fashion to a +# https enabled web server, in that the server has a certificate signed by a CA +# and the client will verify that it is a trusted certificate. The overall aim +# is encryption of the network traffic. By setting require_certificate to true, +# the client must provide a valid certificate in order for the network +# connection to proceed. This allows access to the broker to be controlled +# outside of the mechanisms provided by MQTT. +#require_certificate false + +# If require_certificate is true, you may set use_identity_as_username to true +# to use the CN value from the client certificate as a username. If this is +# true, the password_file option will not be used for this listener. +#use_identity_as_username false + +# ----------------------------------------------------------------- +# Pre-shared-key based SSL/TLS support +# ----------------------------------------------------------------- +# The following options can be used to enable PSK based SSL/TLS support for +# this listener. Note that the recommended port for MQTT over TLS is 8883, but +# this must be set manually. +# +# See also the mosquitto-tls man page and the "Certificate based SSL/TLS +# support" section. Only one of certificate or PSK encryption support can be +# enabled for any listener. + +# The psk_hint option enables pre-shared-key support for this listener and also +# acts as an identifier for this listener. The hint is sent to clients and may +# be used locally to aid authentication. The hint is a free form string that +# doesn't have much meaning in itself, so feel free to be creative. +# If this option is provided, see psk_file to define the pre-shared keys to be +# used or create a security plugin to handle them. +#psk_hint + +# When using PSK, the encryption ciphers used will be chosen from the list of +# available PSK ciphers. If you want to control which ciphers are available, +# use the "ciphers" option. The list of available ciphers can be optained +# using the "openssl ciphers" command and should be provided in the same format +# as the output of that command. +#ciphers + +# Set use_identity_as_username to have the psk identity sent by the client used +# as its username. Authentication will be carried out using the PSK rather than +# the MQTT username/password and so password_file will not be used for this +# listener. +#use_identity_as_username false + + +# ================================================================= +# Persistence +# ================================================================= + +# If persistence is enabled, save the in-memory database to disk +# every autosave_interval seconds. If set to 0, the persistence +# database will only be written when mosquitto exits. See also +# autosave_on_changes. +# Note that writing of the persistence database can be forced by +# sending mosquitto a SIGUSR1 signal. +#autosave_interval 1800 + +# If true, mosquitto will count the number of subscription changes, retained +# messages received and queued messages and if the total exceeds +# autosave_interval then the in-memory database will be saved to disk. +# If false, mosquitto will save the in-memory database to disk by treating +# autosave_interval as a time in seconds. +#autosave_on_changes false + +# Save persistent message data to disk (true/false). +# This saves information about all messages, including +# subscriptions, currently in-flight messages and retained +# messages. +# retained_persistence is a synonym for this option. +#persistence false + +# The filename to use for the persistent database, not including +# the path. +#persistence_file mosquitto.db + +# Location for persistent database. Must include trailing / +# Default is an empty string (current directory). +# Set to e.g. /var/lib/mosquitto/ if running as a proper service on Linux or +# similar. +#persistence_location + + +# ================================================================= +# Logging +# ================================================================= + +# Places to log to. Use multiple log_dest lines for multiple +# logging destinations. +# Possible destinations are: stdout stderr syslog topic file +# +# stdout and stderr log to the console on the named output. +# +# syslog uses the userspace syslog facility which usually ends up +# in /var/log/messages or similar. +# +# topic logs to the broker topic '$SYS/broker/log/', +# where severity is one of D, E, W, N, I, M which are debug, error, +# warning, notice, information and message. Message type severity is used by +# the subscribe/unsubscribe log_types and publishes log messages to +# $SYS/broker/log/M/susbcribe or $SYS/broker/log/M/unsubscribe. +# +# The file destination requires an additional parameter which is the file to be +# logged to, e.g. "log_dest file /var/log/mosquitto.log". The file will be +# closed and reopened when the broker receives a HUP signal. Only a single file +# destination may be configured. +# +# Note that if the broker is running as a Windows service it will default to +# "log_dest none" and neither stdout nor stderr logging is available. +# Use "log_dest none" if you wish to disable logging. +#log_dest stderr + +# Types of messages to log. Use multiple log_type lines for logging +# multiple types of messages. +# Possible types are: debug, error, warning, notice, information, +# none, subscribe, unsubscribe, websockets, all. +# Note that debug type messages are for decoding the incoming/outgoing +# network packets. They are not logged in "topics". +#log_type error +#log_type warning +#log_type notice +#log_type information + + +# If set to true, client connection and disconnection messages will be included +# in the log. +#connection_messages true + +# If using syslog logging (not on Windows), messages will be logged to the +# "daemon" facility by default. Use the log_facility option to choose which of +# local0 to local7 to log to instead. The option value should be an integer +# value, e.g. "log_facility 5" to use local5. +#log_facility + +# If set to true, add a timestamp value to each log message. +#log_timestamp true + +# Set the format of the log timestamp. If left unset, this is the number of +# seconds since the Unix epoch. +# This is a free text string which will be passed to the strftime function. To +# get an ISO 8601 datetime, for example: +# log_timestamp_format %Y-%m-%dT%H:%M:%S +#log_timestamp_format + +# Change the websockets logging level. This is a global option, it is not +# possible to set per listener. This is an integer that is interpreted by +# libwebsockets as a bit mask for its lws_log_levels enum. See the +# libwebsockets documentation for more details. "log_type websockets" must also +# be enabled. +#websockets_log_level 0 + + +# ================================================================= +# Security +# ================================================================= + +# If set, only clients that have a matching prefix on their +# clientid will be allowed to connect to the broker. By default, +# all clients may connect. +# For example, setting "secure-" here would mean a client "secure- +# client" could connect but another with clientid "mqtt" couldn't. +#clientid_prefixes + +# Boolean value that determines whether clients that connect +# without providing a username are allowed to connect. If set to +# false then a password file should be created (see the +# password_file option) to control authenticated client access. +# +# Defaults to true if no other security options are set. If `password_file` or +# `psk_file` is set, or if an authentication plugin is loaded which implements +# username/password or TLS-PSK checks, then `allow_anonymous` defaults to +# false. +# +#allow_anonymous true + +# ----------------------------------------------------------------- +# Default authentication and topic access control +# ----------------------------------------------------------------- + +# Control access to the broker using a password file. This file can be +# generated using the mosquitto_passwd utility. If TLS support is not compiled +# into mosquitto (it is recommended that TLS support should be included) then +# plain text passwords are used, in which case the file should be a text file +# with lines in the format: +# username:password +# The password (and colon) may be omitted if desired, although this +# offers very little in the way of security. +# +# See the TLS client require_certificate and use_identity_as_username options +# for alternative authentication options. If an auth_plugin is used as well as +# password_file, the auth_plugin check will be made first. +#password_file + +# Access may also be controlled using a pre-shared-key file. This requires +# TLS-PSK support and a listener configured to use it. The file should be text +# lines in the format: +# identity:key +# The key should be in hexadecimal format without a leading "0x". +# If an auth_plugin is used as well, the auth_plugin check will be made first. +#psk_file + +# Control access to topics on the broker using an access control list +# file. If this parameter is defined then only the topics listed will +# have access. +# If the first character of a line of the ACL file is a # it is treated as a +# comment. +# Topic access is added with lines of the format: +# +# topic [read|write|readwrite] +# +# The access type is controlled using "read", "write" or "readwrite". This +# parameter is optional (unless contains a space character) - if not +# given then the access is read/write. can contain the + or # +# wildcards as in subscriptions. +# +# The first set of topics are applied to anonymous clients, assuming +# allow_anonymous is true. User specific topic ACLs are added after a +# user line as follows: +# +# user +# +# The username referred to here is the same as in password_file. It is +# not the clientid. +# +# +# If is also possible to define ACLs based on pattern substitution within the +# topic. The patterns available for substition are: +# +# %c to match the client id of the client +# %u to match the username of the client +# +# The substitution pattern must be the only text for that level of hierarchy. +# +# The form is the same as for the topic keyword, but using pattern as the +# keyword. +# Pattern ACLs apply to all users even if the "user" keyword has previously +# been given. +# +# If using bridges with usernames and ACLs, connection messages can be allowed +# with the following pattern: +# pattern write $SYS/broker/connection/%c/state +# +# pattern [read|write|readwrite] +# +# Example: +# +# pattern write sensor/%u/data +# +# If an auth_plugin is used as well as acl_file, the auth_plugin check will be +# made first. +#acl_file + +# ----------------------------------------------------------------- +# External authentication and topic access plugin options +# ----------------------------------------------------------------- + +# External authentication and access control can be supported with the +# auth_plugin option. This is a path to a loadable plugin. See also the +# auth_opt_* options described below. +# +# The auth_plugin option can be specified multiple times to load multiple +# plugins. The plugins will be processed in the order that they are specified +# here. If the auth_plugin option is specified alongside either of +# password_file or acl_file then the plugin checks will be made first. +# +#auth_plugin + +# If the auth_plugin option above is used, define options to pass to the +# plugin here as described by the plugin instructions. All options named +# using the format auth_opt_* will be passed to the plugin, for example: +# +# auth_opt_db_host +# auth_opt_db_port +# auth_opt_db_username +# auth_opt_db_password + + +# ================================================================= +# Bridges +# ================================================================= + +# A bridge is a way of connecting multiple MQTT brokers together. +# Create a new bridge using the "connection" option as described below. Set +# options for the bridges using the remaining parameters. You must specify the +# address and at least one topic to subscribe to. +# +# Each connection must have a unique name. +# +# The address line may have multiple host address and ports specified. See +# below in the round_robin description for more details on bridge behaviour if +# multiple addresses are used. Note that if you use an IPv6 address, then you +# are required to specify a port. +# +# The direction that the topic will be shared can be chosen by +# specifying out, in or both, where the default value is out. +# The QoS level of the bridged communication can be specified with the next +# topic option. The default QoS level is 0, to change the QoS the topic +# direction must also be given. +# +# The local and remote prefix options allow a topic to be remapped when it is +# bridged to/from the remote broker. This provides the ability to place a topic +# tree in an appropriate location. +# +# For more details see the mosquitto.conf man page. +# +# Multiple topics can be specified per connection, but be careful +# not to create any loops. +# +# If you are using bridges with cleansession set to false (the default), then +# you may get unexpected behaviour from incoming topics if you change what +# topics you are subscribing to. This is because the remote broker keeps the +# subscription for the old topic. If you have this problem, connect your bridge +# with cleansession set to true, then reconnect with cleansession set to false +# as normal. +#connection +#address [:] [[:]] +#topic [[[out | in | both] qos-level] local-prefix remote-prefix] + + +# If a bridge has topics that have "out" direction, the default behaviour is to +# send an unsubscribe request to the remote broker on that topic. This means +# that changing a topic direction from "in" to "out" will not keep receiving +# incoming messages. Sending these unsubscribe requests is not always +# desirable, setting bridge_attempt_unsubscribe to false will disable sending +# the unsubscribe request. +#bridge_attempt_unsubscribe true + +# Set the version of the MQTT protocol to use with for this bridge. Can be one +# of mqttv311 or mqttv11. Defaults to mqttv311. +#bridge_protocol_version mqttv311 + +# Set the clean session variable for this bridge. +# When set to true, when the bridge disconnects for any reason, all +# messages and subscriptions will be cleaned up on the remote +# broker. Note that with cleansession set to true, there may be a +# significant amount of retained messages sent when the bridge +# reconnects after losing its connection. +# When set to false, the subscriptions and messages are kept on the +# remote broker, and delivered when the bridge reconnects. +#cleansession false + +# Set the amount of time a bridge using the lazy start type must be idle before +# it will be stopped. Defaults to 60 seconds. +#idle_timeout 60 + +# Set the keepalive interval for this bridge connection, in +# seconds. +#keepalive_interval 60 + +# Set the clientid to use on the local broker. If not defined, this defaults to +# 'local.'. If you are bridging a broker to itself, it is important +# that local_clientid and clientid do not match. +#local_clientid + +# If set to true, publish notification messages to the local and remote brokers +# giving information about the state of the bridge connection. Retained +# messages are published to the topic $SYS/broker/connection//state +# unless the notification_topic option is used. +# If the message is 1 then the connection is active, or 0 if the connection has +# failed. +# This uses the last will and testament feature. +#notifications true + +# Choose the topic on which notification messages for this bridge are +# published. If not set, messages are published on the topic +# $SYS/broker/connection//state +#notification_topic + +# Set the client id to use on the remote end of this bridge connection. If not +# defined, this defaults to 'name.hostname' where name is the connection name +# and hostname is the hostname of this computer. +# This replaces the old "clientid" option to avoid confusion. "clientid" +# remains valid for the time being. +#remote_clientid + +# Set the password to use when connecting to a broker that requires +# authentication. This option is only used if remote_username is also set. +# This replaces the old "password" option to avoid confusion. "password" +# remains valid for the time being. +#remote_password + +# Set the username to use when connecting to a broker that requires +# authentication. +# This replaces the old "username" option to avoid confusion. "username" +# remains valid for the time being. +#remote_username + +# Set the amount of time a bridge using the automatic start type will wait +# until attempting to reconnect. +# This option can be configured to use a constant delay time in seconds, or to +# use a backoff mechanism based on "Decorrelated Jitter", which adds a degree +# of randomness to when the restart occurs. +# +# Set a constant timeout of 20 seconds: +# restart_timeout 20 +# +# Set backoff with a base (start value) of 10 seconds and a cap (upper limit) of +# 60 seconds: +# restart_timeout 10 30 +# +# Defaults to jitter with a base of 5 and cap of 30 +#restart_timeout 5 30 + +# If the bridge has more than one address given in the address/addresses +# configuration, the round_robin option defines the behaviour of the bridge on +# a failure of the bridge connection. If round_robin is false, the default +# value, then the first address is treated as the main bridge connection. If +# the connection fails, the other secondary addresses will be attempted in +# turn. Whilst connected to a secondary bridge, the bridge will periodically +# attempt to reconnect to the main bridge until successful. +# If round_robin is true, then all addresses are treated as equals. If a +# connection fails, the next address will be tried and if successful will +# remain connected until it fails +#round_robin false + +# Set the start type of the bridge. This controls how the bridge starts and +# can be one of three types: automatic, lazy and once. Note that RSMB provides +# a fourth start type "manual" which isn't currently supported by mosquitto. +# +# "automatic" is the default start type and means that the bridge connection +# will be started automatically when the broker starts and also restarted +# after a short delay (30 seconds) if the connection fails. +# +# Bridges using the "lazy" start type will be started automatically when the +# number of queued messages exceeds the number set with the "threshold" +# parameter. It will be stopped automatically after the time set by the +# "idle_timeout" parameter. Use this start type if you wish the connection to +# only be active when it is needed. +# +# A bridge using the "once" start type will be started automatically when the +# broker starts but will not be restarted if the connection fails. +#start_type automatic + +# Set the number of messages that need to be queued for a bridge with lazy +# start type to be restarted. Defaults to 10 messages. +# Must be less than max_queued_messages. +#threshold 10 + +# If try_private is set to true, the bridge will attempt to indicate to the +# remote broker that it is a bridge not an ordinary client. If successful, this +# means that loop detection will be more effective and that retained messages +# will be propagated correctly. Not all brokers support this feature so it may +# be necessary to set try_private to false if your bridge does not connect +# properly. +#try_private true + +# ----------------------------------------------------------------- +# Certificate based SSL/TLS support +# ----------------------------------------------------------------- +# Either bridge_cafile or bridge_capath must be defined to enable TLS support +# for this bridge. +# bridge_cafile defines the path to a file containing the +# Certificate Authority certificates that have signed the remote broker +# certificate. +# bridge_capath defines a directory that will be searched for files containing +# the CA certificates. For bridge_capath to work correctly, the certificate +# files must have ".crt" as the file ending and you must run "openssl rehash +# " each time you add/remove a certificate. +#bridge_cafile +#bridge_capath + + +# If the remote broker has more than one protocol available on its port, e.g. +# MQTT and WebSockets, then use bridge_alpn to configure which protocol is +# requested. Note that WebSockets support for bridges is not yet available. +#bridge_alpn + +# When using certificate based encryption, bridge_insecure disables +# verification of the server hostname in the server certificate. This can be +# useful when testing initial server configurations, but makes it possible for +# a malicious third party to impersonate your server through DNS spoofing, for +# example. Use this option in testing only. If you need to resort to using this +# option in a production environment, your setup is at fault and there is no +# point using encryption. +#bridge_insecure false + +# Path to the PEM encoded client certificate, if required by the remote broker. +#bridge_certfile + +# Path to the PEM encoded client private key, if required by the remote broker. +#bridge_keyfile + +# ----------------------------------------------------------------- +# PSK based SSL/TLS support +# ----------------------------------------------------------------- +# Pre-shared-key encryption provides an alternative to certificate based +# encryption. A bridge can be configured to use PSK with the bridge_identity +# and bridge_psk options. These are the client PSK identity, and pre-shared-key +# in hexadecimal format with no "0x". Only one of certificate and PSK based +# encryption can be used on one +# bridge at once. +#bridge_identity +#bridge_psk + + +# ================================================================= +# External config files +# ================================================================= + +# External configuration files may be included by using the +# include_dir option. This defines a directory that will be searched +# for config files. All files that end in '.conf' will be loaded as +# a configuration file. It is best to have this as the last option +# in the main file. This option will only be processed from the main +# configuration file. The directory specified must not contain the +# main configuration file. +# Files within include_dir will be loaded sorted in case-sensitive +# alphabetical order, with capital letters ordered first. If this option is +# given multiple times, all of the files from the first instance will be +# processed before the next instance. See the man page for examples. +#include_dir diff --git a/docker/simulator/device_simulator.py b/docker/simulator/device_simulator.py index ad394db..2c3c20d 100755 --- a/docker/simulator/device_simulator.py +++ b/docker/simulator/device_simulator.py @@ -37,7 +37,7 @@ def post_json(endpoint, url, data): sleep(10) # retry in 10 seconds -def publish_json(endpoint, data): +def publish_json(transport, endpoint, data): json_data = json.dumps(data) serial = data['device'] @@ -52,6 +52,7 @@ def publish_json(endpoint, data): hostname=endpoint.split(':')[0], port=int(endpoint.split(':')[1]), client_id=serial, + transport=('websockets' if transport == 'ws' else 'tcp'), # auth=auth FIXME ) @@ -69,7 +70,7 @@ def main(): '127.0.0.1:1883'), help='IoT MQTT endpoint') parser.add_argument('-t', '--transport', - choices=['mqtt', 'http'], + choices=['mqtt', 'ws', 'http'], default=os.environ.get('IOT_TL', 'http'), help='IoT transport layer') parser.add_argument('-s', '--serial', @@ -103,8 +104,9 @@ def main(): } if args.transport == 'http': post_json(args.endpoint, telemetry, {**data, 'payload': payload}) - elif args.transport == 'mqtt': - publish_json(args.mqtt, {**data, 'payload': payload}) + elif args.transport in ('mqtt', 'ws'): + publish_json( + args.transport, args.mqtt, {**data, 'payload': payload}) else: raise NotImplementedError sleep(args.delay) diff --git a/docs/application_chart.odg b/docs/application_chart.odg index 126ce84a0dea617a97805173a4836c0d17bd6104..b611cfca0fea4fa0b0d18fba56f76e69d10d3d5d 100644 GIT binary patch delta 22685 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 delta 21451 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 diff --git a/docs/application_chart.svg b/docs/application_chart.svg index 4e76361..133878e 100644 --- a/docs/application_chart.svg +++ b/docs/application_chart.svg @@ -13,11 +13,22 @@ + + + + + + + + + + + @@ -58,7 +69,7 @@ - + @@ -105,387 +116,279 @@ + + + + + + + - - - - - - - - MQTT BROKER - - - - - - Ingress + + + + + + MQTT BROKER - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ... + + + + Ingress - - - + + + + + + SQL - - - - - - SQL - - - - - - - - MQTTtoDB + + + + MQTTtoDB - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + ... - - - - - - - - - - - - - - - - - - - - ... + + + + Web App(REST API) - - - - - - - - - - - - - - - - - - - - ... + + + + + + + + + + + + + + + + + + + + ... - + - - - - Web App(REST API) + + + + MQTT - + - - - - - - - - - - - - - - - - - - - - ... + + + + + HTTP - + - - - - MQTT + + + + IoTHTTP - + - - - + + + + IoTMQTT - + - - - - - HTTP + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IoT... - + - - - - IoTHTTP + + + + + HTTP - + - - - - IoTMQTT + + + + MQTT + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - IoT... - - - - - - - - - HTTP - - - - - - - - MQTT - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -493,26 +396,21 @@ - - - - - - HTTP - - - - - - + + + + + + HTTP - - - - + + + + + UDP @@ -522,53 +420,259 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + IoTMQTT + + + + + + + + + HTTP + + + + + + + + MQTT WS + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Web App(REST API) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ... + + + + + + + + WebSockets