diff --git a/.busted b/.busted
new file mode 100644
index 0000000..b8c1e0b
--- /dev/null
+++ b/.busted
@@ -0,0 +1,10 @@
+return {
+ default = {
+ ROOT = { "tests/spec" },
+ lpath = "./?.lua;./?/?.lua;./?/init.lua",
+ helper = "./tests/configure-log.lua",
+ verbose = true,
+ coverage = false,
+ output = "gtest",
+ },
+}
diff --git a/.editorconfig b/.editorconfig
index 1f7c63c..3125997 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -7,6 +7,8 @@ root = true
[*]
end_of_line = lf
insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
# 4 space tab indentation for lua files
[*.lua]
@@ -20,3 +22,6 @@ indent_size = 2
[*.yml]
indent_style = space
indent_size = 2
+
+[Makefile]
+indent_style = tab
diff --git a/.gitignore b/.gitignore
index 1bc6e22..43f6a90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
local/
luacov.stats.out
luacov.report.out
+examples/openresty/logs/*.log
*.swp
diff --git a/.luacheckrc b/.luacheckrc
index f4aa5c4..4a1bf9e 100644
--- a/.luacheckrc
+++ b/.luacheckrc
@@ -8,21 +8,22 @@
max_line_length = 200
-std = "min"
-
-files["mqtt/**"] = {
- ignore = {
- "113/unpack",
- "212/.+_", -- unused argument value_
- }
+not_globals = {
+ "string.len",
+ "table.getn",
}
-files["tests/spec/**"] = {
- ignore = {
- "113/describe",
- "113/it",
- "143/assert",
- }
+include_files = {
+ "**/*.lua",
+ "*.rockspec",
+ ".busted",
+ ".luacheckrc",
}
+files["tests/spec/**/*.lua"] = { std = "+busted" }
+files["examples/openresty/**/*.lua"] = { std = "+ngx_lua" }
+files["mqtt/loop/detect.lua"] = { std = "+ngx_lua" }
+files["mqtt/loop/nginx.lua"] = { std = "+ngx_lua" }
+files["mqtt/connector/nginx.lua"] = { std = "+ngx_lua" }
+
-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/.markdownlint.yaml b/.markdownlint.yaml
index d95827a..1cc0dda 100644
--- a/.markdownlint.yaml
+++ b/.markdownlint.yaml
@@ -1,3 +1,4 @@
default: true
single-title: false
line-length: false
+code-block-style: false
diff --git a/LICENSE b/LICENSE
index 1bd5a40..0233293 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
-MIT License
+# MIT License
-Copyright (c) 2018 Alexander Kiranov
+Copyright (c) 2018-2021 Alexander Kiranov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 215a884..91d563f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# luamqtt - Pure-lua MQTT v3.1.1 and v5.0 client
+# luamqtt
+
+Pure-lua MQTT v3.1.1 and v5.0 client

@@ -17,12 +19,22 @@ This library is written in **pure-lua** to provide maximum portability.
* Full MQTT v3.1.1 client-side support
* Full MQTT v5.0 client-side support
-* Several long-living MQTT clients in one script thanks to ioloop
+* Support for Copas, OpenResty/Nginx, and an included lightweight ioloop.
+
+# Installation
+
+From LuaRocks:
+
+ luarocks install luamqtt
+
+[More details](./docs_topics/01-installation.md)
# Documentation
See [https://xhaskx.github.io/luamqtt/](https://xhaskx.github.io/luamqtt/)
+[More details](./docs_topics/README.md)
+
# Forum
See [flespi forum thread](https://forum.flespi.com/d/97-luamqtt-mqtt-client-written-in-pure-lua)
@@ -31,139 +43,32 @@ See [flespi forum thread](https://forum.flespi.com/d/97-luamqtt-mqtt-client-writ
[https://github.com/xHasKx/luamqtt](https://github.com/xHasKx/luamqtt)
-# Dependencies
-
-The only main dependency is a [**luasocket**](https://luarocks.org/modules/luasocket/luasocket) to establishing TCP connection to the MQTT broker. Install it like this:
+# Bugs & contributing
-```sh
-luarocks install luasocket
-```
+Please [file a GitHub issue](https://github.com/xHasKx/luamqtt/issues) if you found any bug.
-On Lua 5.1 it also depends on [**LuaBitOp**](http://bitop.luajit.org/) (**bit**) library to perform bitwise operations.
-It's not listed in package dependencies, please install it manually like this:
+And of course, any contribution are welcome!
-```sh
-luarocks install luabitop
-```
+# Tests
-## luasec (SSL/TLS)
+To run tests in this git repo you need [**busted**](https://luarocks.org/modules/olivine-labs/busted) as well as some dependencies:
-To establish secure network connection (SSL/TSL) to MQTT broker
-you also need [**luasec**](https://github.com/brunoos/luasec) module, please install it manually like this:
+Prepare:
```sh
+luarocks install busted
+luarocks install luacov
+luarocks install luasocket
luarocks install luasec
+luarocks install copas
+luarocks install lualogging
+luarocks install ansicolors
```
-This stage is optional and may be skipped if you don't need the secure network connection (e.g. broker is located in your local network).
-
-# Lua versions
-
-It's tested to work on Debian 9 GNU/Linux with Lua versions:
-
-* Lua 5.1 ... Lua 5.4 (**i.e. any modern Lua version**)
-* LuaJIT 2.0.0 ... LuaJIT 2.1.0 beta3
-* It may also work on other Lua versions without any guarantees
-
-Also I've successfully run it under **Windows** and it was ok, but installing luarock-modules may be a non-trivial task on this OS.
-
-# Installation
-
-As the luamqtt is almost zero-dependency you have to install required Lua libraries by yourself, before using the luamqtt library:
-
-```sh
-luarocks install luasocket # optional if you will use your own connectors (see below)
-luarocks install luabitop # you don't need this for lua 5.3 and above
-luarocks install luasec # you don't need this if you don't want to use SSL connections
-```
-
-Then you may install the luamqtt library itself:
-
-```sh
-luarocks install luamqtt
-```
-
-Or for development purposes;
-
-```sh
-# development branch:
-luarocks install luamqtt --dev
-
-# or from the cloned repo:
-luarocks make
-```
-
-[LuaRocks page](http://luarocks.org/modules/xhaskx/luamqtt)
-
-# Examples
-
-Here is a short version of [`examples/simple.lua`](examples/simple.lua):
-
-```lua
--- load mqtt library
-local mqtt = require("mqtt")
-
--- create MQTT client, flespi tokens info: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
-local client = mqtt.client{ uri = "mqtt.flespi.io", username = os.getenv("FLESPI_TOKEN"), clean = true }
-
--- assign MQTT client event handlers
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
- end
-
- -- connection established, now subscribe to test topic and publish a message after
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function()
- assert(client:publish{ topic = "luamqtt/simpletest", payload = "hello" })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- -- receive one message and disconnect
- print("received message", msg)
- client:disconnect()
- end,
-}
-
--- run ioloop for client
-mqtt.run_ioloop(client)
-```
-
-More examples placed in [`examples/`](examples/) directory. Also checkout tests in [`tests/spec/mqtt-client.lua`](tests/spec/mqtt-client.lua)
-
-Also you can learn MQTT protocol by reading [`tests/spec/protocol4-make.lua`](tests/spec/protocol4-make.lua) and [`tests/spec/protocol4-parse.lua`](tests/spec/protocol4-parse.lua) tests
-
-# Connectors
-
-Connector is a network connection layer for luamqtt. There is a three standard connectors included:
-
-* [`luasocket`](mqtt/luasocket.lua)
-* [`luasocket_ssl`](mqtt/luasocket_ssl.lua)
-* [`ngxsocket`](mqtt/ngxsocket.lua) - for using in [openresty environment](examples/openresty)
-
-The `luasocket` or `luasocket_ssl` connector will be used by default, if not specified, according `secure=true/false` option per MQTT client.
-
-In simple terms, connector is a set of functions to establish a network stream (TCP connection usually) and send/receive data through it.
-Every MQTT client instance may have their own connector.
-
-And it's very simple to implement your own connector to make luamqtt works in your environment.
-
-# Bugs & contributing
-
-Please [file a GitHub issue](https://github.com/xHasKx/luamqtt/issues) if you found any bug.
-
-And of course, any contribution are welcome!
-
-# Tests
-
-To run tests in this git repo you need [**busted**](https://luarocks.org/modules/olivine-labs/busted):
+Running the tests:
```sh
-busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/*.lua
+busted
```
There is a script to run all tests for all supported lua versions, using [hererocks](https://github.com/mpeterv/hererocks):
@@ -180,7 +85,7 @@ To collect code coverage stats - install luacov using luarocks and then execute:
```sh
# collect stats during tests
-busted -v -e 'package.path="./?/init.lua;./?.lua;"..package.path;require("luacov.runner")(".luacov")' tests/spec/*.lua
+busted --coverage
# generate report into luacov.report.out file
luacov
diff --git a/docs/config.ld b/docs/config.ld
index 19827bf..050ed24 100644
--- a/docs/config.ld
+++ b/docs/config.ld
@@ -1,7 +1,21 @@
-- usage:
-- execute `ldoc .` in this docs directory
-file = {"../mqtt/init.lua", "../mqtt/const.lua", "../mqtt/client.lua", "../mqtt/ioloop.lua", "../mqtt/protocol.lua"}
+file = {
+ "../mqtt/init.lua",
+ "../mqtt/client.lua",
+ "../mqtt/ioloop.lua",
+
+ "../mqtt/loop/init.lua",
+ "../mqtt/loop/copas.lua",
+ "../mqtt/loop/nginx.lua",
+ "../mqtt/loop/ioloop.lua",
+
+ "../mqtt/connector/init.lua",
+ "../mqtt/connector/copas.lua",
+ "../mqtt/connector/nginx.lua",
+ "../mqtt/connector/luasocket.lua",
+}
project = "luamqtt"
package = "mqtt"
dir = "."
@@ -12,12 +26,18 @@ full_description = "Source code: https://github.com/xHasKx/luamqtt"
examples = {
"../examples/simple.lua",
- "../examples/sync.lua",
"../examples/mqtt5-simple.lua",
- "../examples/copas-example.lua",
+ "../examples/copas.lua",
+ "../examples/openresty/app/openresty.lua",
}
-topics = {"../README.md", "../LICENSE"}
+use_markdown_titles = true
+topics = {
+ "../README.md",
+ "../LICENSE",
+ "../docs_topics/",
+}
format = "markdown"
plain = true
+style = true
diff --git a/docs/ldoc.css b/docs/ldoc.css
index 52c4ad2..fac66f4 100644
--- a/docs/ldoc.css
+++ b/docs/ldoc.css
@@ -120,6 +120,7 @@ pre {
margin: 10px 0 10px 0;
overflow: auto;
font-family: "Andale Mono", monospace;
+ tab-size: 2;
}
pre.example {
diff --git a/docs_topics/01-installation.md b/docs_topics/01-installation.md
new file mode 100644
index 0000000..eead3b5
--- /dev/null
+++ b/docs_topics/01-installation.md
@@ -0,0 +1,15 @@
+# Installation
+
+As luamqtt is almost zero-dependency you have to install any optional Lua libraries by
+yourself, before using the luamqtt library.
+
+When installing using [LuaRocks](http://luarocks.org/modules/xhaskx/luamqtt), the
+LuaSocket dependency will automatically be installed as well, as it is a listed dependency
+in the rockspec.
+
+ luarocks install luamqtt
+
+To install from source clone the repo and make sure the `./mqtt/` folder is in your
+Lua search path.
+
+Check the [dependencies](./02-dependencies.md) on how (and when) to install those.
diff --git a/docs_topics/02-dependencies.md b/docs_topics/02-dependencies.md
new file mode 100644
index 0000000..118ba1e
--- /dev/null
+++ b/docs_topics/02-dependencies.md
@@ -0,0 +1,36 @@
+# Dependencies
+
+The dependencies differ slightly based on the environment you use, and the requirements you have:
+
+* [**luasocket**](https://luarocks.org/modules/luasocket/luasocket) to establish TCP connections to the MQTT broker.
+ This is a listed dependency in the luamqtt rockspec, so it will automatically be installed if you use LuaRocks to
+ install luamqtt. To install it manually:
+
+ luarocks install luasocket
+
+* _[optional dependency]_ [**copas**](https://github.com/keplerproject/copas) module for asynchoneous IO. Copas is an advanced co-routine
+ scheduler with far more features than the included `ioloop`. For anything more than a few devices, or for devices which
+ require network IO beyond mqtt alone, Copas is the better alternative. Copas is also pure-Lua, but has parallel network
+ IO (as opposed to sequential network IO in `ioloop`), and has features like; threads, timers, locks, semaphores, and
+ non-blocking clients for http(s), (s)ftp, and smtp.
+
+ luarocks install copas
+
+* _[optional dependency]_ [**luasec**](https://github.com/brunoos/luasec) module for SSL/TLS based connections. This is optional and may be
+ skipped if you don't need secure network connections (e.g. broker is located in your local network). It's not listed
+ in package dependencies, please install it manually like this:
+
+ luarocks install luasec
+
+* _[optional dependency]_ [**LuaBitOp**](http://bitop.luajit.org/) library to perform bitwise operations, which is required only on
+ Lua 5.1. It's not listed in package dependencies, please install it manually like this:
+
+ luarocks install luabitop
+
+* _[optional dependency]_ [**LuaLogging**](https://github.com/lunarmodules/lualogging/) to enable logging by the MQTT client. This is optional
+ but highly recommended for long running clients. This is a great debugging aid when developing your clients. Also when
+ using OpenResty as your runtime, you'll definitely want to use this, see
+ [openresty.lua](../examples/openresty/app/openresty.lua) for an example.
+ It's not listed in package dependencies, please install it manually like this:
+
+ luarocks install lualogging
diff --git a/docs_topics/03-lua_versions.md b/docs_topics/03-lua_versions.md
new file mode 100644
index 0000000..9496cd9
--- /dev/null
+++ b/docs_topics/03-lua_versions.md
@@ -0,0 +1,11 @@
+# Lua versions
+
+It's tested to work on Debian 9 GNU/Linux with Lua versions:
+
+* Lua 5.1 ... Lua 5.4 (**i.e. any modern Lua version**)
+* LuaJIT 2.0.0 ... LuaJIT 2.1.0 beta3
+* It may also work on other Lua versions without any guarantees
+
+So basically it should work on any modern OS with Lua interpreter available.
+
+Also has run under **Windows** and it was ok, but installing luarocks-modules may be a non-trivial task on this OS.
diff --git a/docs_topics/04-mqtt_versions.md b/docs_topics/04-mqtt_versions.md
new file mode 100644
index 0000000..db10c1b
--- /dev/null
+++ b/docs_topics/04-mqtt_versions.md
@@ -0,0 +1,8 @@
+# MQTT versions
+
+Currently supported versions:
+
+* [MQTT v3.1.1 protocol](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html) version.
+* [MQTT v5.0 protocol](http://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html) version.
+
+Both protocols have full control packets support.
diff --git a/docs_topics/05-connectors.md b/docs_topics/05-connectors.md
new file mode 100644
index 0000000..8575278
--- /dev/null
+++ b/docs_topics/05-connectors.md
@@ -0,0 +1,27 @@
+# Connectors
+
+A connector is a network connection layer for luamqtt. This ensures clean separation between the socket
+implementation and the client/protocol implementation.
+
+By default luamqtt ships with connectors for `ioloop`, `copas`, and `nginx`. It will auto-detect which
+one to use using the `mqtt.loop` module.
+
+## building your own
+
+If you have a different socket implementation you can write your own connector.
+
+There are 2 base-classes `mqtt.connector.base.buffered-base` and `mqtt.connector.base.non-buffered-base`
+to build on, which to pick depends on the environment.
+
+The main question is what event/io loop mechanism does your implementation have?
+
+* a single main (co)routine that runs, and doesn't yield when doing network IO. In this case
+ you should use the `buffered_base` and read on sockets with a `0` timeout. Check the
+ `mqtt.connector.luasocket` implementation for an example (this is what `ioloop` uses).
+
+* multiple co-routines that run within a scheduler, and doing non-blocking network IO (receive/send
+ will implicitly yield control to the scheduler so it will run other tasks until the socket is ready).
+ This is what Copas and Nginx do, and it requires the `non_buffered_base`.
+
+The main thing to look for when checking out the existing implementations is the network timeout settings,
+and the returned `signals`.
diff --git a/docs_topics/README.md b/docs_topics/README.md
new file mode 100644
index 0000000..221fa39
--- /dev/null
+++ b/docs_topics/README.md
@@ -0,0 +1,11 @@
+# Short documentation topics
+
+[Installation](./01-installation.md)
+
+[Dependencies](./02-dependencies.md)
+
+[Lua versions](./03-lua_versions.md)
+
+[MQTT versions](./04-mqtt_versions.md)
+
+[Connectors](./05-connectors.md)
diff --git a/examples/copas-example.lua b/examples/copas-example.lua
index c22e2af..51a24fa 100644
--- a/examples/copas-example.lua
+++ b/examples/copas-example.lua
@@ -2,89 +2,86 @@
local mqtt = require("mqtt")
local copas = require("copas")
-local mqtt_ioloop = require("mqtt.ioloop")
+local add_client = require("mqtt.loop").add
local num_pings = 10 -- total number of ping-pongs
-local timeout = 1 -- timeout between ping-pongs
+local delay = 1 -- delay between ping-pongs
local suffix = tostring(math.random(1000000)) -- mqtt topic suffix to distinct simultaneous running of this script
-- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
local token = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9"
local ping = mqtt.client{
- uri = "mqtt.flespi.io",
+ uri = "mqtt://mqtt.flespi.io",
username = token,
clean = true,
version = mqtt.v50,
+
+ -- create event handlers
+ on = {
+ connect = function(connack, self)
+ assert(connack.rc == 0)
+ print("ping connected")
+
+ -- adding another thread; copas handlers should return quickly, anything
+ -- that can wait should be off-loaded from the handler to a thread.
+ -- Especially anything that yields; socket reads/writes and sleeps, and the
+ -- code below does both, sleeping, and writing (implicit in 'publish')
+ copas.addthread(function()
+ for i = 1, num_pings do
+ copas.pause(delay)
+ print("ping", i)
+ assert(self:publish{ topic = "luamqtt/copas-ping/"..suffix, payload = "ping"..i, qos = 1 })
+ end
+
+ print("ping done")
+ assert(self:publish{ topic = "luamqtt/copas-ping/"..suffix, payload = "done", qos = 1 })
+ self:disconnect()
+ end)
+ end,
+ error = function(err)
+ print("ping MQTT client error:", err)
+ end,
+ }, -- close 'on', event handlers
}
local pong = mqtt.client{
- uri = "mqtt.flespi.io",
+ uri = "mqtt://mqtt.flespi.io",
username = token,
clean = true,
version = mqtt.v50,
-}
-
-ping:on{
- connect = function(connack)
- assert(connack.rc == 0)
- print("ping connected")
-
- for i = 1, num_pings do
- copas.sleep(timeout)
- print("ping", i)
- assert(ping:publish{ topic = "luamqtt/copas-ping/"..suffix, payload = "ping"..i, qos = 1 })
- end
-
- copas.sleep(timeout)
-
- print("ping done")
- assert(ping:publish{ topic = "luamqtt/copas-ping/"..suffix, payload = "done", qos = 1 })
- ping:disconnect()
- end,
- error = function(err)
- print("ping MQTT client error:", err)
- end,
-}
-pong:on{
- connect = function(connack)
- assert(connack.rc == 0)
- print("pong connected")
-
- assert(pong:subscribe{ topic="luamqtt/copas-ping/"..suffix, qos=1, callback=function(suback)
- assert(suback.rc[1] > 0)
- print("pong subscribed")
- end })
- end,
-
- message = function(msg)
- print("pong: received", msg.payload)
- assert(pong:acknowledge(msg))
-
- if msg.payload == "done" then
- print("pong done")
- pong:disconnect()
- end
- end,
- error = function(err)
- print("pong MQTT client error:", err)
- end,
+ -- create event handlers
+ on = {
+ connect = function(connack, self)
+ assert(connack.rc == 0)
+ print("pong connected")
+
+ assert(self:subscribe{ topic="luamqtt/copas-ping/"..suffix, qos=1, callback=function(suback)
+ assert(suback.rc[1] > 0)
+ print("pong subscribed")
+ end })
+ end,
+
+ message = function(msg, self)
+ print("pong: received", msg.payload)
+ assert(self:acknowledge(msg))
+
+ if msg.payload == "done" then
+ print("pong done")
+ self:disconnect()
+ end
+ end,
+ error = function(err)
+ print("pong MQTT client error:", err)
+ end,
+ }, -- close 'on', event handlers
}
print("running copas loop...")
-copas.addthread(function()
- local ioloop = mqtt_ioloop.create{ sleep = 0.01, sleep_function = copas.sleep }
- ioloop:add(ping)
- ioloop:run_until_clients()
-end)
-
-copas.addthread(function()
- local ioloop = mqtt_ioloop.create{ sleep = 0.01, sleep_function = copas.sleep }
- ioloop:add(pong)
- ioloop:run_until_clients()
-end)
+add_client(ping)
+add_client(pong)
copas.loop()
diff --git a/examples/copas.lua b/examples/copas.lua
index 199e49a..14aea68 100644
--- a/examples/copas.lua
+++ b/examples/copas.lua
@@ -1,76 +1,61 @@
-- load mqtt module
local mqtt = require("mqtt")
local copas = require("copas")
+local add_client = require("mqtt.loop").add
-- create mqtt client
local client = mqtt.client{
-- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
-- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
+ uri = "mqtt://mqtt.flespi.io",
-- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
clean = true,
- -- NOTE: copas connector
- connector = require("mqtt.luasocket-copas"),
-}
-print("created MQTT client", client)
-
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
+ -- create event handlers
+ on = {
+ connect = function(connack, self)
+ if connack.rc ~= 0 then
+ print("connection to broker failed:", connack:reason_string(), connack)
+ return
+ end
+ print("connected:", connack) -- successful connection
+
+ -- subscribe to test topic and publish message after it
+ assert(self:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
+ print("subscribed:", suback)
+
+ -- publish test message
+ print('publishing test message "hello" to "luamqtt/simpletest" topic...')
+ assert(self:publish{
+ topic = "luamqtt/simpletest",
+ payload = "hello",
+ qos = 1
+ })
+ end})
+ end,
+
+ message = function(msg, self)
+ assert(self:acknowledge(msg))
+
+ print("received:", msg)
+ print("disconnecting...")
+ assert(self:disconnect())
+ end,
+
+ error = function(err)
+ print("MQTT client error:", err)
+ end,
+
+ close = function()
+ print("MQTT conn closed")
end
- print("connected:", connack) -- successful connection
-
- -- subscribe to test topic and publish message after it
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
- print("subscribed:", suback)
-
- -- publish test message
- print('publishing test message "hello" to "luamqtt/simpletest" topic...')
- assert(client:publish{
- topic = "luamqtt/simpletest",
- payload = "hello",
- qos = 1
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- print("received:", msg)
- print("disconnecting...")
- assert(client:disconnect())
- end,
-
- error = function(err)
- print("MQTT client error:", err)
- end,
-
- close = function()
- print("MQTT conn closed")
- end
+ }, -- close 'on', event handlers
}
--- run io loop for client until connection close
-copas.addthread(function()
- print("running client in separated copas thread #1...")
- mqtt.run_sync(client)
-
- -- NOTE: in sync mode no automatic reconnect is working, but you may just wrap "mqtt.run_sync(client)" call in a loop like this:
- -- while true do
- -- mqtt.run_sync(client)
- -- end
-end)
-
-copas.addthread(function()
- print("execution of separated copas thread #2...")
- copas.sleep(0.1)
- print("thread #2 stopped")
-end)
+print("created MQTT client", client)
+add_client(client)
copas.loop()
+
print("done, copas loop is stopped")
diff --git a/examples/last-will/client-1.lua b/examples/last-will/client-1.lua
index 586d426..d8c607b 100644
--- a/examples/last-will/client-1.lua
+++ b/examples/last-will/client-1.lua
@@ -5,43 +5,43 @@ local client = mqtt.client{
id = "luamqtt-example-will-1",
-- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
-- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
+ uri = "mqtts://mqtt.flespi.io",
-- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
clean = true,
- secure = true,
-- specifying last will message
will = {
topic = "luamqtt/lost",
payload = "client-1 connection lost last will message",
},
-}
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
- end
- print("connected:", connack)
+ -- event handlers
+ on = {
+ connect = function(connack, self)
+ if connack.rc ~= 0 then
+ print("connection to broker failed:", connack:reason_string(), connack)
+ return
+ end
+ print("connected:", connack)
- -- subscribe to topic when we are expecting connection close command from client-2
- assert(client:subscribe{ topic="luamqtt/close", qos=1, callback=function()
- print("subscribed to luamqtt/close, waiting for connection close command from client-2")
- end})
- end,
+ -- subscribe to topic when we are expecting connection close command from client-2
+ assert(self:subscribe{ topic="luamqtt/close", qos=1, callback=function()
+ print("subscribed to luamqtt/close, waiting for connection close command from client-2")
+ end})
+ end,
- message = function(msg)
- assert(client:acknowledge(msg))
+ message = function(msg, self)
+ assert(self:acknowledge(msg))
- print("received:", msg)
- print("closing connection without DISCONNECT and stopping client-1")
- client:close_connection() -- will message should be sent
- end,
+ print("received:", msg)
+ print("closing connection without DISCONNECT and stopping client-1")
+ self:close_connection() -- will message should be sent
+ end,
- error = function(err)
- print("MQTT client error:", err)
- end,
+ error = function(err)
+ print("MQTT client error:", err)
+ end,
+ }
}
-- start receive loop
diff --git a/examples/last-will/client-2.lua b/examples/last-will/client-2.lua
index 81e29b3..7c9988c 100644
--- a/examples/last-will/client-2.lua
+++ b/examples/last-will/client-2.lua
@@ -5,46 +5,46 @@ local client = mqtt.client{
id = "luamqtt-example-will-2",
-- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
-- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
+ uri = "mqtts://mqtt.flespi.io",
-- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
clean = true,
- secure = true,
-}
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
- end
- print("connected:", connack)
-
- -- subscribe to topic when we are expecting last-will message from client-1
- assert(client:subscribe{ topic="luamqtt/lost", qos=1, callback=function()
- print("subscribed to luamqtt/lost")
-
- -- publish close command to client-1
- assert(client:publish{
- topic = "luamqtt/close",
- payload = "Dear client-1, please close your connection",
- qos = 1,
- })
- print("published close command")
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- print("received:", msg)
- print("disconnecting and stopping client-2")
- client:disconnect()
- end,
-
- error = function(err)
- print("MQTT client error:", err)
- end,
+ -- event handlers
+ on = {
+ connect = function(connack, self)
+ if connack.rc ~= 0 then
+ print("connection to broker failed:", connack:reason_string(), connack)
+ return
+ end
+ print("connected:", connack)
+
+ -- subscribe to topic when we are expecting last-will message from client-1
+ assert(self:subscribe{ topic="luamqtt/lost", qos=1, callback=function()
+ print("subscribed to luamqtt/lost")
+
+ -- publish close command to client-1
+ assert(self:publish{
+ topic = "luamqtt/close",
+ payload = "Dear client-1, please close your connection",
+ qos = 1,
+ })
+ print("published close command")
+ end})
+ end,
+
+ message = function(msg, self)
+ assert(self:acknowledge(msg))
+
+ print("received:", msg)
+ print("disconnecting and stopping client-2")
+ self:disconnect()
+ end,
+
+ error = function(err)
+ print("MQTT client error:", err)
+ end,
+ }
}
-- start receive loop
diff --git a/examples/mqtt5-simple.lua b/examples/mqtt5-simple.lua
index 2c85b98..9cd4434 100644
--- a/examples/mqtt5-simple.lua
+++ b/examples/mqtt5-simple.lua
@@ -2,56 +2,58 @@ local mqtt = require("mqtt")
-- create mqtt client
local client = mqtt.client{
- uri = "mqtt.flespi.io",
+ uri = "mqtt://mqtt.flespi.io",
-- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
clean = true,
version = mqtt.v50,
-}
-print("created MQTT v5.0 client:", client)
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
- end
- print("connected:", connack) -- successful connection
-
- -- subscribe to test topic and publish message after it
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
- print("subscribed:", suback)
-
- -- publish test message
- print('publishing test message "hello" to "luamqtt/simpletest" topic...')
- assert(client:publish{
- topic = "luamqtt/simpletest",
- payload = "hello",
- qos = 1,
- properties = {
- payload_format_indicator = 1,
- content_type = "text/plain",
- },
- user_properties = {
- hello = "world",
- },
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- print("received:", msg)
- print("disconnecting...")
- assert(client:disconnect())
- end,
-
- error = function(err)
- print("MQTT client error:", err)
- end,
+ -- create event handlers
+ on = {
+ connect = function(connack, self)
+ if connack.rc ~= 0 then
+ print("connection to broker failed:", connack:reason_string(), connack)
+ return
+ end
+ print("connected:", connack) -- successful connection
+
+ -- subscribe to test topic and publish message after it
+ assert(self:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
+ print("subscribed:", suback)
+
+ -- publish test message
+ print('publishing test message "hello" to "luamqtt/simpletest" topic...')
+ assert(self:publish{
+ topic = "luamqtt/simpletest",
+ payload = "hello",
+ qos = 1,
+ properties = {
+ payload_format_indicator = 1,
+ content_type = "text/plain",
+ },
+ user_properties = {
+ hello = "world",
+ },
+ })
+ end})
+ end,
+
+ message = function(msg, self)
+ assert(self:acknowledge(msg))
+
+ print("received:", msg)
+ print("disconnecting...")
+ assert(self:disconnect())
+ end,
+
+ error = function(err)
+ print("MQTT client error:", err)
+ end,
+ }, -- close 'on', event handlers
}
+
+print("created MQTT v5.0 client:", client)
print("running ioloop for it")
mqtt.run_ioloop(client)
diff --git a/examples/openresty/README.md b/examples/openresty/README.md
index 9f8bd92..c8d3a03 100644
--- a/examples/openresty/README.md
+++ b/examples/openresty/README.md
@@ -1,29 +1,29 @@
# openresty example
-Provided example is based on [the official Getting Started article](https://openresty.org/en/getting-started.html).
+Openresty is primarily a server, and accepts incoming connections.
+This means that running an MQTT client inside OpenResty will require
+some tricks.
-There is a two ways to run MQTT client in openresty:
+Since OpenResty sockets cannot pass a context boundary (without being
+closed), and we need a background task listening on the socket, we're
+creating a timer context, and then handle everything from within that
+context.
-* in synchronous mode
-* in ioloop mode
+In the timer we'll spawn a thread that will do the listening, and the
+timer itself will go in an endless loop to do the keepalives.
-# synchronous mode
+# Caveats
-Started MQTT client is connecting, subscribing and waiting for incoming MQTT publications as you code it, without any magic asynchronous work.
-
-**Caveats**: The keep_alive feature will not work as there is no way for MQTT client to break its receive() operation in keep_alive interval and send PINGREQ packet to MQTT broker to maintain connection. It may lead to disconnects from MQTT broker side in absence of traffic in opened MQTT connection. After disconnecting from broker there is a way to reconnect using openresty's timer.
-
-# ioloop mode
-
-Started MQTT client is connecting, subscribing and waiting for incoming MQTT publications as you code it, maintaining established connection using PINGREQ packets to broker in configured keep_alive interval.
-
-**Caveats**: own luamqtt's ioloop is based on the ability of sockets to timeout its receive() operation, allowing MQTT client to awake in some configured interval and send PINGREQ packet to broker to maintain opened connection, but on every timeout the openresty is writing such in its error.log:
-
- stream lua tcp socket read timed out, context: ngx.timer
+* Due to the socket limitation we cannot Publish anything from another
+ context. If you run into "bad request" errors on socket operations, you
+ are probably accessing a socket from another context.
+* In the long run, timers do leak memory, since timer contexts are
+ supposed to be short-lived. Consider implementing a secondary mechanism
+ to restart the timer-context and restart the client.
# Files
-* [conf/nginx.conf](conf/nginx.conf): configuration for the nginx daemon to run lua script
-* [app/main-sync.lua](app/main-sync.lua): example lua script maintaining connection to some MQTT broker, in synchronous mode
-* [app/main-ioloop.lua](app/main-ioloop.lua): example lua script maintaining connection to some MQTT broker, in ioloop mode
-* start.sh, stop.sh, restart.sh: optional scripts to manage openresty instance
+* [conf/nginx.conf](conf/nginx.conf): configuration for the nginx daemon to run lua scripts
+* [app/openresty.lua](app/openresty.lua): example lua script maintaining connection
+* [mqtt/loop/nginx.lua](../../mqtt/loop/nginx.lua): how to add a client in an Nginx environment
+* `start.sh`, `stop.sh`, `quit.sh`, `restart.sh`: optional scripts to manage the OpenResty instance
diff --git a/examples/openresty/app/main-ioloop.lua b/examples/openresty/app/main-ioloop.lua
deleted file mode 100644
index 86f4552..0000000
--- a/examples/openresty/app/main-ioloop.lua
+++ /dev/null
@@ -1,96 +0,0 @@
--- luacheck: globals ngx
-local log = ngx.log
-local timer_at = ngx.timer.at
-local ERR = ngx.ERR
-local tbl_concat = table.concat
-
-local function trace(...)
- local line = {}
- for i = 1, select("#", ...) do
- line[i] = tostring(select(i, ...))
- end
- log(ERR, tbl_concat(line, " "))
-end
-
-trace("main.lua started")
-
-local start_timer
-
-local function on_timer(...)
- trace("on_timer: ", ...)
-
- local mqtt = require("mqtt")
- local ioloop = require("mqtt.ioloop")
-
- -- create mqtt client
- local client = mqtt.client{
- -- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
- -- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
- -- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
- username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
- clean = true,
- connector = require("mqtt.ngxsocket"),
- secure = true, -- optional
- }
- trace("created MQTT client", client)
-
- client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- trace("connection to broker failed:", connack)
- return
- end
- trace("connected:", connack) -- successful connection
-
- -- subscribe to test topic and publish message after it
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
- trace("subscribed:", suback)
-
- -- publish test message
- trace('publishing test message "hello" to "luamqtt/simpletest" topic...')
- assert(client:publish{
- topic = "luamqtt/simpletest",
- payload = "hello",
- qos = 1
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- trace("received:", msg)
- end,
-
- error = function(err)
- trace("MQTT client error:", err)
- end,
-
- close = function(conn)
- trace("MQTT conn closed:", conn.close_reason)
- end
- }
-
- trace("begin ioloop")
- local loop = ioloop.create{
- timeout = client.args.keep_alive,
- sleep_function = ngx.sleep,
- }
- loop:add(client)
- client:start_connecting()
- loop:run_until_clients()
- trace("done ioloop")
-
- -- to reconnect
- start_timer()
-end
-
-start_timer = function()
- local ok, err = timer_at(1, on_timer)
- if not ok then
- trace("failed to start timer:", err)
- end
-end
-
-start_timer()
diff --git a/examples/openresty/app/main-sync.lua b/examples/openresty/app/main-sync.lua
deleted file mode 100644
index 09e4158..0000000
--- a/examples/openresty/app/main-sync.lua
+++ /dev/null
@@ -1,89 +0,0 @@
--- luacheck: globals ngx
-local log = ngx.log
-local timer_at = ngx.timer.at
-local ERR = ngx.ERR
-local tbl_concat = table.concat
-
-local function trace(...)
- local line = {}
- for i = 1, select("#", ...) do
- line[i] = tostring(select(i, ...))
- end
- log(ERR, tbl_concat(line, " "))
-end
-
-trace("main.lua started")
-
-local start_timer
-
-local function on_timer(...)
- trace("on_timer: ", ...)
-
- local mqtt = require("mqtt")
-
- -- create mqtt client
- local client = mqtt.client{
- -- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
- -- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
- -- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
- username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
- clean = true,
- connector = require("mqtt.ngxsocket"),
- secure = true, -- optional
- }
- trace("created MQTT client", client)
-
- client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- trace("connection to broker failed:", connack)
- return
- end
- trace("connected:", connack) -- successful connection
-
- -- subscribe to test topic and publish message after it
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
- trace("subscribed:", suback)
-
- -- publish test message
- trace('publishing test message "hello" to "luamqtt/simpletest" topic...')
- assert(client:publish{
- topic = "luamqtt/simpletest",
- payload = "hello",
- qos = 1
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- trace("received:", msg)
- end,
-
- error = function(err)
- trace("MQTT client error:", err)
- end,
-
- close = function(conn)
- trace("MQTT conn closed:", conn.close_reason)
- end
- }
-
- trace("running client in synchronous input/output loop")
- mqtt.run_sync(client)
- trace("done, synchronous input/output loop is stopped")
-
- -- to reconnect
- start_timer()
-end
-
-start_timer = function()
- local ok, err = timer_at(1, on_timer)
- if not ok then
- trace("failed to start timer:", err)
- end
-end
-
-start_timer()
diff --git a/examples/openresty/app/openresty.lua b/examples/openresty/app/openresty.lua
new file mode 100644
index 0000000..baec0f4
--- /dev/null
+++ b/examples/openresty/app/openresty.lua
@@ -0,0 +1,63 @@
+-- runs in init_worker_by_lua phase
+
+-- IMPORTANT: set up logging before loading MQTT lib
+if pcall(require, "logging.nginx") then
+ -- LuaLogging nginx forwarder is available
+ ngx.log(ngx.INFO, "forwarding LuaMQTT logs to nginx log, using LuaLogging 'nginx' logger")
+ local ll = require("logging")
+ ll.defaultLogger(ll.nginx()) -- forward logs to nginx logs
+else
+ ngx.log(ngx.WARN, "LuaLogging module 'logging.nginx' not found, it is strongly recommended to install that module. ",
+ "See https://github.com/lunarmodules/lualogging.")
+end
+
+
+local mqtt = require "mqtt"
+local add_client = require("mqtt.loop").add
+
+
+-- create mqtt client
+local client = mqtt.client{
+ -- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
+ -- uri = "test.mosquitto.org",
+ uri = "mqtts://mqtt.flespi.io",
+ -- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
+ username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
+ clean = true,
+
+ -- event handlers
+ on = {
+ connect = function(connack, self)
+ if connack.rc ~= 0 then
+ return
+ end
+
+ -- subscribe to test topic and publish message after it
+ assert(self:subscribe {
+ topic = "luamqtt/#",
+ qos = 1,
+ callback = function()
+ -- publish test message
+ assert(self:publish{
+ topic = "luamqtt/simpletest",
+ payload = "hello",
+ qos = 1
+ })
+ end
+ })
+ end,
+
+ message = function(msg, self)
+ assert(self:acknowledge(msg))
+
+ ngx.log(ngx.INFO, "received:", msg)
+ end,
+
+ close = function(conn)
+ ngx.log(ngx.INFO, "MQTT conn closed:", conn.close_reason)
+ end
+ }
+}
+
+-- start the client
+add_client(client)
diff --git a/examples/openresty/conf/nginx.conf b/examples/openresty/conf/nginx.conf
index ad673c6..6dc6fdd 100644
--- a/examples/openresty/conf/nginx.conf
+++ b/examples/openresty/conf/nginx.conf
@@ -1,5 +1,16 @@
+# the code block below runs in "init-worker", meaning that each worker
+# will create it's own client. By default the number of worker processes
+# will equal the number of CPU-cores.
+# Since we want only a single client, we set the number of workers to 1.
worker_processes 1;
-error_log logs/error.log;
+
+
+# Set the log destination, and the log level. This is for Nginx logs only.
+# LuaMQTT uses LuaLogging for logs, it is strongly recommended to install it.
+# The example code will configure LuaLogging to automatically forward the logs
+# to the nginx log-file specified here, once LuaLogging nginx logger is found.
+error_log logs/error.log debug;
+
events {
worker_connections 1024;
@@ -12,6 +23,7 @@ stream {
resolver 8.8.8.8;
- init_worker_by_lua_file "app/main-sync.lua";
+ # the code file to execute
+ init_worker_by_lua_file "app/openresty.lua";
}
diff --git a/examples/openresty/quit.sh b/examples/openresty/quit.sh
new file mode 100755
index 0000000..57b15d7
--- /dev/null
+++ b/examples/openresty/quit.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+set -e
+
+PATH=/opt/openresty/nginx/sbin:$PATH
+export PATH
+nginx -p "$(pwd)" -s quit
diff --git a/examples/openresty/start.sh b/examples/openresty/start.sh
index 93734fb..50bf45b 100755
--- a/examples/openresty/start.sh
+++ b/examples/openresty/start.sh
@@ -4,5 +4,8 @@ set -e
PATH=/opt/openresty/nginx/sbin:$PATH
export PATH
-nginx -p `pwd`/ -c conf/nginx.conf
+nginx -p "$(pwd)" -c conf/nginx.conf
+# since this is an example, start tailing the logs
+touch logs/error.log
+tail -F logs/error.log
diff --git a/examples/openresty/stop.sh b/examples/openresty/stop.sh
index 87ce494..f7307de 100755
--- a/examples/openresty/stop.sh
+++ b/examples/openresty/stop.sh
@@ -4,5 +4,4 @@ set -e
PATH=/opt/openresty/nginx/sbin:$PATH
export PATH
-nginx -p `pwd`/ -s stop
-
+nginx -p "$(pwd)" -s stop
diff --git a/examples/simple.lua b/examples/simple.lua
index 6364183..d181b1b 100644
--- a/examples/simple.lua
+++ b/examples/simple.lua
@@ -5,48 +5,49 @@ local mqtt = require("mqtt")
local client = mqtt.client{
-- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
-- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
+ uri = "mqtt://mqtt.flespi.io",
-- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
clean = true,
-}
-print("created MQTT client", client)
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
- end
- print("connected:", connack) -- successful connection
-
- -- subscribe to test topic and publish message after it
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
- print("subscribed:", suback)
-
- -- publish test message
- print('publishing test message "hello" to "luamqtt/simpletest" topic...')
- assert(client:publish{
- topic = "luamqtt/simpletest",
- payload = "hello",
- qos = 1
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- print("received:", msg)
- print("disconnecting...")
- assert(client:disconnect())
- end,
-
- error = function(err)
- print("MQTT client error:", err)
- end,
+ -- create event handlers
+ on = {
+ connect = function(connack, self)
+ if connack.rc ~= 0 then
+ print("connection to broker failed:", connack:reason_string(), connack)
+ return
+ end
+ print("connected:", connack) -- successful connection
+
+ -- subscribe to test topic and publish message after it
+ assert(self:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
+ print("subscribed:", suback)
+
+ -- publish test message
+ print('publishing test message "hello" to "luamqtt/simpletest" topic...')
+ assert(self:publish{
+ topic = "luamqtt/simpletest",
+ payload = "hello",
+ qos = 1
+ })
+ end})
+ end,
+
+ message = function(msg, self)
+ assert(self:acknowledge(msg))
+
+ print("received:", msg)
+ print("disconnecting...")
+ assert(self:disconnect())
+ end,
+
+ error = function(err)
+ print("MQTT client error:", err)
+ end,
+ }, -- close 'on', event handlers
}
+print("created MQTT client", client)
print("running ioloop for it")
mqtt.run_ioloop(client)
diff --git a/examples/sync.lua b/examples/sync.lua
deleted file mode 100644
index b415b07..0000000
--- a/examples/sync.lua
+++ /dev/null
@@ -1,59 +0,0 @@
--- load mqtt module
-local mqtt = require("mqtt")
-
--- create mqtt client
-local client = mqtt.client{
- -- NOTE: this broker is not working sometimes; comment username = "..." below if you still want to use it
- -- uri = "test.mosquitto.org",
- uri = "mqtt.flespi.io",
- -- NOTE: more about flespi tokens: https://flespi.com/kb/tokens-access-keys-to-flespi-platform
- username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
- clean = true,
-}
-print("created MQTT client", client)
-
-client:on{
- connect = function(connack)
- if connack.rc ~= 0 then
- print("connection to broker failed:", connack:reason_string(), connack)
- return
- end
- print("connected:", connack) -- successful connection
-
- -- subscribe to test topic and publish message after it
- assert(client:subscribe{ topic="luamqtt/#", qos=1, callback=function(suback)
- print("subscribed:", suback)
-
- -- publish test message
- print('publishing test message "hello" to "luamqtt/simpletest" topic...')
- assert(client:publish{
- topic = "luamqtt/simpletest",
- payload = "hello",
- qos = 1
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
-
- print("received:", msg)
- print("disconnecting...")
- assert(client:disconnect())
- end,
-
- error = function(err)
- print("MQTT client error:", err)
- end,
-
- close = function()
- print("MQTT conn closed")
- end
-}
-
--- run io loop for client until connection close
--- please note that in sync mode background PINGREQ's are not available, and automatic reconnects too
-print("running client in synchronous input/output loop")
-mqtt.run_sync(client)
-
-print("done, synchronous input/output loop is stopped")
\ No newline at end of file
diff --git a/mqtt/client.lua b/mqtt/client.lua
index 3ff03b3..08828b1 100644
--- a/mqtt/client.lua
+++ b/mqtt/client.lua
@@ -1,9 +1,6 @@
---- MQTT client module
--- @module mqtt.client
--- @alias client
-local client = {}
-
--- TODO: list event names
+--- This class contains the MQTT client implementation.
+-- @classmod Client
+local _M = {}
-------
@@ -20,16 +17,10 @@ local os_time = os.time
local string = require("string")
local str_format = string.format
local str_gsub = string.gsub
-local str_match = string.match
local table = require("table")
local table_remove = table.remove
-local coroutine = require("coroutine")
-local coroutine_create = coroutine.create
-local coroutine_resume = coroutine.resume
-local coroutine_yield = coroutine.yield
-
local math = require("math")
local math_random = math.random
@@ -49,52 +40,63 @@ local protocol5 = require("mqtt.protocol5")
local make_packet5 = protocol5.make_packet
local parse_packet5 = protocol5.parse_packet
-local ioloop = require("mqtt.ioloop")
-local ioloop_get = ioloop.get
+local log = require "mqtt.log"
-------
--- MQTT client instance metatable
--- @type client_mt
-local client_mt = {}
-client_mt.__index = client_mt
-
---- Create and initialize MQTT client instance
--- @tparam table args MQTT client creation arguments table
--- @tparam string args.uri MQTT broker uri to connect.
--- Expecting "host:port" or "host" format, in second case the port will be selected automatically:
--- 1883 port for plain or 8883 for secure network connections
--- @tparam string args.clean clean session start flag
--- @tparam[opt=4] number args.version MQTT protocol version to use, either 4 (for MQTT v3.1.1) or 5 (for MQTT v5.0).
--- Also you may use special values mqtt.v311 or mqtt.v50 for this field.
--- @tparam[opt] string args.id MQTT client ID, will be generated by luamqtt library if absent
--- @tparam[opt] string args.username username for authorization on MQTT broker
--- @tparam[opt] string args.password password for authorization on MQTT broker; not acceptable in absence of username
--- @tparam[opt=false] boolean,table args.secure use secure network connection, provided by luasec lua module;
--- set to true to select default params: { mode="client", protocol="tlsv1_2", verify="none", options="all" }
--- or set to luasec-compatible table, for example with cafile="...", certificate="...", key="..."
--- @tparam[opt] table args.will will message table with required fields { topic="...", payload="..." }
--- and optional fields { qos=1...3, retain=true/false }
--- @tparam[opt=60] number args.keep_alive time interval for client to send PINGREQ packets to the server when network connection is inactive
--- @tparam[opt=false] boolean args.reconnect force created MQTT client to reconnect on connection close.
--- Set to number value to provide reconnect timeout in seconds
--- It's not recommended to use values < 3
--- @tparam[opt] table args.connector connector table to open and send/receive packets over network connection.
--- default is require("mqtt.luasocket"), or require("mqtt.luasocket_ssl") if secure argument is set
--- @tparam[opt="ssl"] string args.ssl_module module name for the luasec-compatible ssl module, default is "ssl"
+local Client = {}
+Client.__index = Client
+
+--- Create and initialize MQTT client instance. Typically this is not called directly,
+-- but through `Client.create`.
+-- @tparam table opts MQTT client creation options table
+-- @tparam string opts.uri MQTT broker uri to connect. Expected format:
+--
`[mqtt[s]://][username[:password]@]hostname[:port]`
+--
Any option specifically added to the options
+-- table will take precedence over the option specified in this uri.
+-- @tparam boolean|string opts.clean clean session start flag, use "first" to start clean only on first connect
+-- @tparam[opt] string opts.protocol either `"mqtt"` or `"mqtts"`
+-- @tparam[opt] string opts.username username for authorization on MQTT broker
+-- @tparam[opt] string opts.password password for authorization on MQTT broker; not acceptable in absence of username
+-- @tparam[opt] string opts.host hostname of the MQTT broker to connect to
+-- @tparam[opt] int opts.port port number to connect to on the MQTT broker, defaults to `1883` port for plain or `8883` for secure network connections
+-- @tparam[opt=4] number opts.version MQTT protocol version to use, either `4` (for MQTT v3.1.1) or `5` (for MQTT v5.0).
+-- Also you may use special values `mqtt.v311` or `mqtt.v50` for this field.
+-- @tparam[opt] string opts.id MQTT client ID, will be generated by luamqtt library if absent
+-- @tparam[opt=false] boolean|table opts.secure use secure network connection, provided by the lua module set in `opts.ssl_module`.
+-- Set to true to select default parameters, check individual `mqtt.connectors` for supported options.
+-- @tparam[opt] table opts.will will message table with required fields `{ topic="...", payload="..." }`
+-- and optional fields `{ qos=0...2, retain=true/false }`
+-- @tparam[opt=60] number opts.keep_alive time interval (in seconds) for client to send PINGREQ packets to the server when network connection is inactive
+-- @tparam[opt=false] boolean opts.reconnect force created MQTT client to reconnect on connection close.
+-- Set to number value to provide reconnect timeout in seconds.
+-- It's not recommended to use values `< 3`. See also `Client:shutdown`.
+-- @tparam[opt] table opts.connector connector table to open and send/receive packets over network connection.
+-- default is `require("mqtt.connector")` which tries to auto-detect. See `mqtt.connector`.
+-- @tparam[opt="ssl"] string opts.ssl_module module name for the luasec-compatible ssl module, default is `"ssl"`
-- may be used in some non-standard lua environments with own luasec-compatible ssl module
--- @treturn client_mt MQTT client instance table
-function client_mt:__init(args)
+-- @tparam[opt] table opts.on List of event-handlers. See `Client:on` for the format.
+-- @treturn Client MQTT client instance table
+-- @usage
+-- local Client = require "mqtt.client"
+--
+-- local my_client = Client.create {
+-- uri = "mqtts://broker.host.com",
+-- clean = "first",
+-- version = mqtt.v50,
+-- }
+function Client:__init(opts)
if not luamqtt_VERSION then
luamqtt_VERSION = require("mqtt")._VERSION
end
- -- fetch and validate client args
- local a = {} -- own client copy of args
+ -- fetch and validate client opts
+ local a = {} -- own client copy of opts
- for key, value in pairs(args) do
+ for key, value in pairs(opts) do
if type(key) ~= "string" then
- error("expecting string key in args, got: "..type(key))
+ error("expecting string key in opts, got: "..type(key))
end
local value_type = type(value)
@@ -102,7 +104,7 @@ function client_mt:__init(args)
assert(value_type == "string", "expecting uri to be a string")
a.uri = value
elseif key == "clean" then
- assert(value_type == "boolean", "expecting clean to be a boolean")
+ assert(value_type == "boolean" or value == "first", "expecting clean to be a boolean, or 'first'")
a.clean = value
elseif key == "version" then
assert(value_type == "number", "expecting version to be a number")
@@ -140,15 +142,17 @@ function client_mt:__init(args)
elseif key == "ssl_module" then
assert(value_type == "string", "expecting ssl_module to be a string")
a.ssl_module = value
+ elseif key == "on" then
+ assert(value_type == "table", "expecting 'on' to be a table with events and callbacks")
+ a.on = value
else
- error("unexpected key in client args: "..key.." = "..tostring(value))
+ error("unexpected key in client opts: "..key.." = "..tostring(value))
end
end
-- check required arguments
assert(a.uri, 'expecting uri="..." to create MQTT client')
- assert(a.clean ~= nil, "expecting clean=true or clean=false to create MQTT client")
- assert(not a.password or a.username, "password is not accepted in absence of username")
+ assert(a.clean ~= nil, "expecting clean=true, clean=false, or clean='first' to create MQTT client")
if not a.id then
-- generate random client id
@@ -156,19 +160,27 @@ function client_mt:__init(args)
end
-- default connector
- if a.connector == nil then
- if a.secure then
- a.connector = require("mqtt.luasocket_ssl")
- else
- a.connector = require("mqtt.luasocket")
- end
+ a.connector = a.connector or require("mqtt.connector")
+
+ -- default reconnect interval
+ if a.reconnect == true then
+ a.reconnect = 30
end
+
-- validate connector content
assert(type(a.connector) == "table", "expecting connector to be a table")
+ assert(type(a.connector.validate) == "function", "expecting connector.validate to be a function")
assert(type(a.connector.connect) == "function", "expecting connector.connect to be a function")
assert(type(a.connector.shutdown) == "function", "expecting connector.shutdown to be a function")
assert(type(a.connector.send) == "function", "expecting connector.send to be a function")
assert(type(a.connector.receive) == "function", "expecting connector.receive to be a function")
+ assert(a.connector.signal_closed, "missing connector.signal_closed signal value")
+ assert(a.connector.signal_idle, "missing connector.signal_idle signal value")
+
+ -- validate connection properties
+ local test_conn = setmetatable({ uri = opts.uri }, a.connector)
+ Client._parse_connection_opts(a, test_conn)
+ test_conn:validate()
-- will table content check
if a.will then
@@ -188,8 +200,8 @@ function client_mt:__init(args)
a.keep_alive = 60
end
- -- client args
- self.args = a
+ -- client opts
+ self.opts = a
-- event handlers
self.handlers = {
@@ -201,13 +213,15 @@ function client_mt:__init(args)
error = {},
close = {},
auth = {},
+ shutdown = {},
}
self._handling = {}
self._to_remove_handlers = {}
-- state
- self.first_connect = true -- contains true to perform one network connection attempt after client creation
self.send_time = 0 -- time of the last network send from client side
+ self.first_connect = true -- contains true to perform one network connection attempt after client creation
+ -- Note: remains true, during the connect process. False after succes or failure.
-- packet creation/parse functions according version
if not a.version then
@@ -221,17 +235,55 @@ function client_mt:__init(args)
self._parse_packet = parse_packet5
end
- -- automatically add client to default ioloop, if it's available and running, then start connecting
- local loop = ioloop_get(false)
- if loop and loop.running then
- loop:add(self)
- self:start_connecting()
+ -- register event handlers
+ if a.on then
+ self:on(self.opts.on)
end
+
+ log:info("[LuaMQTT] client '%s' created", a.id)
end
---- Add functions as handlers of given events
--- @param ... (event_name, function) or { event1 = func1, event2 = func2 } table
-function client_mt:on(...)
+--- Add functions as handlers of given events.
+-- @tparam table events MQTT client creation options table
+-- @tparam function events.connect `function(connack_packet, client_obj)`
+-- After a connect attempt, after receiving the CONNACK packet from the broker.
+-- check `connack_packet.rc == 0` for a succesful connect.
+-- @tparam function events.error `function(errmsg, client_obj [, packet])`
+-- on errors, optional `packet` is only provided if the
+-- received `CONNACK.rc ~= 0` when connecting.
+-- @tparam function events.close `function(connection_obj, client_obj)`
+-- upon closing the connection. `connection_obj.close_reason`
+-- (string) will hold the close reason.
+-- @tparam function events.shutdown `function(client_obj)`
+-- upon shutting down the client (diconnecting an no more reconnects).
+-- @tparam function events.subscribe `function(suback_packet, client_obj)`
+-- upon a succesful subscription, after receiving the SUBACK packet from the broker
+-- @tparam function events.unsubscribe `function(unsuback_packet, client_obj)`
+-- upon a succesful unsubscription, after receiving the UNSUBACK packet from the broker
+-- @tparam function events.message `function(publish_packet, client_obj)`
+-- upon receiving a PUBLISH packet from the broker
+-- @tparam function events.acknowledge `function(ack_packet, client_obj)`
+-- upon receiving a PUBACK or PUBREC packet from the broker
+-- @tparam function events.auth `function(auth_packet, client_obj)`
+-- upon receiving an AUTH packet
+-- @usage
+-- client:on {
+-- connect = function(pck, self)
+-- if pck.rc ~= 0 then
+-- return -- connection failed
+-- end
+-- -- succesfully connected
+-- end,
+-- message = function(pck, self)
+-- -- handle received message
+-- end,
+-- }
+--
+-- -- an alternative way to add individual handlers;
+-- client:on("message", function(pck, self)
+-- -- handle received message
+-- end)
+function Client:on(...)
local nargs = select("#", ...)
local events
if nargs == 2 then
@@ -239,7 +291,7 @@ function client_mt:on(...)
elseif nargs == 1 then
events = select(1, ...)
else
- error("invalid args: expected only one or two arguments")
+ error("invalid arguments: expected only one or two arguments")
end
for event, func in pairs(events) do
assert(type(event) == "string", "expecting event to be a string")
@@ -262,10 +314,22 @@ local function remove_item(list, item)
end
end
---- Remove given function handler for specified event
+--- Remove given function handler for specified event.
-- @tparam string event event name to remove handler
-- @tparam function func handler function to remove
-function client_mt:off(event, func)
+-- @usage
+-- local handler = function(pck, self)
+-- -- handle received message
+-- end
+--
+-- -- add event handler
+-- client:on {
+-- message = handler
+-- }
+--
+-- -- remove it again
+-- client:off("message", handler)
+function Client:off(event, func)
local handlers = self.handlers[event]
if not handlers then
error("invalid event '"..tostring(event).."' to handle")
@@ -283,27 +347,27 @@ function client_mt:off(event, func)
end
--- Subscribe to specified topic. Returns the SUBSCRIBE packet id and calls optional callback when subscription will be created on broker
--- @tparam table args subscription arguments
--- @tparam string args.topic topic to subscribe
--- @tparam[opt=0] number args.qos QoS level for subscription
--- @tparam boolean args.no_local for MQTT v5.0 only: no_local flag for subscription
--- @tparam boolean args.retain_as_published for MQTT v5.0 only: retain_as_published flag for subscription
--- @tparam boolean args.retain_handling for MQTT v5.0 only: retain_handling flag for subscription
--- @tparam[opt] table args.properties for MQTT v5.0 only: properties for subscribe operation
--- @tparam[opt] table args.user_properties for MQTT v5.0 only: user properties for subscribe operation
--- @tparam[opt] function args.callback callback function to be called when subscription will be created
+-- @tparam table opts subscription options
+-- @tparam string opts.topic topic to subscribe
+-- @tparam[opt=0] number opts.qos QoS level for subscription
+-- @tparam boolean opts.no_local for MQTT v5.0 only: no_local flag for subscription
+-- @tparam boolean opts.retain_as_published for MQTT v5.0 only: retain_as_published flag for subscription
+-- @tparam boolean opts.retain_handling for MQTT v5.0 only: retain_handling flag for subscription
+-- @tparam[opt] table opts.properties for MQTT v5.0 only: properties for subscribe operation
+-- @tparam[opt] table opts.user_properties for MQTT v5.0 only: user properties for subscribe operation
+-- @tparam[opt] function opts.callback callback function to be called when subscription is acknowledged by broker
-- @return packet id on success or false and error message on failure
-function client_mt:subscribe(args)
- -- fetch and validate args
- assert(type(args) == "table", "expecting args to be a table")
- assert(type(args.topic) == "string", "expecting args.topic to be a string")
- assert(args.qos == nil or (type(args.qos) == "number" and check_qos(args.qos)), "expecting valid args.qos value")
- assert(args.no_local == nil or type(args.no_local) == "boolean", "expecting args.no_local to be a boolean")
- assert(args.retain_as_published == nil or type(args.retain_as_published) == "boolean", "expecting args.retain_as_published to be a boolean")
- assert(args.retain_handling == nil or type(args.retain_handling) == "boolean", "expecting args.retain_handling to be a boolean")
- assert(args.properties == nil or type(args.properties) == "table", "expecting args.properties to be a table")
- assert(args.user_properties == nil or type(args.user_properties) == "table", "expecting args.user_properties to be a table")
- assert(args.callback == nil or type(args.callback) == "function", "expecting args.callback to be a function")
+function Client:subscribe(opts)
+ -- fetch and validate opts
+ assert(type(opts) == "table", "expecting opts to be a table")
+ assert(type(opts.topic) == "string", "expecting opts.topic to be a string")
+ assert(opts.qos == nil or (type(opts.qos) == "number" and check_qos(opts.qos)), "expecting valid opts.qos value")
+ assert(opts.no_local == nil or type(opts.no_local) == "boolean", "expecting opts.no_local to be a boolean")
+ assert(opts.retain_as_published == nil or type(opts.retain_as_published) == "boolean", "expecting opts.retain_as_published to be a boolean")
+ assert(opts.retain_handling == nil or type(opts.retain_handling) == "boolean", "expecting opts.retain_handling to be a boolean")
+ assert(opts.properties == nil or type(opts.properties) == "table", "expecting opts.properties to be a table")
+ assert(opts.user_properties == nil or type(opts.user_properties) == "table", "expecting opts.user_properties to be a table")
+ assert(opts.callback == nil or type(opts.callback) == "function", "expecting opts.callback to be a function")
-- check connection is alive
if not self.connection then
@@ -315,31 +379,34 @@ function client_mt:subscribe(args)
type = packet_type.SUBSCRIBE,
subscriptions = {
{
- topic = args.topic,
- qos = args.qos,
- no_local = args.no_local,
- retain_as_published = args.retain_as_published,
- retain_handling = args.retain_handling
+ topic = opts.topic,
+ qos = opts.qos,
+ no_local = opts.no_local,
+ retain_as_published = opts.retain_as_published,
+ retain_handling = opts.retain_handling
},
},
- properties = args.properties,
- user_properties = args.user_properties,
+ properties = opts.properties,
+ user_properties = opts.user_properties,
}
self:_assign_packet_id(pargs)
local packet_id = pargs.packet_id
local subscribe = self._make_packet(pargs)
+ log:info("[LuaMQTT] subscribing client '%s' to topic '%s' (packet: %s)", self.opts.id, opts.topic, packet_id or "n.a.")
+
-- send SUBSCRIBE packet
local ok, err = self:_send_packet(subscribe)
if not ok then
err = "failed to send SUBSCRIBE: "..err
+ log:error("[LuaMQTT] client '%s': %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
end
-- add subscribe callback
- local callback = args.callback
+ local callback = opts.callback
if callback then
local function handler(suback, ...)
if suback.packet_id == packet_id then
@@ -355,19 +422,19 @@ function client_mt:subscribe(args)
end
--- Unsubscribe from specified topic, and calls optional callback when subscription will be removed on broker
--- @tparam table args subscription arguments
--- @tparam string args.topic topic to unsubscribe
--- @tparam[opt] table args.properties properties for unsubscribe operation
--- @tparam[opt] table args.user_properties user properties for unsubscribe operation
--- @tparam[opt] function args.callback callback function to be called when subscription will be removed on broker
+-- @tparam table opts subscription options
+-- @tparam string opts.topic topic to unsubscribe
+-- @tparam[opt] table opts.properties properties for unsubscribe operation
+-- @tparam[opt] table opts.user_properties user properties for unsubscribe operation
+-- @tparam[opt] function opts.callback callback function to be called when the unsubscribe is acknowledged by the broker
-- @return packet id on success or false and error message on failure
-function client_mt:unsubscribe(args)
- -- fetch and validate args
- assert(type(args) == "table", "expecting args to be a table")
- assert(type(args.topic) == "string", "expecting args.topic to be a string")
- assert(args.properties == nil or type(args.properties) == "table", "expecting args.properties to be a table")
- assert(args.user_properties == nil or type(args.user_properties) == "table", "expecting args.user_properties to be a table")
- assert(args.callback == nil or type(args.callback) == "function", "expecting args.callback to be a function")
+function Client:unsubscribe(opts)
+ -- fetch and validate opts
+ assert(type(opts) == "table", "expecting opts to be a table")
+ assert(type(opts.topic) == "string", "expecting opts.topic to be a string")
+ assert(opts.properties == nil or type(opts.properties) == "table", "expecting opts.properties to be a table")
+ assert(opts.user_properties == nil or type(opts.user_properties) == "table", "expecting opts.user_properties to be a table")
+ assert(opts.callback == nil or type(opts.callback) == "function", "expecting opts.callback to be a function")
-- check connection is alive
@@ -378,25 +445,28 @@ function client_mt:unsubscribe(args)
-- create UNSUBSCRIBE packet
local pargs = {
type = packet_type.UNSUBSCRIBE,
- subscriptions = {args.topic},
- properties = args.properties,
- user_properties = args.user_properties,
+ subscriptions = {opts.topic},
+ properties = opts.properties,
+ user_properties = opts.user_properties,
}
self:_assign_packet_id(pargs)
local packet_id = pargs.packet_id
local unsubscribe = self._make_packet(pargs)
+ log:info("[LuaMQTT] unsubscribing client '%s' from topic '%s' (packet: %s)", self.opts.id, opts.topic, packet_id or "n.a.")
+
-- send UNSUBSCRIBE packet
local ok, err = self:_send_packet(unsubscribe)
if not ok then
err = "failed to send UNSUBSCRIBE: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
end
-- add unsubscribe callback
- local callback = args.callback
+ local callback = opts.callback
if callback then
local function handler(unsuback, ...)
if unsuback.packet_id == packet_id then
@@ -412,30 +482,30 @@ function client_mt:unsubscribe(args)
end
--- Publish message to broker
--- @tparam table args publish operation arguments table
--- @tparam string args.topic topic to publish message
--- @tparam[opt] string args.payload publish message payload
--- @tparam[opt=0] number args.qos QoS level for message publication
--- @tparam[opt=false] boolean args.retain retain message publication flag
--- @tparam[opt=false] boolean args.dup dup message publication flag
--- @tparam[opt] table args.properties properties for publishing message
--- @tparam[opt] table args.user_properties user properties for publishing message
--- @tparam[opt] function args.callback callback to call when published message will be acknowledged
+-- @tparam table opts publish operation options table
+-- @tparam string opts.topic topic to publish message
+-- @tparam[opt] string opts.payload publish message payload
+-- @tparam[opt=0] number opts.qos QoS level for message publication
+-- @tparam[opt=false] boolean opts.retain retain message publication flag
+-- @tparam[opt=false] boolean opts.dup dup message publication flag
+-- @tparam[opt] table opts.properties properties for publishing message
+-- @tparam[opt] table opts.user_properties user properties for publishing message
+-- @tparam[opt] function opts.callback callback to call when published message has been acknowledged by the broker
-- @return true or packet id on success or false and error message on failure
-function client_mt:publish(args)
- -- fetch and validate args
- assert(type(args) == "table", "expecting args to be a table")
- assert(type(args.topic) == "string", "expecting args.topic to be a string")
- assert(args.payload == nil or type(args.payload) == "string", "expecting args.payload to be a string")
- assert(args.qos == nil or type(args.qos) == "number", "expecting args.qos to be a number")
- if args.qos then
- assert(check_qos(args.qos), "expecting qos to be a valid QoS value")
- end
- assert(args.retain == nil or type(args.retain) == "boolean", "expecting args.retain to be a boolean")
- assert(args.dup == nil or type(args.dup) == "boolean", "expecting args.dup to be a boolean")
- assert(args.properties == nil or type(args.properties) == "table", "expecting args.properties to be a table")
- assert(args.user_properties == nil or type(args.user_properties) == "table", "expecting args.user_properties to be a table")
- assert(args.callback == nil or type(args.callback) == "function", "expecting args.callback to be a function")
+function Client:publish(opts)
+ -- fetch and validate opts
+ assert(type(opts) == "table", "expecting opts to be a table")
+ assert(type(opts.topic) == "string", "expecting opts.topic to be a string")
+ assert(opts.payload == nil or type(opts.payload) == "string", "expecting opts.payload to be a string")
+ assert(opts.qos == nil or type(opts.qos) == "number", "expecting opts.qos to be a number")
+ if opts.qos then
+ assert(check_qos(opts.qos), "expecting qos to be a valid QoS value")
+ end
+ assert(opts.retain == nil or type(opts.retain) == "boolean", "expecting opts.retain to be a boolean")
+ assert(opts.dup == nil or type(opts.dup) == "boolean", "expecting opts.dup to be a boolean")
+ assert(opts.properties == nil or type(opts.properties) == "table", "expecting opts.properties to be a table")
+ assert(opts.user_properties == nil or type(opts.user_properties) == "table", "expecting opts.user_properties to be a table")
+ assert(opts.callback == nil or type(opts.callback) == "function", "expecting opts.callback to be a function")
-- check connection is alive
local conn = self.connection
@@ -444,27 +514,30 @@ function client_mt:publish(args)
end
-- create PUBLISH packet
- args.type = packet_type.PUBLISH
- self:_assign_packet_id(args)
- local packet_id = args.packet_id
- local publish = self._make_packet(args)
+ opts.type = packet_type.PUBLISH
+ self:_assign_packet_id(opts)
+ local packet_id = opts.packet_id
+ local publish = self._make_packet(opts)
+
+ log:debug("[LuaMQTT] client '%s' publishing to topic '%s' (packet: %s)", self.opts.id, opts.topic, packet_id or "n.a.")
-- send PUBLISH packet
local ok, err = self:_send_packet(publish)
if not ok then
err = "failed to send PUBLISH: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
end
-- record packet id as waited for QoS 2 exchange
- if args.qos == 2 then
+ if opts.qos == 2 then
conn.wait_for_pubrec[packet_id] = true
end
-- add acknowledge callback
- local callback = args.callback
+ local callback = opts.callback
if callback then
if packet_id then
local function handler(ack, ...)
@@ -489,7 +562,7 @@ end
-- @tparam[opt] table properties properties for PUBACK/PUBREC packets
-- @tparam[opt] table user_properties user properties for PUBACK/PUBREC packets
-- @return true on success or false and error message on failure
-function client_mt:acknowledge(msg, rc, properties, user_properties)
+function Client:acknowledge(msg, rc, properties, user_properties)
assert(type(msg) == "table" and msg.type == packet_type.PUBLISH, "expecting msg to be a publish packet")
assert(rc == nil or type(rc) == "number", "expecting rc to be a number")
assert(properties == nil or type(properties) == "table", "expecting properties to be a table")
@@ -507,6 +580,8 @@ function client_mt:acknowledge(msg, rc, properties, user_properties)
return true
end
+ log:debug("[LuaMQTT] client '%s' acknowledging packet %s", self.opts.id, packet_id or "n.a.")
+
if msg.qos == 1 then
-- PUBACK should be sent
@@ -523,6 +598,7 @@ function client_mt:acknowledge(msg, rc, properties, user_properties)
local ok, err = self:_send_packet(puback)
if not ok then
err = "failed to send PUBACK: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -543,6 +619,7 @@ function client_mt:acknowledge(msg, rc, properties, user_properties)
local ok, err = self:_send_packet(pubrec)
if not ok then
err = "failed to send PUBREC: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -555,13 +632,15 @@ function client_mt:acknowledge(msg, rc, properties, user_properties)
return true
end
---- Send DISCONNECT packet to the broker and close the connection
+--- Send DISCONNECT packet to the broker and close the connection.
+-- Note: if the client is set to automatically reconnect, it will do so. If you
+-- want to disconnect and NOT reconnect, use `Client:shutdown`.
-- @tparam[opt=0] number rc The Disconnect Reason Code value from MQTT v5.0 protocol
-- @tparam[opt] table properties properties for PUBACK/PUBREC packets
-- @tparam[opt] table user_properties user properties for PUBACK/PUBREC packets
-- @return true on success or false and error message on failure
-function client_mt:disconnect(rc, properties, user_properties)
- -- validate args
+function Client:disconnect(rc, properties, user_properties)
+ -- validate opts
assert(rc == nil or type(rc) == "number", "expecting rc to be a number")
assert(properties == nil or type(properties) == "table", "expecting properties to be a table")
assert(user_properties == nil or type(user_properties) == "table", "expecting user_properties to be a table")
@@ -579,10 +658,13 @@ function client_mt:disconnect(rc, properties, user_properties)
user_properties = user_properties,
}
+ log:info("[LuaMQTT] client '%s' disconnecting (rc = %d)", self.opts.id, rc or 0)
+
-- send DISCONNECT packet
local ok, err = self:_send_packet(disconnect)
if not ok then
err = "failed to send DISCONNECT: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -594,17 +676,32 @@ function client_mt:disconnect(rc, properties, user_properties)
return true
end
+--- Shuts the client down.
+-- Disconnects if still connected, and disables reconnecting. If the client is
+-- added to an ioloop, this will prevent an automatic reconnect.
+-- Raises the "shutdown" event.
+-- @param ... see `Client:disconnect`
+-- @return `true`
+function Client:shutdown(...)
+ log:debug("[LuaMQTT] client '%s' shutting down", self.opts.id)
+ self.first_connect = false
+ self.opts.reconnect = false
+ self:disconnect(...)
+ self:handle("shutdown", self)
+ return true
+end
+
--- Send AUTH packet to authenticate client on broker, in MQTT v5.0 protocol
-- @tparam[opt=0] number rc Authenticate Reason Code
-- @tparam[opt] table properties properties for PUBACK/PUBREC packets
-- @tparam[opt] table user_properties user properties for PUBACK/PUBREC packets
-- @return true on success or false and error message on failure
-function client_mt:auth(rc, properties, user_properties)
- -- validate args
+function Client:auth(rc, properties, user_properties)
+ -- validate opts
assert(rc == nil or type(rc) == "number", "expecting rc to be a number")
assert(properties == nil or type(properties) == "table", "expecting properties to be a table")
assert(user_properties == nil or type(user_properties) == "table", "expecting user_properties to be a table")
- assert(self.args.version == 5, "allowed only in MQTT v5.0 protocol")
+ assert(self.opts.version == 5, "allowed only in MQTT v5.0 protocol")
-- check connection is alive
if not self.connection then
@@ -619,10 +716,13 @@ function client_mt:auth(rc, properties, user_properties)
user_properties = user_properties,
}
+ log:info("[LuaMQTT] client '%s' authenticating")
+
-- send AUTH packet
local ok, err = self:_send_packet(auth)
if not ok then
err = "failed to send AUTH: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -633,44 +733,39 @@ end
--- Immediately close established network connection, without graceful session finishing with DISCONNECT packet
-- @tparam[opt] string reason the reasong string of connection close
-function client_mt:close_connection(reason)
+function Client:close_connection(reason)
assert(not reason or type(reason) == "string", "expecting reason to be a string")
local conn = self.connection
if not conn then
return true
end
- local args = self.args
- args.connector.shutdown(conn)
- self.connection = nil
- conn.close_reason = reason or "unspecified"
+ reason = reason or "unspecified"
- self:handle("close", conn, self)
+ log:info("[LuaMQTT] client '%s' closing connection (reason: %s)", self.opts.id, reason)
- -- check connection is still closed (self.connection may be re-created in "close" handler)
- if not self.connection then
- -- remove from ioloop
- if self.ioloop and not args.reconnect then
- self.ioloop:remove(self)
- end
- end
+ conn:shutdown()
+ self.connection = nil
+ conn.close_reason = reason
+ self:handle("close", conn, self)
return true
end
--- Start connecting to broker
-- @return true on success or false and error message on failure
-function client_mt:start_connecting()
- -- print("start connecting") -- debug
+function Client:start_connecting()
-- open network connection
local ok, err = self:open_connection()
if not ok then
+ self.first_connect = false
return false, err
end
-- send CONNECT packet
ok, err = self:send_connect()
if not ok then
+ self.first_connect = false
return false, err
end
@@ -682,7 +777,7 @@ end
--- Send PINGREQ packet
-- @return true on success or false and error message on failure
-function client_mt:send_pingreq()
+function Client:send_pingreq()
-- check connection is alive
if not self.connection then
return false, "network connection is not opened"
@@ -693,40 +788,48 @@ function client_mt:send_pingreq()
type = packet_type.PINGREQ,
}
+ log:debug("[LuaMQTT] client '%s' sending PINGREQ", self.opts.id)
+
-- send PINGREQ packet
local ok, err = self:_send_packet(pingreq)
if not ok then
err = "failed to send PINGREQ: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
end
+ -- set ping timeout; for now 1 ping-request interval
+ self.ping_expire_time = os_time() + self.opts.keep_alive
+
return true
end
--- Open network connection to the broker
-- @return true on success or false and error message on failure
-function client_mt:open_connection()
+function Client:open_connection()
if self.connection then
return true
end
- local args = self.args
- local connector = assert(args.connector, "no connector configured in MQTT client")
+ local opts = self.opts
+ local connector = assert(opts.connector, "no connector configured in MQTT client")
-- create connection table
- local conn = {
- uri = args.uri,
+ local conn = setmetatable({
+ uri = opts.uri,
wait_for_pubrec = {}, -- a table with packet_id of partially acknowledged sent packets in QoS 2 exchange process
wait_for_pubrel = {}, -- a table with packet_id of partially acknowledged received packets in QoS 2 exchange process
- }
- client_mt._parse_uri(args, conn)
- client_mt._apply_secure(args, conn)
+ }, connector)
+ Client._parse_connection_opts(opts, conn)
+
+ log:info("[LuaMQTT] client '%s' connecting to broker '%s' (using: %s)", self.opts.id, opts.uri, conn.type or "unknown")
-- perform connect
- local ok, err = connector.connect(conn)
+ local ok, err = conn:connect()
if not ok then
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
err = "failed to open network connection: "..err
self:handle("error", err, self)
return false, err
@@ -735,44 +838,42 @@ function client_mt:open_connection()
-- assign connection
self.connection = conn
- -- create receive function
- local receive = connector.receive
- self.connection.recv_func = function(size)
- return receive(conn, size)
- end
-
- self:_apply_network_timeout()
+ -- reset ping timeout
+ self.ping_expire_time = nil
return true
end
--- Send CONNECT packet into opened network connection
-- @return true on success or false and error message on failure
-function client_mt:send_connect()
+function Client:send_connect()
-- check connection is alive
if not self.connection then
return false, "network connection is not opened"
end
- local args = self.args
+ local opts = self.opts
-- create CONNECT packet
local connect = self._make_packet{
type = packet_type.CONNECT,
- id = args.id,
- clean = args.clean,
- username = args.username,
- password = args.password,
- will = args.will,
- keep_alive = args.keep_alive,
- properties = args.properties,
- user_properties = args.user_properties,
+ id = opts.id,
+ clean = not not opts.clean, -- force to boolean, in case "first"
+ username = opts.username,
+ password = opts.password,
+ will = opts.will,
+ keep_alive = opts.keep_alive,
+ properties = opts.properties,
+ user_properties = opts.user_properties,
}
+ log:info("[LuaMQTT] client '%s' sending CONNECT (user '%s')", self.opts.id, opts.username or "not specified")
+
-- send CONNECT packet
local ok, err = self:_send_packet(connect)
if not ok then
err = "failed to send CONNECT: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -784,17 +885,61 @@ function client_mt:send_connect()
return true
end
--- Internal methods
+--- Checks last message send, and sends a PINGREQ if necessary.
+-- Use this function to check and send keep-alives when using an external event loop. When using the
+-- included modules to add clients (see `mqtt.loop`), this will be taken care of automatically.
+-- @treturn[1] number time till next keep_alive (in seconds)
+-- @treturn[2] number time till next keep_alive (in seconds)
+-- @treturn[2] string in case of errors (eg. not connected) the second return value is an error string
+-- @usage
+-- -- example using a Copas event loop to send and check keep-alives
+-- copas.addthread(function()
+-- while true do
+-- if not my_client then
+-- return -- exiting, client was destroyed
+-- end
+-- copas.pause(my_client:check_keep_alive())
+-- end
+-- end)
+function Client:check_keep_alive()
+ local interval = self.opts.keep_alive
+ if not self.connection then
+ return interval, "network connection is not opened"
+ end
+
+ local t_now = os_time()
+ local t_next = self.send_time + interval
+ local t_timeout = self.ping_expire_time
--- Set or rest ioloop for MQTT client
-function client_mt:set_ioloop(loop)
- self.ioloop = loop
- self:_apply_network_timeout()
+ -- check last ping request
+ if t_timeout and t_timeout <= t_now then
+ -- we timed-out, close and exit
+ local err = str_format("failed to receive PINGRESP within %d seconds", interval)
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
+ self:handle("error", err, self)
+ self:close_connection("error")
+ return interval, err
+ end
+
+ -- send PINGREQ if keep_alive interval is reached
+ if t_now >= t_next then
+ local _, err = self:send_pingreq()
+ return interval, err
+ end
+
+ -- return which ever is earlier, timeout or next ping request
+ if t_timeout and t_timeout < t_next then
+ return t_timeout - t_now
+ end
+ return t_next - t_now
end
+
+-- Internal methods
+
-- Send PUBREL acknowledge packet - second phase of QoS 2 exchange
-- Returns true on success or false and error message on failure
-function client_mt:acknowledge_pubrel(packet_id)
+function Client:acknowledge_pubrel(packet_id)
-- check connection is alive
if not self.connection then
return false, "network connection is not opened"
@@ -803,10 +948,13 @@ function client_mt:acknowledge_pubrel(packet_id)
-- create PUBREL packet
local pubrel = self._make_packet{type=packet_type.PUBREL, packet_id=packet_id, rc=0}
+ log:debug("[LuaMQTT] client '%s' sending PUBREL (packet: %s)", self.opts.id, packet_id or "n.a.")
+
-- send PUBREL packet
local ok, err = self:_send_packet(pubrel)
if not ok then
err = "failed to send PUBREL: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -817,7 +965,7 @@ end
-- Send PUBCOMP acknowledge packet - last phase of QoS 2 exchange
-- Returns true on success or false and error message on failure
-function client_mt:acknowledge_pubcomp(packet_id)
+function Client:acknowledge_pubcomp(packet_id)
-- check connection is alive
if not self.connection then
return false, "network connection is not opened"
@@ -826,10 +974,13 @@ function client_mt:acknowledge_pubcomp(packet_id)
-- create PUBCOMP packet
local pubcomp = self._make_packet{type=packet_type.PUBCOMP, packet_id=packet_id, rc=0}
+ log:debug("[LuaMQTT] client '%s' sending PUBCOMP (packet: %s)", self.opts.id, packet_id or "n.a.")
+
-- send PUBCOMP packet
local ok, err = self:_send_packet(pubcomp)
if not ok then
err = "failed to send PUBCOMP: "..err
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
return false, err
@@ -839,18 +990,18 @@ function client_mt:acknowledge_pubcomp(packet_id)
end
-- Call specified event handlers
-function client_mt:handle(event, ...)
+function Client:handle(event, ...)
local handlers = self.handlers[event]
if not handlers then
error("invalid event '"..tostring(event).."' to handle")
end
- self._handling[event] = true -- protecting self.handlers[event] table from modifications by client_mt:off() when iterating
+ self._handling[event] = true -- protecting self.handlers[event] table from modifications by Client:off() when iterating
for _, handler in ipairs(handlers) do
handler(...)
end
self._handling[event] = nil
- -- process handlers removing, scheduled by client_mt:off()
+ -- process handlers removing, scheduled by Client:off()
local to_remove = self._to_remove_handlers[event]
if to_remove then
for _, func in ipairs(to_remove) do
@@ -862,8 +1013,8 @@ end
-- Internal methods
--- Assign next packet id for given packet creation args
-function client_mt:_assign_packet_id(pargs)
+-- Assign next packet id for given packet creation opts
+function Client:_assign_packet_id(pargs)
if not pargs.packet_id then
if packet_id_required(pargs) then
self._last_packet_id = next_packet_id(self._last_packet_id)
@@ -872,272 +1023,288 @@ function client_mt:_assign_packet_id(pargs)
end
end
--- Receive packet function in sync mode
-local function sync_recv(self)
- return true, self:_receive_packet()
-end
+-- Handle a single received packet
+function Client:handle_received_packet(packet)
+ local conn = self.connection
+ local err
--- Perform one input/output iteration, called by sync receiving loop
-function client_mt:_sync_iteration()
- return self:_io_iteration(sync_recv)
-end
+ log:debug("[LuaMQTT] client '%s' received '%s' (packet: %s)", self.opts.id, packet_type[packet.type], packet.packet_id or "n.a.")
--- Receive packet function - from ioloop's coroutine
-local function ioloop_recv(self)
- return coroutine_resume(self.connection.coro)
-end
+ if not conn.connack then
+ -- expecting only CONNACK packet here
+ if packet.type ~= packet_type.CONNACK then
+ err = "expecting CONNACK but received "..packet.type
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
+ self:handle("error", err, self)
+ self:close_connection("error")
+ self.first_connect = false
+ return false, err
+ end
--- Perform one input/output iteration, called by ioloop
-function client_mt:_ioloop_iteration()
- -- working according state
- local loop = self.ioloop
- local args = self.args
+ -- store connack packet in connection
+ conn.connack = packet
- local conn = self.connection
- if conn then
- -- network connection opened
- -- perform packet receiving using ioloop receive function
- local ok, err
- if loop then
- ok, err = self:_io_iteration(ioloop_recv)
- else
- ok, err = self:_sync_iteration()
+ -- check CONNACK rc
+ if packet.rc ~= 0 then
+ err = str_format("CONNECT failed with CONNACK [rc=%d]: %s", packet.rc, packet:reason_string())
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
+ self:handle("error", err, self, packet)
+ self:handle("connect", packet, self)
+ self:close_connection("connection failed")
+ self.first_connect = false
+ return false, err
end
- if ok then
- -- send PINGREQ if keep_alive interval is reached
- if os_time() - self.send_time >= args.keep_alive then
- self:send_pingreq()
- end
- end
+ log:info("[LuaMQTT] client '%s' connected successfully to '%s:%s'", self.opts.id, conn.host, conn.port)
- return ok, err
+ -- fire connect event
+ if self.opts.clean == "first" then
+ self.opts.clean = false -- reset clean flag to false, so next connection resumes previous session
+ log:debug("[LuaMQTT] client '%s'; switching clean flag to false (was 'first')", self.opts.id)
+ end
+ self:handle("connect", packet, self)
+ self.first_connect = false
else
- -- no connection - first connect, reconnect or remove from ioloop
- if self.first_connect then
- self.first_connect = false
- self:start_connecting()
- elseif args.reconnect then
- if args.reconnect == true then
- self:start_connecting()
- else
- -- reconnect in specified timeout
- if self.reconnect_timer_start then
- if os_time() - self.reconnect_timer_start >= args.reconnect then
- self.reconnect_timer_start = nil
- self:start_connecting()
- else
- if loop then
- loop:can_sleep()
- end
- end
- else
- self.reconnect_timer_start = os_time()
+ -- connection authorized, so process usual packets
+
+ -- handle packet according its type
+ local ptype = packet.type
+ if ptype == packet_type.PINGRESP then -- luacheck: ignore
+ -- PINGREQ answer, clear timeout
+ self.ping_expire_time = nil
+ elseif ptype == packet_type.SUBACK then
+ self:handle("subscribe", packet, self)
+ elseif ptype == packet_type.UNSUBACK then
+ self:handle("unsubscribe", packet, self)
+ elseif ptype == packet_type.PUBLISH then
+ -- check such packet is not waiting for pubrel acknowledge
+ self:handle("message", packet, self)
+ elseif ptype == packet_type.PUBACK then
+ self:handle("acknowledge", packet, self)
+ elseif ptype == packet_type.PUBREC then
+ local packet_id = packet.packet_id
+ if conn.wait_for_pubrec[packet_id] then
+ conn.wait_for_pubrec[packet_id] = nil
+ -- send PUBREL acknowledge
+ if self:acknowledge_pubrel(packet_id) then
+ -- and fire acknowledge event
+ self:handle("acknowledge", packet, self)
end
end
- else
- -- finish working with client
- if loop then
- loop:remove(self)
+ elseif ptype == packet_type.PUBREL then
+ -- second phase of QoS 2 exchange - check we are already acknowledged such packet by PUBREL
+ local packet_id = packet.packet_id
+ if conn.wait_for_pubrel[packet_id] then
+ -- remove packet from waiting for PUBREL packets table
+ conn.wait_for_pubrel[packet_id] = nil
+ -- send PUBCOMP acknowledge
+ return self:acknowledge_pubcomp(packet_id)
end
+ elseif ptype == packet_type.PUBCOMP then --luacheck: ignore
+ -- last phase of QoS 2 exchange
+ -- do nothing here
+ elseif ptype == packet_type.DISCONNECT then
+ self:close_connection("disconnect received from broker")
+ elseif ptype == packet_type.AUTH then
+ self:handle("auth", packet, self)
+ else
+ log:warn("[LuaMQTT] client '%s' don't know how to handle %s", self.opts.id, ptype)
end
end
+ return true
end
--- Performing one IO iteration - receive next packet
-function client_mt:_io_iteration(recv)
- local conn = self.connection
+do
+ -- implict (re)connecting when reading
+ local function implicit_connect(self)
+ local reconnect = self.opts.reconnect
- -- first - try to receive packet
- local ok, packet, err = recv(self)
- -- print("received packet", ok, packet, err)
+ if not self.first_connect and not reconnect then
+ -- this would be a re-connect, but we're not supposed to auto-reconnect
+ log:debug("[LuaMQTT] client '%s' was disconnected and not set to auto-reconnect", self.opts.id)
+ return false, "network connection is not opened"
+ end
- -- check coroutine resume status
- if not ok then
- err = "failed to resume receive packet coroutine: "..tostring(packet)
- self:handle("error", err, self)
- self:close_connection("error")
- return false, err
- end
+ -- should we wait for a timeout between retries?
+ local t_reconnect = (self.last_connect_time or 0) + (reconnect or 0)
+ local t_now = os_time()
+ if t_reconnect > t_now then
+ -- were delaying before retrying, return remaining delay
+ return t_reconnect - t_now
+ end
- -- check for communication error
- if packet == false then
- if err == "closed" then
- self:close_connection("connection closed by broker")
- return false, err
- else
- err = "failed to receive next packet: "..err
- self:handle("error", err, self)
- self:close_connection("error")
- return false, err
+ self.last_connect_time = t_now
+
+ local ok, err = self:start_connecting()
+ if not ok then
+ -- we failed to connect
+ return reconnect, err
+ end
+
+ -- connected succesfully, but don't know how long it took, so return now
+ -- to be nice to other clients. Return 0 to indicate ready-for-reading.
+ return 0
+ end
+
+ --- Performs a single IO loop step.
+ -- It will connect if not connected, will re-connect if set to.
+ -- This should be called repeatedly in a loop. When using the included modules to
+ -- add clients (see `mqtt.loop`), this will be taken care of automatically.
+ --
+ -- The return value is the time after which this method must be called again.
+ -- It can be called sooner, but shouldn't be called later.
+ -- @return[1] `-1`: the socket read timed out, so it is idle. This return code is only
+ -- returned with buffered connectors (luasocket), never for yielding sockets
+ -- (Copas or OpenResty)
+ -- @return[2] `0`: a packet was succesfully handled, so retry immediately, no delays,
+ -- in case additional data is waiting to be read on the socket.
+ -- @return[3] `>0`: The reconnect timer needs a delay before it can retry (calling
+ -- sooner is not a problem, it will only reconnect when the delay
+ -- has actually passed)
+ -- @return[4] nil
+ -- @return[4] error message
+ function Client:step()
+ local conn = self.connection
+ local reconnect = self.opts.reconnect
+
+ -- try and connect if not connected yet
+ if not conn then
+ return implicit_connect(self)
end
- end
- -- check some packet received
- if packet ~= "timeout" and packet ~= "wantread" then
- if not conn.connack then
- -- expecting only CONNACK packet here
- if packet.type ~= packet_type.CONNACK then
- err = "expecting CONNACK but received "..packet.type
+ local packet, err = self:_receive_packet()
+ if not packet then
+ if err == conn.signal_idle then
+ -- connection was idle, nothing happened
+ return -1
+ elseif err == conn.signal_closed then
+ self:close_connection("connection closed by broker")
+ return reconnect and 0, err
+ else
+ err = "failed to receive next packet: "..tostring(err)
+ log:error("[LuaMQTT] client '%s' %s", self.opts.id, err)
self:handle("error", err, self)
self:close_connection("error")
- return false, err
+ return reconnect and 0, err
end
+ end
- -- store connack packet in connection
- conn.connack = packet
+ local ok
+ ok, err = self:handle_received_packet(packet)
+ if not ok then
+ return reconnect and 0, err
+ end
- -- check CONNACK rc
- if packet.rc ~= 0 then
- err = str_format("CONNECT failed with CONNACK [rc=%d]: %s", packet.rc, packet:reason_string())
- self:handle("error", err, self, packet)
- self:handle("connect", packet, self)
- self:close_connection("connection failed")
- return false, err
- end
+ -- succesfully handled packed, maybe there is more, so retry asap
+ return 0
+ end
+end
- -- fire connect event
- self:handle("connect", packet, self)
+-- Fill given connection table with host and port according given opts
+-- uri: mqtt[s]://[username][:password]@host.domain[:port]
+function Client._parse_connection_opts(opts, conn)
+ local uri = assert(conn.uri)
+
+ -- protocol
+ local uriprotocol = uri:match("^([%a%d%-]-)://")
+ if uriprotocol then
+ uriprotocol = uriprotocol:lower()
+ uri = uri:gsub("^[%a%d%-]-://","")
+
+ if uriprotocol == "mqtts" and opts.secure == nil then
+ opts.secure = true -- NOTE: goes into client 'opts' table, not in 'conn'
+
+ elseif uriprotocol == "mqtt" and opts.secure == nil then
+ opts.secure = false -- NOTE: goes into client 'opts' table, not in 'conn'
+
+ elseif uriprotocol == "mqtts" and opts.secure == false then
+ error("cannot use protocol 'mqtts' with 'secure=false'")
+
+ elseif uriprotocol == "mqtt" and opts.secure then
+ error("cannot use protocol 'mqtt' with 'secure=true|table'")
+ end
+ else
+ -- no protocol info found in uri
+ if opts.secure then
+ uriprotocol = "mqtts"
else
- -- connection authorized, so process usual packets
-
- -- handle packet according its type
- local ptype = packet.type
- if ptype == packet_type.PINGRESP then -- luacheck: ignore
- -- PINGREQ answer, nothing to do
- -- TODO: break the connectin in absence of this packet in some timeout
- elseif ptype == packet_type.SUBACK then
- self:handle("subscribe", packet, self)
- elseif ptype == packet_type.UNSUBACK then
- self:handle("unsubscribe", packet, self)
- elseif ptype == packet_type.PUBLISH then
- -- check such packet is not waiting for pubrel acknowledge
- self:handle("message", packet, self)
- elseif ptype == packet_type.PUBACK then
- self:handle("acknowledge", packet, self)
- elseif ptype == packet_type.PUBREC then
- local packet_id = packet.packet_id
- if conn.wait_for_pubrec[packet_id] then
- conn.wait_for_pubrec[packet_id] = nil
- -- send PUBREL acknowledge
- if self:acknowledge_pubrel(packet_id) then
- -- and fire acknowledge event
- self:handle("acknowledge", packet, self)
- end
- end
- elseif ptype == packet_type.PUBREL then
- -- second phase of QoS 2 exchange - check we are already acknowledged such packet by PUBREL
- local packet_id = packet.packet_id
- if conn.wait_for_pubrel[packet_id] then
- -- remove packet from waiting for PUBREL packets table
- conn.wait_for_pubrel[packet_id] = nil
- -- send PUBCOMP acknowledge
- self:acknowledge_pubcomp(packet_id)
- end
- elseif ptype == packet_type.PUBCOMP then --luacheck: ignore
- -- last phase of QoS 2 exchange
- -- do nothing here
- elseif ptype == packet_type.DISCONNECT then
- self:close_connection("disconnect received from broker")
- elseif ptype == packet_type.AUTH then
- self:handle("auth", packet, self)
- -- else
- -- print("unhandled packet:", packet) -- debug
- end
+ uriprotocol = "mqtt"
end
end
- return true
-end
+ conn.protocol = opts.protocol or uriprotocol
+ assert(type(conn.protocol) == "string", "expected protocol to be a string")
+ assert(conn.protocol:match("mqtts?"), "only 'mqtt(s)' protocol is supported in the uri, got '"..tostring(conn.protocol).."'")
+ -- print("protocol: ", uriprotocol)
--- Apply ioloop network timeout to already established connection (if any)
-function client_mt:_apply_network_timeout()
- local conn = self.connection
- if conn then
- local loop = self.ioloop
- if loop then
- -- apply connection timeout
- self.args.connector.settimeout(conn, loop.args.timeout)
-
- -- connection packets receive loop coroutine
- conn.coro = coroutine_create(function()
- while true do
- local packet, err = self:_receive_packet()
- if not packet then
- return false, err
- else
- coroutine_yield(packet)
- end
- end
- end)
-
- -- replace connection recv_func with coroutine-based version
- local sync_recv_func = conn.recv_func
- conn.recv_func = function(...)
- while true do
- local data, err = sync_recv_func(...)
- if not data and (err == "timeout" or err == "wantread") then
- loop.timeouted = true
- coroutine_yield(err)
- else
- return data, err
- end
- end
- end
- conn.sync_recv_func = sync_recv_func
- else
- -- disable connection timeout
- self.args.connector.settimeout(conn, nil)
+ -- creds, host/port
+ local creds, host_port
+ if uri:find("@") then
+ host_port = uri:match("@(.-)$"):lower()
+ creds = uri:gsub("@.-$", "")
+ else
+ host_port = uri
+ end
+ -- print("creds: ", creds)
+ -- print("host_port:", host_port)
- -- replace back usual (blocking) connection recv_func
- if conn.sync_recv_func then
- conn.recv_func = conn.sync_recv_func
- conn.sync_recv_func = nil
- end
+ -- host-port
+ local host, port = host_port:match("^([^:]+):?([^:]*)$")
+ if port and #port > 0 then
+ port = assert(tonumber(port), "port in uri must be numeric, got: '"..port.."'")
+ else
+ port = nil
+ end
+ -- print("port: ", port)
+ -- print("host: ", host)
+ conn.host = opts.host or host
+ assert(type(conn.host) == "string", "expected host to be a string")
+ -- default port
+ conn.port = opts.port or port
+ if not conn.port then
+ if opts.secure then
+ conn.port = 8883 -- default MQTT secure connection port
+ else
+ conn.port = 1883 -- default MQTT connection port
end
end
-end
+ assert(type(conn.port) == "number", "expected port to be a number")
--- Fill given connection table with host and port according given args
-function client_mt._parse_uri(args, conn)
- local host, port = str_match(args.uri, "^([^%s]+):(%d+)$")
- if not host then
- -- trying pattern without port
- host = assert(str_match(conn.uri, "^([^%s]+)$"), "invalid uri format: expecting at least host/ip in .uri")
- end
- if not port then
- if args.secure then
- port = 8883 -- default MQTT secure connection port
- else
- port = 1883 -- default MQTT connection port
+
+ -- username-password
+ local username, password
+ if creds then
+ username, password = creds:match("^([^:]+):?([^:]*)$")
+ if password and #password == 0 then
+ password = nil
end
- else
- port = tonumber(port)
end
- conn.host, conn.port = host, port
-end
+ -- NOTE: these go into client 'opts' table, not in 'conn'
+ opts.username = opts.username or username
+ assert(opts.username == nil or type(opts.username) == "string", "expected username to be a string")
+ opts.password = opts.password or password
+ assert(opts.password == nil or type(opts.password) == "string", "expected password to be a string")
+ assert(not conn.password or conn.username, "password is not accepted in absence of username")
+ -- print("username: ", username)
+ -- print("password: ", password)
--- Creates the conn.secure_params table and its content according client creation args
-function client_mt._apply_secure(args, conn)
- local secure = args.secure
+
+ local secure = opts.secure
if secure then
conn.secure = true
- if type(secure) == "table" then
- conn.secure_params = secure
- else
- conn.secure_params = {
- mode = "client",
- protocol = "tlsv1_2",
- verify = "none",
- options = "all",
- }
- end
- conn.ssl_module = args.ssl_module or "ssl"
+ conn.secure_params = secure ~= true and secure or nil
+ conn.ssl_module = opts.ssl_module or "ssl"
+ assert(conn.ssl_module == nil or type(conn.ssl_module) == "string", "expected ssl_module to be a string")
+ else
+ -- sanity
+ conn.secure = false
+ conn.secure_params = nil
+ conn.ssl_module = nil
end
end
-- Send given packet to opened network connection
-function client_mt:_send_packet(packet)
+function Client:_send_packet(packet)
local conn = self.connection
if not conn then
return false, "network connection is not opened"
@@ -1148,39 +1315,51 @@ function client_mt:_send_packet(packet)
return false, "sending empty packet"
end
-- and send binary packet to network connection
- local i, err = 1
- local send = self.args.connector.send
- while i < len do
- i, err = send(conn, data, i)
- if not i then
- return false, "connector.send failed: "..err
- end
+ local ok, err = conn:send(data)
+ if not ok then
+ return false, "connector.send failed: "..err
end
self.send_time = os_time()
return true
end
-- Receive one packet from established network connection
-function client_mt:_receive_packet()
+function Client:_receive_packet()
local conn = self.connection
if not conn then
return false, "network connection is not opened"
end
- -- parse packet
- local packet, err = self._parse_packet(conn.recv_func)
- if not packet then
+ -- read & parse packet
+ local packet, err = self._parse_packet(
+ function(size)
+ return conn:receive(size)
+ end
+ )
+ if packet then
+ -- succesful packet, clear handled data and return it
+ conn:buffer_clear()
+ return packet
+ end
+
+ -- check if we need more data, if not, clear the buffer because were done with
+ -- the data in that case
+ if err == conn.signal_idle then
+ -- we need more data, so do not clear buffer, just return the error
return false, err
end
- return packet
+
+ -- some other error, can't use buffered data, dispose of it
+ conn:buffer_clear()
+ return false, err
end
-- Represent MQTT client as string
-function client_mt:__tostring()
- return str_format("mqtt.client{id=%q}", tostring(self.args.id))
+function Client:__tostring()
+ return str_format("mqtt.client{id=%q}", tostring(self.opts.id))
end
-- Garbage collection handler
-function client_mt:__gc()
+function Client:__gc()
-- close network connection if it's available, without sending DISCONNECT packet
if self.connection then
self:close_connection("garbage")
@@ -1191,18 +1370,24 @@ end
-- @section exported
--- Create, initialize and return new MQTT client instance
--- @param ... see arguments of client_mt:__init(args)
--- @see client_mt:__init
--- @treturn client_mt MQTT client instance
-function client.create(...)
- local cl = setmetatable({}, client_mt)
- cl:__init(...)
+-- @name client.create
+-- @param ... see arguments of `Client:__init`
+-- @see Client:__init
+-- @treturn Client MQTT client instance
+function _M.create(opts)
+ local cl = setmetatable({}, Client)
+ cl:__init(opts)
return cl
end
-------
+if _G._TEST then
+ -- export functions for test purposes (different name!)
+ _M.__parse_connection_opts = Client._parse_connection_opts
+end
+
-- export module table
-return client
+return _M
-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/connector/base/buffered_base.lua b/mqtt/connector/base/buffered_base.lua
new file mode 100644
index 0000000..48ac0ce
--- /dev/null
+++ b/mqtt/connector/base/buffered_base.lua
@@ -0,0 +1,89 @@
+-- base connector class for buffered reading.
+--
+-- Use this base class if the sockets do NOT yield.
+-- So LuaSocket for example, when using Copas or OpenResty
+-- use the non-buffered base class.
+--
+-- This base class derives from `non_buffered_base` it implements the
+-- `receive` and `buffer_clear` methods. But adds the `plain_receive` method
+-- that must be implemented.
+--
+-- NOTE: the `plain_receive` method is supposed to be non-blocking (see its
+-- description), but the `send` method has no such facilities, so is `blocking`
+-- in this class. Make sure to set the proper timeouts in either method before
+-- starting the send/receive. So for example for LuaSocket call `settimeout(0)`
+-- before receiving, and `settimeout(30)` before sending.
+--
+-- @class mqtt.connector.base.buffered_base
+
+
+local super = require "mqtt.connector.base.non_buffered_base"
+local buffered = setmetatable({}, super)
+buffered.__index = buffered
+buffered.super = super
+buffered.type = "buffered, blocking i/o"
+
+-- debug helper function
+-- function buffered:buffer_state(msg)
+-- print(string.format("buffer: size = %03d last-byte-done = %03d -- %s",
+-- #(self.buffer_string or ""), self.buffer_pointer or 0, msg))
+-- end
+
+-- bytes read were handled, clear those
+function buffered:buffer_clear()
+ -- self:buffer_state("before clearing buffer")
+ self.buffer_string = nil
+ self.buffer_pointer = nil
+end
+
+-- read bytes, first from buffer, remaining from function
+-- if function returns "idle" then reset read pointer
+function buffered:receive(size)
+ -- self:buffer_state("receive start "..size.." bytes")
+
+ local buf = self.buffer_string or ""
+ local idx = self.buffer_pointer or 0
+
+ while size > (#buf - idx) do
+ -- buffer is lacking bytes, read more...
+ local data, err = self:plain_receive(size - (#buf - idx))
+ if not data then
+ if err == self.signal_idle then
+ -- read timedout, retry entire packet later, reset buffer
+ self.buffer_pointer = 0
+ end
+ return data, err
+ end
+
+ -- append received data, and try again
+ buf = buf .. data
+ self.buffer_string = buf
+ -- self:buffer_state("receive added "..#data.." bytes")
+ end
+
+ self.buffer_pointer = idx + size
+ local data = buf:sub(idx + 1, idx + size)
+ -- print("data: ", require("mqtt.tools").hex(data))
+ -- self:buffer_state("receive done "..size.." bytes\n")
+ return data
+end
+
+--- Retrieves the requested number of bytes from the socket, in a non-blocking
+-- manner.
+-- The implementation MUST read with a timeout that immediately returns if there
+-- is no data to read. If there is no data, then it MUST return
+-- `nil, self.signal_idle` to indicate it no data was there and we need to retry later.
+--
+-- If there is partial data, it should return that data (less than the requested
+-- number of bytes), with no error/signal.
+--
+-- If the receive errors, because of a closed connection it should return
+-- `nil, self.signal_closed` to indicate this. Any other errors can be returned
+-- as a regular `nil, err`.
+-- @tparam size int number of bytes to receive.
+-- @return data, or `false, err`, where `err` can be a signal.
+function buffered:plain_receive(size) -- luacheck: ignore
+ error("method 'plain_receive' on buffered connector wasn't implemented")
+end
+
+return buffered
diff --git a/mqtt/connector/base/luasec.lua b/mqtt/connector/base/luasec.lua
new file mode 100644
index 0000000..660dfec
--- /dev/null
+++ b/mqtt/connector/base/luasec.lua
@@ -0,0 +1,29 @@
+-- validates the LuaSec options, and applies defaults
+return function(conn)
+ if conn.secure then
+ local params = conn.secure_params
+ if not params then
+ -- set default LuaSec options
+ conn.secure_params = {
+ mode = "client",
+ protocol = "any",
+ verify = "none",
+ options = {"all", "no_sslv2", "no_sslv3", "no_tlsv1"},
+ }
+ return
+ end
+
+ local ok, ssl = pcall(require, conn.ssl_module)
+ assert(ok, "ssl_module '"..tostring(conn.ssl_module).."' not found, secure connections unavailable")
+
+ assert(type(params) == "table", "expecting .secure_params to be a table, got: "..type(params))
+
+ params.mode = params.mode or "client"
+ assert(params.mode == "client", "secure parameter 'mode' must be set to 'client' if given, got: "..tostring(params.mode))
+
+ local ctx, err = ssl.newcontext(params)
+ if not ctx then
+ error("Couldn't create secure context: "..tostring(err))
+ end
+ end
+end
diff --git a/mqtt/connector/base/non_buffered_base.lua b/mqtt/connector/base/non_buffered_base.lua
new file mode 100644
index 0000000..ded109e
--- /dev/null
+++ b/mqtt/connector/base/non_buffered_base.lua
@@ -0,0 +1,67 @@
+-- base connector class for non-buffered reading.
+--
+-- Use this base class if the sockets DO yield.
+-- So Copas or OpenResty for example, when using LuaSocket
+-- use the buffered base class.
+--
+-- NOTE: when the send operation can also yield (as is the case with Copas and
+-- OpenResty) you should wrap the `send` handler in a lock to prevent a half-send
+-- message from being interleaved by another message send from another thread.
+--
+-- @class mqtt.connector.base.non_buffered_base
+
+
+local non_buffered = {
+ type = "non-buffered, yielding i/o",
+ timeout = 30 -- default timeout
+}
+non_buffered.__index = non_buffered
+
+-- we need to specify signals for these conditions such that the client
+-- doesn't have to rely on magic strings like "timeout", "wantread", etc.
+-- the connector is responsible for translating those connector specific
+-- messages to a generic signal
+non_buffered.signal_idle = {} -- read timeout occured, so we're idle need to come back later and try again
+non_buffered.signal_closed = {} -- remote closed the connection
+
+--- Validate connection options.
+function non_buffered:shutdown() -- luacheck: ignore
+ error("method 'shutdown' on connector wasn't implemented")
+end
+
+--- Clears consumed bytes.
+-- Called by the mqtt client when the consumed bytes from the buffer are handled
+-- and can be cleared from the buffer.
+-- A no-op for the non-buffered classes, since the sockets yield when incomplete.
+function non_buffered.buffer_clear()
+end
+
+--- Retrieves the requested number of bytes from the socket.
+-- If the receive errors, because of a closed connection it should return
+-- `nil, self.signal_closed` to indicate this. Any other errors can be returned
+-- as a regular `nil, err`.
+-- @tparam size int number of retrieve to return.
+-- @return data, or `false, err`, where `err` can be a signal.
+function non_buffered:receive(size) -- luacheck: ignore
+ error("method 'receive' on non-buffered connector wasn't implemented")
+end
+
+--- Open network connection to `self.host` and `self.port`.
+-- @return `true` on success, or `false, err` on failure
+function non_buffered:connect() -- luacheck: ignore
+ error("method 'connect' on connector wasn't implemented")
+end
+
+--- Shutdown the network connection.
+function non_buffered:shutdown() -- luacheck: ignore
+ error("method 'shutdown' on connector wasn't implemented")
+end
+
+--- Shutdown the network connection.
+-- @tparam data string data to send
+-- @return `true` on success, or `false, err` on failure
+function non_buffered:send(data) -- luacheck: ignore
+ error("method 'send' on connector wasn't implemented")
+end
+
+return non_buffered
diff --git a/mqtt/connector/copas.lua b/mqtt/connector/copas.lua
new file mode 100644
index 0000000..9926480
--- /dev/null
+++ b/mqtt/connector/copas.lua
@@ -0,0 +1,121 @@
+--- Copas based connector.
+--
+-- Copas is an advanced coroutine scheduler in pure-Lua. It uses LuaSocket
+-- under the hood, but in a non-blocking way. It also uses LuaSec for TLS
+-- based connections (like the `mqtt.connector.luasocket` one). And hence uses
+-- the same defaults for the `secure` option when creating the `client`.
+--
+-- Caveats:
+--
+-- * the `client` option `ssl_module` is not supported by the Copas connector,
+-- It will always use the module named `ssl`.
+--
+-- * multiple threads can send simultaneously (sending is wrapped in a lock)
+--
+-- * since the client creates a long lived connection for reading, it returns
+-- upon receiving a packet, to call an event handler. The handler must return
+-- swiftly, since while the handler runs the socket will not be reading.
+-- Any task that might take longer than a few milliseconds should be off
+-- loaded to another thread (the Copas-loop will take care of this).
+--
+-- NOTE: you will need to install copas like this: `luarocks install copas`.
+-- @module mqtt.connector.copas
+
+local super = require "mqtt.connector.base.non_buffered_base"
+local connector = setmetatable({}, super)
+connector.__index = connector
+connector.super = super
+
+local socket = require("socket")
+local copas = require("copas")
+local new_lock = require("copas.lock").new
+local validate_luasec = require("mqtt.connector.base.luasec")
+
+
+-- validate connection options
+function connector:validate()
+ if self.secure then
+ assert(self.ssl_module == "ssl" or self.ssl_module == nil, "Copas connector only supports 'ssl' as 'ssl_module'")
+
+ validate_luasec(self)
+ end
+end
+
+-- Open network connection to .host and .port in conn table
+-- Store opened socket to conn table
+-- Returns true on success, or false and error text on failure
+function connector:connect()
+ self:validate()
+ local sock = copas.wrap(socket.tcp(), self.secure_params)
+ copas.setsocketname("mqtt@"..self.host..":"..self.port, sock)
+
+ sock:settimeouts(self.timeout, self.timeout, -1) -- no timout on reading
+
+ local ok, err = sock:connect(self.host, self.port)
+ if not ok then
+ return false, "copas.connect failed: "..err
+ end
+ self.sock = sock
+ self.send_lock = new_lock(30) -- 30 second timeout
+ return true
+end
+
+-- the packet was fully read, we can clear the bufer.
+function connector:buffer_clear()
+ -- since the packet is complete, we wait now indefinitely for the next one
+ self.sock:settimeouts(nil, nil, -1) -- no timeout on reading
+end
+
+-- Shutdown network connection
+function connector:shutdown()
+ self.sock:close()
+ self.send_lock:destroy()
+end
+
+-- Send data to network connection
+function connector:send(data)
+ -- cache locally in case lock/sock gets replaced while we were sending
+ local sock = self.sock
+ local lock = self.send_lock
+
+ local ok, err = lock:get()
+ if not ok then
+ return nil, "failed acquiring send_lock: "..tostring(err)
+ end
+
+ local i = 1
+ while i < #data do
+ i, err = sock:send(data, i)
+ if not i then
+ lock:release()
+ return false, err
+ end
+ end
+ lock:release()
+ return true
+end
+
+-- Receive given amount of data from network connection
+function connector:receive(size)
+ local sock = self.sock
+ local data, err = sock:receive(size)
+ if data then
+ -- bytes received, so change from idefinite timeout to regular until
+ -- packet is complete (see buffer_clear method)
+ self.sock:settimeouts(nil, nil, self.timeout)
+ return data
+ end
+
+ if err == "closed" then
+ return false, self.signal_closed
+ elseif err == "timout" then
+ return false, self.signal_idle
+ else
+ return false, err
+ end
+end
+
+-- export module table
+return connector
+
+-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/connector/init.lua b/mqtt/connector/init.lua
new file mode 100644
index 0000000..20db5fa
--- /dev/null
+++ b/mqtt/connector/init.lua
@@ -0,0 +1,34 @@
+--- Auto detect the connector to use.
+-- The different environments require different socket implementations to work
+-- properly. The 'connectors' are an abstraction to facilitate that without
+-- having to modify the client itself.
+--
+-- This module is will auto-detect the environment and return the proper
+-- module from;
+--
+-- * `mqtt.connector.nginx` for using the non-blocking OpenResty co-socket apis
+--
+-- * `mqtt.connector.copas` for the non-blocking Copas wrapped sockets
+--
+-- * `mqtt.connector.luasocket` for LuaSocket based sockets (blocking)
+--
+-- Since the selection is based on a.o. packages loaded, make sure that in case
+-- of using the `copas` scheduler, you require it before the `mqtt` modules.
+--
+-- Since the `client` defaults to this module (`mqtt.connector`) there typically
+-- is no need to use this directly. When implementing your own connectors,
+-- the included connectors provide good examples of what to look out for.
+-- @module mqtt.connector
+
+local loops = setmetatable({
+ copas = "mqtt.connector.copas",
+ nginx = "mqtt.connector.nginx",
+ ioloop = "mqtt.connector.luasocket"
+}, {
+ __index = function()
+ error("failed to auto-detect connector to use, please set one explicitly", 2)
+ end
+})
+local loop = require("mqtt.loop.detect")()
+
+return require(loops[loop])
diff --git a/mqtt/connector/luasocket.lua b/mqtt/connector/luasocket.lua
new file mode 100644
index 0000000..b5bf004
--- /dev/null
+++ b/mqtt/connector/luasocket.lua
@@ -0,0 +1,142 @@
+--- LuaSocket (and LuaSec) based connector.
+--
+-- This connector works with the blocking LuaSocket sockets. This connector uses
+-- `LuaSec` for TLS connections. This is the connector used for the included
+-- `mqtt.ioloop` scheduler.
+--
+-- When using TLS / MQTTS connections, the `secure` option passed to the `client`
+-- when creating it, can be the standard table of options as used by LuaSec
+-- for creating a context. When omitted the defaults will be;
+-- `{ mode="client", protocol="any", verify="none",
+-- options={ "all", "no_sslv2", "no_sslv3", "no_tlsv1" } }`
+--
+-- Caveats:
+--
+-- * since the client creates a long lived connection for reading, it returns
+-- upon receiving a packet, to call an event handler. The handler must return
+-- swiftly, since while the handler runs the socket will not be reading.
+-- Any task that might take longer than a few milliseconds should be off
+-- loaded to another task.
+--
+-- @module mqtt.connector.luasocket
+
+local super = require "mqtt.connector.base.buffered_base"
+local luasocket = setmetatable({}, super)
+luasocket.__index = luasocket
+luasocket.super = super
+
+local socket = require("socket")
+local validate_luasec = require("mqtt.connector.base.luasec")
+
+
+-- table with error messages that indicate a read timeout
+luasocket.timeout_errors = {
+ timeout = true, -- luasocket
+ wantread = true, -- luasec
+ wantwrite = true, -- luasec
+}
+
+-- validate connection options
+function luasocket:validate()
+ if self.secure then
+ validate_luasec(self)
+ end
+end
+
+-- Open network connection to .host and .port in conn table
+-- Store opened socket to conn table
+-- Returns true on success, or false and error text on failure
+function luasocket:connect()
+ self:validate()
+
+ local ssl
+ if self.secure then
+ ssl = require(self.ssl_module)
+ end
+
+ self:buffer_clear() -- sanity
+ local sock = socket.tcp()
+ sock:settimeout(self.timeout)
+
+ local ok, err = sock:connect(self.host, self.port)
+ if not ok then
+ return false, "socket.connect failed to connect to '"..tostring(self.host)..":"..tostring(self.port).."': "..err
+ end
+
+ if self.secure_params then
+ -- Wrap socket in TLS one
+ do
+ local wrapped
+ wrapped, err = ssl.wrap(sock, self.secure_params)
+ if not wrapped then
+ sock:close(self) -- close TCP level
+ return false, "ssl.wrap() failed: "..tostring(err)
+ end
+ -- replace sock with wrapped secure socket
+ sock = wrapped
+ end
+
+ -- do TLS/SSL initialization/handshake
+ sock:settimeout(self.timeout) -- sanity; again since its now a luasec socket
+ ok, err = sock:dohandshake()
+ if not ok then
+ sock:close()
+ return false, "ssl dohandshake failed: "..tostring(err)
+ end
+ end
+
+ self.sock = sock
+ return true
+end
+
+-- Shutdown network connection
+function luasocket:shutdown()
+ self.sock:close()
+end
+
+-- Send data to network connection
+function luasocket:send(data)
+ local sock = self.sock
+ local i = 0
+ local err
+
+ sock:settimeout(self.timeout)
+
+ while i < #data do
+ i, err = sock:send(data, i + 1)
+ if not i then
+ return false, err
+ end
+ end
+
+ return true
+end
+
+-- Receive given amount of data from network connection
+function luasocket:plain_receive(size)
+ local sock = self.sock
+
+ sock:settimeout(0)
+
+ local data, err, partial = sock:receive(size)
+
+ data = data or partial or ""
+ if #data > 0 then
+ return data
+ end
+
+ -- convert error to signal if required
+ if self.timeout_errors[err or -1] then
+ return false, self.signal_idle
+ elseif err == "closed" then
+ return false, self.signal_closed
+ else
+ return false, err
+ end
+end
+
+
+-- export module table
+return luasocket
+
+-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/connector/nginx.lua b/mqtt/connector/nginx.lua
new file mode 100644
index 0000000..ca7c954
--- /dev/null
+++ b/mqtt/connector/nginx.lua
@@ -0,0 +1,102 @@
+--- Nginx OpenResty co-sockets based connector.
+--
+-- This connector works with the non-blocking openresty sockets. Note that the
+-- secure setting haven't been implemented yet. It will simply use defaults
+-- when doing a TLS handshake.
+--
+-- Caveats:
+--
+-- * sockets cannot cross phase/context boundaries. So all client interaction
+-- must be done from the timer context in which the client threads run.
+--
+-- * multiple threads cannot send simultaneously (simple scenarios will just
+-- work)
+--
+-- * since the client creates a long lived connection for reading, it returns
+-- upon receiving a packet, to call an event handler. The handler must return
+-- swiftly, since while the handler runs the socket will not be reading.
+-- Any task that might take longer than a few milliseconds should be off
+-- loaded to another thread.
+--
+-- * Nginx timers should be short lived because memory is only released after
+-- the context is destroyed. In this case we're using them for prolonged periods
+-- of time, so be aware of this and implement client restarts if required.
+--
+-- thanks to @irimiab: https://github.com/xHasKx/luamqtt/issues/13
+-- @module mqtt.connector.nginx
+
+local super = require "mqtt.connector.base.non_buffered_base"
+local ngxsocket = setmetatable({}, super)
+ngxsocket.__index = ngxsocket
+ngxsocket.super = super
+
+-- load required stuff
+local ngx_socket_tcp = ngx.socket.tcp
+local long_timeout = 7*24*60*60*1000 -- one week
+
+-- validate connection options
+function ngxsocket:validate()
+ if self.secure then
+ assert(self.ssl_module == "ssl", "specifying custom ssl module when using Nginx connector is not supported")
+ assert(self.secure_params == nil or type(self.secure_params) == "table", "expecting .secure_params to be a table if given")
+ -- TODO: validate nginx stuff
+ end
+end
+
+-- Open network connection to .host and .port in conn table
+-- Store opened socket to conn table
+-- Returns true on success, or false and error text on failure
+function ngxsocket:connect()
+ -- TODO: add a lock for sending to prevent multiple threads from writing to
+ -- the same socket simultaneously (see the Copas connector)
+ local sock = ngx_socket_tcp()
+ -- set read-timeout to 'nil' to not timeout at all
+ sock:settimeouts(self.timeout * 1000, self.timeout * 1000, long_timeout) -- no timeout on reading
+ local ok, err = sock:connect(self.host, self.port)
+ if not ok then
+ return false, "socket:connect failed: "..err
+ end
+ if self.secure then
+ sock:sslhandshake()
+ end
+ self.sock = sock
+ return true
+end
+
+-- Shutdown network connection
+function ngxsocket:shutdown()
+ self.sock:close()
+end
+
+-- Send data to network connection
+function ngxsocket:send(data)
+ return self.sock:send(data)
+end
+
+function ngxsocket:buffer_clear()
+ -- since the packet is complete, we wait now indefinitely for the next one
+ self.sock:settimeouts(self.timeout * 1000, self.timeout * 1000, long_timeout) -- no timeout on reading
+end
+
+-- Receive given amount of data from network connection
+function ngxsocket:receive(size)
+ local sock = self.sock
+ local data, err = sock:receive(size)
+ if data then
+ -- bytes received, so change from idefinite timeout to regular until
+ -- packet is complete (see buffer_clear method)
+ self.sock:settimeouts(self.timeout * 1000, self.timeout * 1000, self.timeout * 1000)
+ return data
+ end
+
+ if err == "closed" then
+ return false, self.signal_closed
+ elseif err == "timout" then
+ return false, self.signal_idle
+ else
+ return false, err
+ end
+end
+
+-- export module table
+return ngxsocket
diff --git a/mqtt/init.lua b/mqtt/init.lua
index c6f597c..dc295b6 100644
--- a/mqtt/init.lua
+++ b/mqtt/init.lua
@@ -1,5 +1,11 @@
--- MQTT module
-- @module mqtt
+-- @usage
+-- local client = mqtt.client {
+-- uri = "mqtts://aladdin:soopersecret@mqttbroker.com",
+-- clean = true,
+-- version = mqtt.v50, -- specify constant for MQTT version
+-- }
--[[
MQTT protocol DOC: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
@@ -18,7 +24,6 @@ CONVENTIONS:
-- @tfield number v50 MQTT v5.0 protocol version constant
-- @tfield string _VERSION luamqtt library version string
-- @table mqtt
--- @see mqtt.const
local mqtt = {}
-- copy all values from const module
@@ -28,59 +33,240 @@ for key, value in pairs(const) do
end
-- load required stuff
-local type = type
+local log = require "mqtt.log"
+
local select = select
local require = require
local client = require("mqtt.client")
local client_create = client.create
-local ioloop_get = require("mqtt.ioloop").get
+local ioloop = require("mqtt.ioloop")
+local ioloop_get = ioloop.get
--- Create new MQTT client instance
--- @param ... Same as for mqtt.client.create(...)
--- @see mqtt.client.client_mt:__init
+-- @param ... Same as for `Client.create`(...)
+-- @treturn Client new client instance
+-- @see Client:__init
function mqtt.client(...)
return client_create(...)
end
---- Returns default ioloop instance
+--- Returns default `ioloop` instance. Shortcut to `Ioloop.get`.
-- @function mqtt.get_ioloop
+-- @see Ioloop.get
mqtt.get_ioloop = ioloop_get
---- Run default ioloop for given MQTT clients or functions
--- @param ... MQTT clients or lopp functions to add to ioloop
--- @see mqtt.ioloop.get
--- @see mqtt.ioloop.run_until_clients
+--- Run default `ioloop` for given MQTT clients or functions.
+-- Will not return until all clients/functions have exited.
+-- @param ... MQTT clients or loop functions to add to ioloop, see `Ioloop:add` for details on functions.
+-- @see Ioloop.get
+-- @see Ioloop.run_until_clients
+-- @usage
+-- mqtt.run_ioloop(client1, client2, func1)
function mqtt.run_ioloop(...)
+ log:info("[LuaMQTT] starting default ioloop instance")
local loop = ioloop_get()
for i = 1, select("#", ...) do
local cl = select(i, ...)
loop:add(cl)
- if type(cl) ~= "function" then
- cl:start_connecting()
- end
end
return loop:run_until_clients()
end
---- Run synchronous input/output loop for only one given MQTT client.
--- Provided client's connection will be opened.
--- Client reconnect feature will not work, and keep_alive too.
--- @param cl MQTT client instance to run
-function mqtt.run_sync(cl)
- local ok, err = cl:start_connecting()
+
+--- Validates a topic with wildcards.
+-- @tparam string t wildcard-topic to validate
+-- @treturn[1] string the input topic if valid
+-- @treturn[2] boolean false if invalid
+-- @treturn[2] string error description
+-- @usage local t = assert(mqtt.validate_subscribe_topic("base/+/thermostat/#"))
+function mqtt.validate_subscribe_topic(t)
+ if type(t) ~= "string" then
+ return false, "not a string"
+ end
+ if #t < 1 then
+ return false, "minimum topic length is 1"
+ end
+ do
+ local _, count = t:gsub("#", "")
+ if count > 1 then
+ return false, "wildcard '#' may only appear once"
+ end
+ if count == 1 then
+ if t ~= "#" and not t:find("/#$") then
+ return false, "wildcard '#' must be the last character, and be prefixed with '/' (unless the topic is '#')"
+ end
+ end
+ end
+ do
+ local t1 = "/"..t.."/"
+ local i = 1
+ while i do
+ i = t1:find("+", i)
+ if i then
+ if t1:sub(i-1, i+1) ~= "/+/" then
+ return false, "wildcard '+' must be enclosed between '/' (except at start/end)"
+ end
+ i = i + 1
+ end
+ end
+ end
+ return t
+end
+
+--- Validates a topic without wildcards.
+-- @tparam string t topic to validate
+-- @treturn[1] string the input topic if valid
+-- @treturn[2] boolean false if invalid
+-- @treturn[2] string error description
+-- @usage local t = assert(mqtt.validate_publish_topic("base/living/thermostat/setpoint"))
+function mqtt.validate_publish_topic(t)
+ if type(t) ~= "string" then
+ return false, "not a string"
+ end
+ if #t < 1 then
+ return false, "minimum topic length is 1"
+ end
+ if t:find("+", nil, true) or t:find("#", nil, true) then
+ return false, "wildcards '#', and '+' are not allowed when publishing"
+ end
+ return t
+end
+
+--- Returns a Lua pattern from topic.
+-- Takes a wildcarded-topic and returns a Lua pattern that can be used
+-- to validate if a received topic matches the wildcard-topic
+-- @tparam string t the wildcard topic
+-- @treturn[1] string Lua-pattern that matches the topic and returns the captures
+-- @treturn[2] boolean false if the topic was invalid
+-- @treturn[2] string error description
+-- @usage
+-- local patt = compile_topic_pattern("homes/+/+/#")
+--
+-- local topic = "homes/myhome/living/mainlights/brightness"
+-- local homeid, roomid, varargs = topic:match(patt)
+function mqtt.compile_topic_pattern(t)
+ local ok, err = mqtt.validate_subscribe_topic(t)
if not ok then
- return false, err
+ return ok, err
end
- while cl.connection do
- ok, err = cl:_sync_iteration()
- if not ok then
- return false, err
+ if t == "#" then
+ t = "(.+)" -- matches anything at least 1 character long
+ else
+ -- first replace valid mqtt '+' and '#' with placeholders
+ local hash = string.char(1)
+ t = t:gsub("/#$", "/" .. hash)
+ local plus = string.char(2)
+ t = t:gsub("^%+$", plus)
+ t = t:gsub("^%+/", plus .. "/")
+ local c = 1
+ while c ~= 0 do -- must loop, since adjacent patterns can overlap
+ t, c = t:gsub("/%+/", "/" .. plus .. "/")
end
+ t = t:gsub("/%+$", "/" .. plus)
+
+ -- now escape any special Lua pattern characters
+ t = t:gsub("[%\\%(%)%.%%%+%-%*%?%[%^%$]", function(cap) return "%"..cap end)
+
+ -- finally replace placeholders with captures
+ t = t:gsub(hash,"(.-)") -- match anything, can be empty
+ t = t:gsub(plus,"([^/]-)") -- match anything between '/', can be empty
end
+ return "^"..t.."$"
end
+--- Parses wildcards in a topic into a table.
+-- @tparam string topic incoming topic string
+-- @tparam table opts parsing options table
+-- @tparam string opts.topic the wild-carded topic to match against (optional if `opts.pattern` is given)
+-- @tparam string opts.pattern the compiled pattern for the wild-carded topic (optional if `opts.topic`
+-- is given). If not given then topic will be compiled and the result will be
+-- stored in this field for future use (cache).
+-- @tparam array opts.keys array of field names. The order must be the same as the
+-- order of the wildcards in `topic`
+-- @treturn[1] table `fields`: the array part will have the values of the wildcards, in
+-- the order they appeared. The hash part, will have the field names provided
+-- in `opts.keys`, with the values of the corresponding wildcard. If a `#`
+-- wildcard was used, that one will be the last in the table.
+-- @treturn[1] `varargs`: The returned table is an array, with all segments that were
+-- matched by the `#` wildcard (empty if there was no `#` wildcard).
+-- @treturn[2] boolean `false` if there was no match
+-- @return[3] `false`+err on error, eg. pattern was invalid.
+-- @usage
+-- local opts = {
+-- topic = "homes/+/+/#",
+-- keys = { "homeid", "roomid", "varargs"},
+-- }
+-- local fields, varargst = topic_match("homes/myhome/living/mainlights/brightness", opts)
+--
+-- print(fields[1], fields.homeid) -- "myhome myhome"
+-- print(fields[2], fields.roomid) -- "living living"
+-- print(fields[3], fields.varargs) -- "mainlights/brightness mainlights/brightness"
+--
+-- print(varargst[1]) -- "mainlights"
+-- print(varargst[2]) -- "brightness"
+function mqtt.topic_match(topic, opts)
+ if type(topic) ~= "string" then
+ return false, "expected topic to be a string"
+ end
+ if type(opts) ~= "table" then
+ return false, "expected options to be a table"
+ end
+ local pattern = opts.pattern
+ if not pattern then
+ local ptopic = opts.topic
+ if not ptopic then
+ return false, "either 'opts.topic' or 'opts.pattern' must set"
+ end
+ local err
+ pattern, err = mqtt.compile_topic_pattern(ptopic)
+ if not pattern then
+ return false, "failed to compile 'opts.topic' into pattern: "..tostring(err)
+ end
+ -- store/cache compiled pattern for next time
+ opts.pattern = pattern
+ end
+ local values = { topic:match(pattern) }
+ if values[1] == nil then
+ return false
+ end
+ local keys = opts.keys
+ if keys ~= nil then
+ if type(keys) ~= "table" then
+ return false, "expected 'opts.keys' to be a table (array)"
+ end
+ -- we have a table with keys, copy values to fields
+ for i, value in ipairs(values) do
+ local key = keys[i]
+ if key ~= nil then
+ values[key] = value
+ end
+ end
+ end
+ if not pattern:find("%(%.[%-%+]%)%$$") then -- pattern for "#" as last char
+ -- we're done
+ return values, {}
+ end
+ -- we have a '#' wildcard
+ local vararg = values[#values]
+ local varargs = {}
+ local i = 0
+ local ni = 0
+ while ni do
+ ni = vararg:find("/", i, true)
+ if ni then
+ varargs[#varargs + 1] = vararg:sub(i, ni-1)
+ i = ni + 1
+ else
+ varargs[#varargs + 1] = vararg:sub(i, -1)
+ end
+ end
+
+ return values, varargs
+end
+
+
-- export module table
return mqtt
diff --git a/mqtt/ioloop.lua b/mqtt/ioloop.lua
index b903c9a..63cfb99 100644
--- a/mqtt/ioloop.lua
+++ b/mqtt/ioloop.lua
@@ -1,35 +1,28 @@
---- ioloop module
--- @module mqtt.ioloop
--- @alias ioloop
-
---[[
- ioloop module
-
- In short: allowing you to work with several MQTT clients in one script, and allowing them to maintain
- a long-term connection to broker, using PINGs.
-
- NOTE: this module will work only with MQTT clients using standard luasocket/luasocket_ssl connectors.
-
- In long:
- Providing an IO loop instance dealing with efficient (as much as possible in limited lua IO) network communication
- for several MQTT clients in the same OS thread.
- The main idea is that you are creating an ioloop instance, then adding created and connected MQTT clients to it.
- The ioloop instance is setting a non-blocking mode for sockets in MQTT clients and setting a small timeout
- for their receive/send operations. Then ioloop is starting an endless loop trying to receive/send data for all added MQTT clients.
- You may add more or remove some MQTT clients from the ioloop after it's created and started.
-
- Using that ioloop is allowing you to run a MQTT client for long time, through sending PINGREQ packets to broker
- in keepAlive interval to maintain long-term connection.
-
- Also, any function can be added to the ioloop instance, and it will be called in the same endless loop over and over
- alongside with added MQTT clients to provide you a piece of processor time to run your own logic (like running your own
- network communications or any other thing good working in an io-loop)
-]]
-
--- module table
-local ioloop = {}
+--- This class contains the ioloop implementation.
+--
+-- In short: allowing you to work with several MQTT clients in one script, and allowing them to maintain
+-- a long-term connection to broker, using PINGs. This is the bundled alternative to Copas and Nginx.
+--
+-- NOTE: this module will work only with MQTT clients using the `connector.luasocket` connector.
+--
+-- Providing an IO loop instance dealing with efficient (as much as possible in limited lua IO) network communication
+-- for several MQTT clients in the same OS thread.
+-- The main idea is that you are creating an ioloop instance, then adding MQTT clients to it.
+-- Then ioloop is starting an endless loop trying to receive/send data for all added MQTT clients.
+-- You may add more or remove some MQTT clients to/from the ioloop after it has been created and started.
+--
+-- Using an ioloop is allowing you to run a MQTT client for long time, through sending PINGREQ packets to broker
+-- in keepAlive interval to maintain long-term connection.
+--
+-- Also, any function can be added to the ioloop instance, and it will be called in the same endless loop over and over
+-- alongside with added MQTT clients to provide you a piece of processor time to run your own logic (like running your own
+-- network communications or any other thing good working in an io-loop)
+-- @classmod Ioloop
+
+local _M = {}
-- load required stuff
+local log = require "mqtt.log"
local next = next
local type = type
local ipairs = ipairs
@@ -39,135 +32,214 @@ local setmetatable = setmetatable
local table = require("table")
local tbl_remove = table.remove
+local math = require("math")
+local math_min = math.min
+
--- ioloop instances metatable
--- @type ioloop_mt
-local ioloop_mt = {}
-ioloop_mt.__index = ioloop_mt
-
---- Initialize ioloop instance
--- @tparam table args ioloop creation arguments table
--- @tparam[opt=0.005] number args.timeout network operations timeout in seconds
--- @tparam[opt=0] number args.sleep sleep interval after each iteration
--- @tparam[opt] function args.sleep_function custom sleep function to call after each iteration
--- @treturn ioloop_mt ioloop instance
-function ioloop_mt:__init(args)
- args = args or {}
- args.timeout = args.timeout or 0.005
- args.sleep = args.sleep or 0
- args.sleep_function = args.sleep_function or require("socket").sleep
- self.args = args
+local Ioloop = {}
+Ioloop.__index = Ioloop
+
+--- Initialize ioloop instance.
+-- @tparam table opts ioloop creation options table
+-- @tparam[opt=0] number opts.sleep_min min sleep interval after each iteration
+-- @tparam[opt=0.002] number opts.sleep_step increase in sleep after every idle iteration
+-- @tparam[opt=0.030] number opts.sleep_max max sleep interval after each iteration
+-- @tparam[opt=luasocket.sleep] function opts.sleep_function custom sleep function to call after each iteration
+-- @treturn Ioloop ioloop instance
+function Ioloop:__init(opts)
+ log:debug("[LuaMQTT] initializing ioloop instance '%s'", tostring(self))
+ opts = opts or {}
+ opts.sleep_min = opts.sleep_min or 0
+ opts.sleep_step = opts.sleep_step or 0.002
+ opts.sleep_max = opts.sleep_max or 0.030
+ opts.sleep_function = opts.sleep_function or require("socket").sleep
+ self.opts = opts
self.clients = {}
+ self.timeouts = setmetatable({}, { __mode = "v" })
self.running = false --ioloop running flag, used by MQTT clients which are adding after this ioloop started to run
end
---- Add MQTT client or a loop function to the ioloop instance
--- @tparam client_mt|function client MQTT client or a loop function to add to ioloop
+--- Add MQTT client or a loop function to the ioloop instance.
+-- When adding a function, the function should on each call return the time (in seconds) it wishes to sleep. The ioloop
+-- will sleep after each iteration based on what clients/functions returned. So the function may be called sooner than
+-- the requested time, but will not be called later.
+-- @tparam client_mt|function client MQTT client or a loop function to add to ioloop
-- @return true on success or false and error message on failure
-function ioloop_mt:add(client)
+-- @usage
+-- -- create a timer on a 1 second interval
+-- local timer do
+-- local interval = 1
+-- local next_call = socket.gettime() + interval
+-- timer = function()
+-- if next_call >= socket.gettime() then
+--
+-- -- do stuff here
+--
+-- next_call = socket.gettime() + interval
+-- return interval
+-- else
+-- return next_call - socket.gettime()
+-- end
+-- end
+-- end
+--
+-- loop:add(timer)
+function Ioloop:add(client)
local clients = self.clients
if clients[client] then
- return false, "such MQTT client or loop function is already added to this ioloop"
+ if type(client) == "table" then
+ log:warn("[LuaMQTT] client '%s' was already added to ioloop '%s'", client.opts.id, tostring(self))
+ return false, "MQTT client was already added to this ioloop"
+ else
+ log:warn("[LuaMQTT] loop function '%s' was already added to this ioloop '%s'", tostring(client), tostring(self))
+ return false, "MQTT loop function was already added to this ioloop"
+ end
end
clients[#clients + 1] = client
clients[client] = true
-
- -- associate ioloop with adding MQTT client
- if type(client) ~= "function" then
- client:set_ioloop(self)
+ self.timeouts[client] = self.opts.sleep_min
+
+ if type(client) == "table" then
+ log:info("[LuaMQTT] adding client '%s' to ioloop '%s'", client.opts.id, tostring(self))
+ -- create and add function for PINGREQ
+ local function f()
+ if not clients[client] then
+ -- the client were supposed to do keepalive for is gone, remove ourselves
+ self:remove(f)
+ end
+ return client:check_keep_alive()
+ end
+ -- add it to start doing keepalive checks
+ self:add(f)
+ else
+ log:info("[LuaMQTT] adding function '%s' to ioloop '%s'", tostring(client), tostring(self))
end
return true
end
--- Remove MQTT client or a loop function from the ioloop instance
--- @tparam client_mt|function client MQTT client or a loop function to remove from ioloop
+-- @tparam client_mt|function client MQTT client or a loop function to remove from ioloop
-- @return true on success or false and error message on failure
-function ioloop_mt:remove(client)
+function Ioloop:remove(client)
local clients = self.clients
if not clients[client] then
- return false, "no such MQTT client or loop function was added to ioloop"
+ if type(client) == "table" then
+ log:warn("[LuaMQTT] client not found '%s' in ioloop '%s'", client.opts.id, tostring(self))
+ return false, "MQTT client not found"
+ else
+ log:warn("[LuaMQTT] loop function not found '%s' in ioloop '%s'", tostring(client), tostring(self))
+ return false, "MQTT loop function not found"
+ end
end
- clients[client] = nil
-- search an index of client to remove
for i, item in ipairs(clients) do
if item == client then
+ -- found it, remove
tbl_remove(clients, i)
+ clients[client] = nil
break
end
end
- -- unlink ioloop from MQTT client
- if type(client) ~= "function" then
- client:set_ioloop(nil)
+ if type(client) == "table" then
+ log:info("[LuaMQTT] removed client '%s' from ioloop '%s'", client.opts.id, tostring(self))
+ else
+ log:info("[LuaMQTT] removed loop function '%s' from ioloop '%s'", tostring(client), tostring(self))
end
return true
end
---- Perform one ioloop iteration
-function ioloop_mt:iteration()
- self.timeouted = false
+--- Perform one ioloop iteration.
+-- TODO: make this smarter do not wake-up functions or clients returning a longer
+-- sleep delay. Currently they will be tried earlier if another returns a smaller delay.
+function Ioloop:iteration()
+ local opts = self.opts
+ local sleep = opts.sleep_max
+
for _, client in ipairs(self.clients) do
+ local t, err
+ -- read data and handle events
if type(client) ~= "function" then
- client:_ioloop_iteration()
+ t, err = client:step()
else
- client()
+ t = client() or opts.sleep_max
end
+ if t == -1 then
+ -- no data read, client is idle, step up timeout
+ t = math_min(self.timeouts[client] + opts.sleep_step, opts.sleep_max)
+ self.timeouts[client] = t
+ elseif not t then
+ -- an error from a client was returned
+ if not client.opts.reconnect then
+ -- error and not reconnecting, remove the client
+ log:info("[LuaMQTT] client '%s' returned '%s', no re-connect set, removing client", client.opts.id, err)
+ self:remove(client)
+ t = opts.sleep_max
+ else
+ -- error, but will reconnect
+ log:error("[LuaMQTT] client '%s' failed with '%s', will try re-connecting", client.opts.id, err)
+ t = opts.sleep_min -- try asap
+ end
+ else
+ -- a number of seconds was returned
+ t = math_min(t, opts.sleep_max)
+ self.timeouts[client] = opts.sleep_min
+ end
+ sleep = math_min(sleep, t)
end
-- sleep a bit
- local args = self.args
- local sleep = args.sleep
if sleep > 0 then
- args.sleep_function(sleep)
+ opts.sleep_function(sleep)
end
end
---- Perform sleep if no one of the network operation in current iteration was not timeouted
-function ioloop_mt:can_sleep()
- if not self.timeouted then
- local args = self.args
- args.sleep_function(args.timeout)
- self.timeouted = true
- end
-end
+--- Run the ioloop.
+-- While there is at least one client/function in the ioloop it will continue
+-- iterating. After all clients/functions are gone, it will return.
+function Ioloop:run_until_clients()
+ log:info("[LuaMQTT] ioloop started with %d clients/functions", #self.clients)
---- Run ioloop until at least one client are in ioloop
-function ioloop_mt:run_until_clients()
self.running = true
while next(self.clients) do
self:iteration()
end
self.running = false
+
+ log:info("[LuaMQTT] ioloop finished with %d clients/functions", #self.clients)
end
--------
+--- Exported functions
+-- @section exported
+
--- Create IO loop instance with given options
--- @see ioloop_mt:__init
--- @treturn ioloop_mt ioloop instance
-local function ioloop_create(args)
- local inst = setmetatable({}, ioloop_mt)
- inst:__init(args)
+-- @name ioloop.create
+-- @see Ioloop:__init
+-- @treturn Ioloop ioloop instance
+function _M.create(opts)
+ local inst = setmetatable({}, Ioloop)
+ inst:__init(opts)
return inst
end
-ioloop.create = ioloop_create
-- Default ioloop instance
local ioloop_instance
--- Returns default ioloop instance
+-- @name ioloop.get
-- @tparam[opt=true] boolean autocreate Automatically create ioloop instance
--- @tparam[opt] table args Arguments for creating ioloop instance
--- @treturn ioloop_mt ioloop instance
-function ioloop.get(autocreate, args)
+-- @tparam[opt] table opts Arguments for creating ioloop instance
+-- @treturn Ioloop ioloop instance
+function _M.get(autocreate, opts)
if autocreate == nil then
autocreate = true
end
- if autocreate then
- if not ioloop_instance then
- ioloop_instance = ioloop_create(args)
- end
+ if autocreate and not ioloop_instance then
+ log:info("[LuaMQTT] auto-creating default ioloop instance")
+ ioloop_instance = _M.create(opts)
end
return ioloop_instance
end
@@ -175,6 +247,6 @@ end
-------
-- export module table
-return ioloop
+return _M
-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/log.lua b/mqtt/log.lua
new file mode 100644
index 0000000..42a6964
--- /dev/null
+++ b/mqtt/log.lua
@@ -0,0 +1,17 @@
+-- logging
+
+-- returns a LuaLogging compatible logger object if LuaLogging was already loaded
+-- otherwise returns a stub
+
+local ll = package.loaded.logging
+if ll and type(ll) == "table" and ll.defaultLogger and
+ tostring(ll._VERSION):find("LuaLogging") then
+ -- default LuaLogging logger is available
+ return ll.defaultLogger()
+else
+ -- just use a stub logger with only no-op functions
+ local nop = function() end
+ return setmetatable({}, {
+ __index = function(self, key) self[key] = nop return nop end
+ })
+end
diff --git a/mqtt/loop/copas.lua b/mqtt/loop/copas.lua
new file mode 100644
index 0000000..94b4d13
--- /dev/null
+++ b/mqtt/loop/copas.lua
@@ -0,0 +1,72 @@
+--- Copas specific client handling module.
+-- Typically this module is not used directly, but through `mqtt.loop` when
+-- auto-detecting the environment.
+-- @module mqtt.loop.copas
+
+local copas = require "copas"
+local log = require "mqtt.log"
+
+local client_registry = {}
+
+local _M = {}
+
+
+--- Add MQTT client to the Copas scheduler.
+-- Each received packet will be handled by a new thread, such that the thread
+-- listening on the socket can return immediately.
+-- The client will automatically be removed after it exits. It will set up a
+-- thread to call `Client:check_keep_alive`.
+-- @param cl mqtt-client to add to the Copas scheduler
+-- @return `true` on success or `false` and error message on failure
+function _M.add(cl)
+ if client_registry[cl] then
+ log:warn("[LuaMQTT] client '%s' was already added to Copas", cl.opts.id)
+ return false, "MQTT client was already added to Copas"
+ end
+ client_registry[cl] = true
+
+ do -- make mqtt device async for incoming packets
+ local handle_received_packet = cl.handle_received_packet
+ local count = 0
+ -- replace packet handler; create a new thread for each packet received
+ cl.handle_received_packet = function(mqttdevice, packet)
+ count = count + 1
+ copas.addnamedthread(handle_received_packet, cl.opts.id..":receive_"..count, mqttdevice, packet)
+ return true
+ end
+ end
+
+ -- add keep-alive timer
+ local timer = copas.addnamedthread(function()
+ while client_registry[cl] do
+ local next_check = cl:check_keep_alive()
+ if next_check > 0 then
+ copas.pause(next_check)
+ end
+ end
+ end, cl.opts.id .. ":keep_alive")
+
+ -- add client to connect and listen
+ copas.addnamedthread(function()
+ while client_registry[cl] do
+ local timeout = cl:step()
+ if not timeout then
+ client_registry[cl] = nil -- exiting
+ log:debug("[LuaMQTT] client '%s' exited, removed from Copas", cl.opts.id)
+ copas.wakeup(timer)
+ else
+ if timeout > 0 then
+ copas.pause(timeout)
+ end
+ end
+ end
+ end, cl.opts.id .. ":listener")
+
+ return true
+end
+
+return setmetatable(_M, {
+ __call = function(self, ...)
+ return self.add(...)
+ end,
+})
diff --git a/mqtt/loop/detect.lua b/mqtt/loop/detect.lua
new file mode 100644
index 0000000..eaa8cae
--- /dev/null
+++ b/mqtt/loop/detect.lua
@@ -0,0 +1,30 @@
+--- Module returns a single function to detect the io-loop in use.
+-- Either 'copas', 'nginx', or 'ioloop', or nil+error
+local log = require "mqtt.log"
+
+local loop
+return function()
+ if loop then return loop end
+ if type(ngx) == "table" then
+ -- there is a global 'ngx' table, so we're running OpenResty
+ log:info("[LuaMQTT] auto-detected Nginx as the runtime environment")
+ loop = "nginx"
+ return loop
+
+ elseif package.loaded.copas then
+ -- 'copas' was already loaded
+ log:info("[LuaMQTT] auto-detected Copas as the io-loop in use")
+ loop = "copas"
+ return loop
+
+ elseif pcall(require, "socket") and tostring(require("socket")._VERSION):find("LuaSocket") then
+ -- LuaSocket is available
+ log:info("[LuaMQTT] auto-detected LuaSocket as the socket library to use with mqtt-ioloop")
+ loop = "ioloop"
+ return loop
+
+ else
+ -- unknown
+ return nil, "LuaMQTT io-loop/connector auto-detection failed, please specify one explicitly"
+ end
+end
diff --git a/mqtt/loop/init.lua b/mqtt/loop/init.lua
new file mode 100644
index 0000000..d499c2e
--- /dev/null
+++ b/mqtt/loop/init.lua
@@ -0,0 +1,37 @@
+--- Auto detect the IO loop to use.
+-- Interacting with the supported IO loops (ioloop, copas, and nginx) requires
+-- specific implementations to get it right.
+-- This module will auto-detect the environment and return the proper
+-- module from;
+--
+-- * `mqtt.loop.ioloop`
+--
+-- * `mqtt.loop.copas`
+--
+-- * `mqtt.loop.nginx`
+--
+-- Since the selection is based on a.o. packages loaded, make sure that in case
+-- of using the `copas` scheduler, you require it before the `mqtt` modules.
+--
+-- @usage
+-- --local copas = require "copas" -- only if you use Copas
+-- local mqtt = require "mqtt"
+-- local add_client = require("mqtt.loop").add -- returns a loop-specific function
+--
+-- local client = mqtt.create { ... options ... }
+-- add_client(client) -- works for ioloop, copas, and nginx
+--
+-- @module mqtt.loop
+
+local loops = setmetatable({
+ copas = "mqtt.loop.copas",
+ nginx = "mqtt.loop.nginx",
+ ioloop = "mqtt.loop.ioloop"
+}, {
+ __index = function()
+ error("failed to auto-detect connector to use, please set one explicitly", 2)
+ end
+})
+local loop = require("mqtt.loop.detect")()
+
+return require(loops[loop])
diff --git a/mqtt/loop/ioloop.lua b/mqtt/loop/ioloop.lua
new file mode 100644
index 0000000..d585651
--- /dev/null
+++ b/mqtt/loop/ioloop.lua
@@ -0,0 +1,24 @@
+--- IOloop specific client handling module.
+-- Typically this module is not used directly, but through `mqtt.loop` when
+-- auto-detecting the environment.
+-- @module mqtt.loop.ioloop
+
+local _M = {}
+
+local mqtt = require "mqtt"
+
+--- Add MQTT client to the integrated ioloop.
+-- The client will automatically be removed after it exits. It will set up a
+-- function to call `Client:check_keep_alive` in the ioloop.
+-- @param client mqtt-client to add to the ioloop
+-- @return `true` on success or `false` and error message on failure
+function _M.add(client)
+ local default_loop = mqtt.get_ioloop()
+ return default_loop:add(client)
+end
+
+return setmetatable(_M, {
+ __call = function(self, ...)
+ return self.add(...)
+ end,
+})
diff --git a/mqtt/loop/nginx.lua b/mqtt/loop/nginx.lua
new file mode 100644
index 0000000..d3d944d
--- /dev/null
+++ b/mqtt/loop/nginx.lua
@@ -0,0 +1,76 @@
+--- Nginx specific client handling module.
+-- Typically this module is not used directly, but through `mqtt.loop` when
+-- auto-detecting the environment.
+-- @module mqtt.loop.nginx
+
+local client_registry = {}
+
+local _M = {}
+
+
+--- Add MQTT client to the Nginx environment.
+-- The client will automatically be removed after it exits. It will set up a
+-- thread to call `Client:check_keep_alive`.
+-- @param client mqtt-client to add to the Nginx environment
+-- @return `true` on success or `false` and error message on failure
+function _M.add(client)
+ if client_registry[client] then
+ ngx.log(ngx.WARN, "MQTT client '%s' was already added to Nginx", client.opts.id)
+ return false, "MQTT client was already added to Nginx"
+ end
+
+ do -- make mqtt device async for incoming packets
+ local handle_received_packet = client.handle_received_packet
+
+ -- replace packet handler; create a new thread for each packet received
+ client.handle_received_packet = function(mqttdevice, packet)
+ ngx.thread.spawn(handle_received_packet, mqttdevice, packet)
+ return true
+ end
+ end
+
+
+ local ok, err = ngx.timer.at(0, function()
+ -- spawn a thread to listen on the socket
+ local coro = ngx.thread.spawn(function()
+ while true do
+ local sleeptime = client:step()
+ if not sleeptime then
+ ngx.log(ngx.INFO, "MQTT client '", client.opts.id, "' exited, stopping client-thread")
+ client_registry[client] = nil
+ return
+ else
+ if sleeptime > 0 then
+ ngx.sleep(sleeptime * 1000)
+ end
+ end
+ end
+ end)
+
+ -- endless keep-alive loop
+ while not ngx.worker.exiting() do
+ ngx.sleep((client:check_keep_alive())) -- double (()) to trim to 1 argument
+ end
+
+ -- exiting
+ client_registry[client] = nil
+ ngx.log(ngx.DEBUG, "MQTT client '", client.opts.id, "' keep-alive loop exited")
+ client:disconnect()
+ ngx.thread.wait(coro)
+ ngx.log(ngx.DEBUG, "MQTT client '", client.opts.id, "' exit complete")
+ end)
+
+ if not ok then
+ ngx.log(ngx.CRIT, "Failed to start timer-context for device '", client.id,"': ", err)
+ return false, "timer failed: " .. err
+ end
+
+ return true
+end
+
+
+return setmetatable(_M, {
+ __call = function(self, ...)
+ return self.add(...)
+ end,
+})
diff --git a/mqtt/luasocket-copas.lua b/mqtt/luasocket-copas.lua
deleted file mode 100644
index 069229e..0000000
--- a/mqtt/luasocket-copas.lua
+++ /dev/null
@@ -1,48 +0,0 @@
--- DOC: https://keplerproject.github.io/copas/
--- NOTE: you will need to install copas like this: luarocks install copas
-
--- module table
-local connector = {}
-
-local socket = require("socket")
-local copas = require("copas")
-
--- Open network connection to .host and .port in conn table
--- Store opened socket to conn table
--- Returns true on success, or false and error text on failure
-function connector.connect(conn)
- local sock, err = socket.connect(conn.host, conn.port)
- if not sock then
- return false, "socket.connect failed: "..err
- end
- conn.sock = sock
- return true
-end
-
--- Shutdown network connection
-function connector.shutdown(conn)
- conn.sock:shutdown()
-end
-
--- Send data to network connection
-function connector.send(conn, data, i, j)
- local ok, err = copas.send(conn.sock, data, i, j)
- return ok, err
-end
-
--- Receive given amount of data from network connection
-function connector.receive(conn, size)
- local ok, err = copas.receive(conn.sock, size)
- return ok, err
-end
-
--- Set connection's socket to non-blocking mode and set a timeout for it
-function connector.settimeout(conn, timeout)
- conn.timeout = timeout
- conn.sock:settimeout(0)
-end
-
--- export module table
-return connector
-
--- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/luasocket.lua b/mqtt/luasocket.lua
deleted file mode 100644
index 5b8de2b..0000000
--- a/mqtt/luasocket.lua
+++ /dev/null
@@ -1,52 +0,0 @@
--- DOC: http://w3.impa.br/~diego/software/luasocket/tcp.html
-
--- module table
-local luasocket = {}
-
-local socket = require("socket")
-
--- Open network connection to .host and .port in conn table
--- Store opened socket to conn table
--- Returns true on success, or false and error text on failure
-function luasocket.connect(conn)
- local sock, err = socket.connect(conn.host, conn.port)
- if not sock then
- return false, "socket.connect failed: "..err
- end
- conn.sock = sock
- return true
-end
-
--- Shutdown network connection
-function luasocket.shutdown(conn)
- conn.sock:shutdown()
-end
-
--- Send data to network connection
-function luasocket.send(conn, data, i, j)
- local ok, err = conn.sock:send(data, i, j)
- -- print(" luasocket.send:", ok, err, require("mqtt.tools").hex(data))
- return ok, err
-end
-
--- Receive given amount of data from network connection
-function luasocket.receive(conn, size)
- local ok, err = conn.sock:receive(size)
- -- if ok then
- -- print(" luasocket.receive:", size, require("mqtt.tools").hex(ok))
- -- elseif err ~= "timeout" then
- -- print(" luasocket.receive:", ok, err)
- -- end
- return ok, err
-end
-
--- Set connection's socket to non-blocking mode and set a timeout for it
-function luasocket.settimeout(conn, timeout)
- conn.timeout = timeout
- conn.sock:settimeout(timeout, "t")
-end
-
--- export module table
-return luasocket
-
--- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/luasocket_ssl.lua b/mqtt/luasocket_ssl.lua
deleted file mode 100644
index 15b31cb..0000000
--- a/mqtt/luasocket_ssl.lua
+++ /dev/null
@@ -1,56 +0,0 @@
--- DOC: http://w3.impa.br/~diego/software/luasocket/tcp.html
-
--- module table
-local luasocket_ssl = {}
-
-local type = type
-local assert = assert
-local luasocket = require("mqtt.luasocket")
-
--- Open network connection to .host and .port in conn table
--- Store opened socket to conn table
--- Returns true on success, or false and error text on failure
-function luasocket_ssl.connect(conn)
- assert(type(conn.secure_params) == "table", "expecting .secure_params to be a table")
-
- -- open usual TCP connection
- local ok, err = luasocket.connect(conn)
- if not ok then
- return false, "luasocket connect failed: "..err
- end
- local wrapped
-
- -- load right ssl module
- local ssl = require(conn.ssl_module or "ssl")
-
- -- TLS/SSL initialization
- wrapped, err = ssl.wrap(conn.sock, conn.secure_params)
- if not wrapped then
- conn.sock:shutdown()
- return false, "ssl.wrap() failed: "..err
- end
- ok = wrapped:dohandshake()
- if not ok then
- conn.sock:shutdown()
- return false, "ssl dohandshake failed"
- end
-
- -- replace sock in connection table with wrapped secure socket
- conn.sock = wrapped
- return true
-end
-
--- Shutdown network connection
-function luasocket_ssl.shutdown(conn)
- conn.sock:close()
-end
-
--- Copy original methods from mqtt.luasocket module
-luasocket_ssl.send = luasocket.send
-luasocket_ssl.receive = luasocket.receive
-luasocket_ssl.settimeout = luasocket.settimeout
-
--- export module table
-return luasocket_ssl
-
--- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/mqtt/ngxsocket.lua b/mqtt/ngxsocket.lua
deleted file mode 100644
index 349780c..0000000
--- a/mqtt/ngxsocket.lua
+++ /dev/null
@@ -1,55 +0,0 @@
--- module table
--- thanks to @irimiab: https://github.com/xHasKx/luamqtt/issues/13
-local ngxsocket = {}
-
--- load required stuff
-local string_sub = string.sub
-local ngx_socket_tcp = ngx.socket.tcp -- luacheck: ignore
-
--- Open network connection to .host and .port in conn table
--- Store opened socket to conn table
--- Returns true on success, or false and error text on failure
-function ngxsocket.connect(conn)
- local socket = ngx_socket_tcp()
- socket:settimeout(0x7FFFFFFF)
- local sock, err = socket:connect(conn.host, conn.port)
- if not sock then
- return false, "socket:connect failed: "..err
- end
- if conn.secure then
- socket:sslhandshake()
- end
- conn.sock = socket
- return true
-end
-
--- Shutdown network connection
-function ngxsocket.shutdown(conn)
- conn.sock:close()
-end
-
--- Send data to network connection
-function ngxsocket.send(conn, data, i, j)
- if i then
- return conn.sock:send(string_sub(data, i, j))
- else
- return conn.sock:send(data)
- end
-end
-
--- Receive given amount of data from network connection
-function ngxsocket.receive(conn, size)
- return conn.sock:receive(size)
-end
-
--- Set connection's socket to non-blocking mode and set a timeout for it
-function ngxsocket.settimeout(conn, timeout)
- if not timeout then
- conn.sock:settimeout(0x7FFFFFFF)
- else
- conn.sock:settimeout(timeout * 1000)
- end
-end
-
--- export module table
-return ngxsocket
diff --git a/mqtt/protocol.lua b/mqtt/protocol.lua
index 3e6233c..ae34c43 100644
--- a/mqtt/protocol.lua
+++ b/mqtt/protocol.lua
@@ -556,14 +556,14 @@ function protocol.start_parse_packet(read_func)
-- DOC[v5.0]: https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901020
byte1, err = read_func(1)
if not byte1 then
- return false, "failed to read first byte: "..err
+ return false, err
end
byte1 = str_byte(byte1, 1, 1)
local ptype = rshift(byte1, 4)
local flags = band(byte1, 0xF)
len, err = parse_var_length(read_func)
if not len then
- return false, "failed to parse remaining length: "..err
+ return false, err
end
-- create packet parser instance (aka input)
@@ -574,14 +574,14 @@ function protocol.start_parse_packet(read_func)
data = ""
end
if not data then
- return false, "failed to read packet data: "..err
+ return false, err
end
input.available = data:len()
-- read data function for the input instance
input.read_func = function(size)
if size > input.available then
- return false, "not enough data to read size: "..size
+ return false, size
end
local off = input[1]
local res = str_sub(data, off, off + size - 1)
diff --git a/mqtt/protocol5.lua b/mqtt/protocol5.lua
index 2e9088d..9055d74 100644
--- a/mqtt/protocol5.lua
+++ b/mqtt/protocol5.lua
@@ -189,8 +189,8 @@ local property_pairs = {
make = make_uint8_0_or_1,
parse = parse_uint8_0_or_1, },
{ 0x26, "user_property", -- NOTE: not implemented intentionally
- make = function(value_) error("not implemented") end,
- parse = function(read_func_) error("not implemented") end, },
+ make = function(value_) error("not implemented") end, -- luacheck: ignore
+ parse = function(read_func_) error("not implemented") end, }, -- luacheck: ignore
{ 0x27, "maximum_packet_size",
make = make_uint32,
parse = parse_uint32, },
@@ -1324,7 +1324,7 @@ local function parse_packet_unsuback(ptype, flags, input)
end
-- Parse PINGREQ packet, DOC: 3.12 PINGREQ – PING request
-local function parse_packet_pingreq(ptype, flags, input_)
+local function parse_packet_pingreq(ptype, flags, _)
-- DOC: 3.12.1 PINGREQ Fixed Header
if flags ~= 0 then -- Reserved
return false, packet_type[ptype]..": unexpected flags value: "..flags
@@ -1333,7 +1333,7 @@ local function parse_packet_pingreq(ptype, flags, input_)
end
-- Parse PINGRESP packet, DOC: 3.13 PINGRESP – PING response
-local function parse_packet_pingresp(ptype, flags, input_)
+local function parse_packet_pingresp(ptype, flags, _)
-- DOC: 3.13.1 PINGRESP Fixed Header
if flags ~= 0 then -- Reserved
return false, packet_type[ptype]..": unexpected flags value: "..flags
diff --git a/rockspecs/luamqtt-3.4.3-1.rockspec b/rockspecs/luamqtt-3.4.3-1.rockspec
index 45a658f..a48cfa9 100644
--- a/rockspecs/luamqtt-3.4.3-1.rockspec
+++ b/rockspecs/luamqtt-3.4.3-1.rockspec
@@ -26,13 +26,22 @@ build = {
["mqtt.ioloop"] = "mqtt/ioloop.lua",
["mqtt.bit53"] = "mqtt/bit53.lua",
["mqtt.bitwrap"] = "mqtt/bitwrap.lua",
- ["mqtt.luasocket"] = "mqtt/luasocket.lua",
- ["mqtt.luasocket_ssl"] = "mqtt/luasocket_ssl.lua",
- ["mqtt.luasocket-copas"] = "mqtt/luasocket-copas.lua",
- ["mqtt.ngxsocket"] = "mqtt/ngxsocket.lua",
["mqtt.protocol"] = "mqtt/protocol.lua",
["mqtt.protocol4"] = "mqtt/protocol4.lua",
["mqtt.protocol5"] = "mqtt/protocol5.lua",
["mqtt.tools"] = "mqtt/tools.lua",
+ ["mqtt.log"] = "mqtt/log.lua",
+ ["mqtt.connector.init"] = "mqtt/connector/init.lua",
+ ["mqtt.connector.base.buffered_base"] = "mqtt/connector/base/buffered_base.lua",
+ ["mqtt.connector.base.non_buffered_base"] = "mqtt/connector/base/non_buffered_base.lua",
+ ["mqtt.connector.base.luasec"] = "mqtt/connector/base/luasec.lua",
+ ["mqtt.connector.luasocket"] = "mqtt/connector/luasocket.lua",
+ ["mqtt.connector.copas"] = "mqtt/connector/copas.lua",
+ ["mqtt.connector.nginx"] = "mqtt/connector/nginx.lua",
+ ["mqtt.loop.init"] = "mqtt/loop/init.lua",
+ ["mqtt.loop.detect"] = "mqtt/loop/detect.lua",
+ ["mqtt.loop.ioloop"] = "mqtt/loop/ioloop.lua",
+ ["mqtt.loop.copas"] = "mqtt/loop/copas.lua",
+ ["mqtt.loop.nginx"] = "mqtt/loop/nginx.lua",
},
}
diff --git a/tests/configure-log.lua b/tests/configure-log.lua
new file mode 100644
index 0000000..acfd980
--- /dev/null
+++ b/tests/configure-log.lua
@@ -0,0 +1,17 @@
+local ansicolors = require("ansicolors") -- https://github.com/kikito/ansicolors.lua
+local ll = require("logging")
+require "logging.console"
+
+-- configure the default logger used when testing
+ll.defaultLogger(ll.console {
+ logLevel = ll.DEBUG,
+ destination = "stderr",
+ timestampPattern = "%y-%m-%d %H:%M:%S",
+ logPatterns = {
+ [ll.DEBUG] = ansicolors("%date%{cyan} %level %message %{reset}(%source)\n"),
+ [ll.INFO] = ansicolors("%date %level %message\n"),
+ [ll.WARN] = ansicolors("%date%{yellow} %level %message\n"),
+ [ll.ERROR] = ansicolors("%date%{red bright} %level %message %{reset}(%source)\n"),
+ [ll.FATAL] = ansicolors("%date%{magenta bright} %level %message %{reset}(%source)\n"),
+ }
+})
diff --git a/tests/run-for-all-lua-versions.sh b/tests/run-for-all-lua-versions.sh
index 7bcf489..948c9c1 100755
--- a/tests/run-for-all-lua-versions.sh
+++ b/tests/run-for-all-lua-versions.sh
@@ -37,6 +37,9 @@ for ver in -l5.1 -l5.2 -l5.3 -l5.4 -j2.0 -j2.1; do
fi
luarocks install busted > /dev/null
luarocks install copas > /dev/null
+ luarocks install ansicolors > /dev/null
+ # using --dev since the 'defaulLogger' feature hasn't been released yet
+ luarocks install lualogging > /dev/null
if [ -d /usr/lib/x86_64-linux-gnu ]; then
# debian-based OS
[ -f /etc/lsb-release ] && . /etc/lsb-release
@@ -56,10 +59,10 @@ for ver in -l5.1 -l5.2 -l5.3 -l5.4 -j2.0 -j2.1; do
echo "installing coveralls lib for $ver"
luarocks install luacov-coveralls
echo "running tests and collecting coverage for $ver"
- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path;require("luacov.runner")(".luacov")' $BFLAGS tests/spec/*.lua
+ busted -o utfTerminal --coverage 2> /dev/null
else
echo "running tests for $ver"
- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' $BFLAGS tests/spec/*.lua
+ busted -o utfTerminal 2> /dev/null
fi
done
diff --git a/tests/spec/module-basics.lua b/tests/spec/01-module-basics_spec.lua
similarity index 98%
rename from tests/spec/module-basics.lua
rename to tests/spec/01-module-basics_spec.lua
index c9aa56c..9138eb0 100644
--- a/tests/spec/module-basics.lua
+++ b/tests/spec/01-module-basics_spec.lua
@@ -1,4 +1,3 @@
--- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/module-basics.lua
-- DOC v3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
-- DOC v5.0: http://docs.oasis-open.org/mqtt/mqtt/v5.0/cos01/mqtt-v5.0-cos01.html
@@ -18,8 +17,9 @@ describe("MQTT lua library component test:", function()
require("mqtt.const")
require("mqtt.client")
require("mqtt.ioloop")
- require("mqtt.luasocket")
- require("mqtt.luasocket_ssl")
+ require("mqtt.connector.luasocket")
+ require("mqtt.connector.copas")
+ -- require("mqtt.connector.nginx") -- cannot load this one without nginx
require("mqtt.protocol4")
require("mqtt.protocol5")
end)
@@ -290,7 +290,6 @@ describe("MQTT lua library component test:", function()
assert.are.equal(2097152, protocol.parse_var_length_nonzero(make_read_func("80808001")))
assert.are.equal(268435455, protocol.parse_var_length_nonzero(make_read_func("FFFFFF7F")))
assert.is_false(protocol.parse_var_length_nonzero(make_read_func("FFFFFFFF")))
-
end)
it("protocol.next_packet_id", function()
diff --git a/tests/spec/protocol4-make.lua b/tests/spec/02-protocol4-make_spec.lua
similarity index 99%
rename from tests/spec/protocol4-make.lua
rename to tests/spec/02-protocol4-make_spec.lua
index 73b856c..3de5239 100644
--- a/tests/spec/protocol4-make.lua
+++ b/tests/spec/02-protocol4-make_spec.lua
@@ -1,4 +1,3 @@
--- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/protocol4-make.lua
-- DOC: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
describe("MQTT v3.1.1 protocol: making packets", function()
diff --git a/tests/spec/protocol4-parse.lua b/tests/spec/03-protocol4-parse_spec.lua
similarity index 99%
rename from tests/spec/protocol4-parse.lua
rename to tests/spec/03-protocol4-parse_spec.lua
index d2a417c..46725c5 100644
--- a/tests/spec/protocol4-parse.lua
+++ b/tests/spec/03-protocol4-parse_spec.lua
@@ -1,4 +1,3 @@
--- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/protocol4-parse.lua
-- DOC: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
describe("MQTT v3.1.1 protocol: parsing packets", function()
diff --git a/tests/spec/protocol5-make.lua b/tests/spec/04-protocol5-make_spec.lua
similarity index 99%
rename from tests/spec/protocol5-make.lua
rename to tests/spec/04-protocol5-make_spec.lua
index 40b1581..bc7e937 100644
--- a/tests/spec/protocol5-make.lua
+++ b/tests/spec/04-protocol5-make_spec.lua
@@ -1,4 +1,3 @@
--- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/protocol5-make.lua
-- DOC: https://docs.oasis-open.org/mqtt/mqtt/v5.0/cos02/mqtt-v5.0-cos02.html
describe("MQTT v5.0 protocol: making packets: CONNECT[1]", function()
diff --git a/tests/spec/protocol5-parse.lua b/tests/spec/05-protocol5-parse_spec.lua
similarity index 99%
rename from tests/spec/protocol5-parse.lua
rename to tests/spec/05-protocol5-parse_spec.lua
index 99d3a5e..aa15448 100644
--- a/tests/spec/protocol5-parse.lua
+++ b/tests/spec/05-protocol5-parse_spec.lua
@@ -1,4 +1,3 @@
--- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/protocol5-parse.lua
-- DOC: https://docs.oasis-open.org/mqtt/mqtt/v5.0/cos02/mqtt-v5.0-cos02.html
-- returns read_func-compatible function
diff --git a/tests/spec/mqtt-client.lua b/tests/spec/06-mqtt-client_spec.lua
similarity index 64%
rename from tests/spec/mqtt-client.lua
rename to tests/spec/06-mqtt-client_spec.lua
index 8be205a..a773701 100644
--- a/tests/spec/mqtt-client.lua
+++ b/tests/spec/06-mqtt-client_spec.lua
@@ -1,7 +1,9 @@
--- busted -e 'package.path="./?/init.lua;"..package.path;' spec/*.lua
-- DOC: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
-- DOC v5.0: http://docs.oasis-open.org/mqtt/mqtt/v5.0/cos01/mqtt-v5.0-cos01.html
+local log = require("logging").defaultLogger()
+local socket = require("socket")
+
describe("MQTT lua library", function()
-- load MQTT lua library
local mqtt = require("mqtt")
@@ -11,12 +13,153 @@ describe("MQTT lua library", function()
end)
end)
+
+
+describe("uri parsing:", function()
+
+ -- @param opts uri string, or options table
+ -- @param expected_conn expected connection table after parsing
+ -- @param expected_opts (optional) if given expected options table after parsing
+ local function try(opts, expected_conn, expected_opts)
+ -- reload client in test mode
+ _G._TEST = true
+ package.loaded["mqtt.client"] = nil
+ local client = require("mqtt.client")
+
+ if type(opts) == "string" then
+ opts = { uri = opts }
+ end
+ local conn = {
+ uri = opts.uri
+ }
+
+ client.__parse_connection_opts(opts, conn)
+
+ expected_conn.uri = opts.uri -- must remain the same anyway, so add here
+ conn.secure_params = nil -- not validating those
+ assert.same(expected_conn, conn)
+
+ if expected_opts then
+ expected_opts.uri = opts.uri -- must remain the same anyway, so add here
+ assert.same(expected_opts, opts)
+ end
+ return conn, opts
+ end
+
+
+ describe("valid uri strings", function()
+
+ it("protocol+user+password+host+port", function()
+ try("mqtts://usr:pwd@host.com:123", {
+ -- expected conn
+ host = "host.com",
+ port = 123,
+ protocol = "mqtts",
+ secure = true,
+ ssl_module = "ssl",
+ }, {
+ -- expected opts
+ password = "pwd",
+ secure = true, -- was set because of protocol
+ username = "usr",
+ })
+ end)
+
+ it("user+password+host+port", function()
+ try("usr:pwd@host.com:123", {
+ -- expected conn
+ host = "host.com",
+ port = 123,
+ protocol = "mqtt",
+ secure = false,
+ ssl_module = nil,
+ }, {
+ -- expected opts
+ password = "pwd",
+ secure = nil,
+ username = "usr",
+ })
+ end)
+
+ it("protocol+host+port", function()
+ try("mqtts://host.com:123", {
+ -- expected conn
+ host = "host.com",
+ port = 123,
+ protocol = "mqtts",
+ secure = true,
+ ssl_module = "ssl",
+ }, {
+ -- expected opts
+ secure = true, -- was set because of protocol
+ })
+ end)
+
+ it("host+port", function()
+ try("host.com:123", {
+ -- expected conn
+ host = "host.com",
+ port = 123,
+ protocol = "mqtt",
+ secure = false,
+ ssl_module = nil,
+ }, {
+ -- expected opts
+ })
+ end)
+
+ it("host only", function()
+ try("host.com", {
+ -- expected conn
+ host = "host.com",
+ port = 1883, -- default port
+ protocol = "mqtt",
+ secure = false,
+ ssl_module = nil,
+ }, {
+ -- expected opts
+ })
+ end)
+
+ end)
+
+
+ it("uri properties are overridden by specific properties", function()
+ try({
+ uri = "mqtt://usr:pwd@host.com:123",
+ host = "another.com",
+ port = 456,
+ protocol = "mqtt",
+ password = "king",
+ username = "arthur",
+ }, {
+ -- expected conn
+ host = "another.com",
+ port = 456,
+ protocol = "mqtt",
+ secure = false,
+ ssl_module = nil,
+ }, {
+ -- expected opts
+ host = "another.com",
+ port = 456,
+ protocol = "mqtt",
+ password = "king",
+ secure = false,
+ username = "arthur",
+ })
+ end)
+
+end)
+
+
+
describe("invalid arguments to mqtt.client constructor", function()
-- load MQTT lua library
local mqtt = require("mqtt")
it("argument table key is not a string", function()
- assert.has_error(function() mqtt.client{1} end, "expecting string key in args, got: number")
+ assert.has_error(function() mqtt.client{1} end, "expecting string key in opts, got: number")
end)
it("id is not a string", function()
@@ -48,7 +191,7 @@ describe("invalid arguments to mqtt.client constructor", function()
end)
it("unexpected key", function()
- assert.has_error(function() mqtt.client{unexpected=true} end, "unexpected key in client args: unexpected = true")
+ assert.has_error(function() mqtt.client{unexpected=true} end, "unexpected key in client opts: unexpected = true")
end)
end)
@@ -75,7 +218,7 @@ describe("correct arguments to mqtt.client constructor", function()
},
user_properties = {a="b", c="d"},
reconnect = 5,
- connector = require("mqtt.luasocket"),
+ connector = require("mqtt.connector.luasocket"),
ssl_module = "ssl",
}
end)
@@ -234,9 +377,14 @@ describe("MQTT client", function()
local prefix = "luamqtt/"..tostring(math.floor(math.random()*1e13))
-- set on-connect handler
- client:on("connect", function()
+ client:on("connect", function(packet)
+ log:warn("connect: %d %s", packet.rc, packet:reason_string())
+ assert(packet.rc == 0, packet:reason_string())
assert(client:send_pingreq()) -- NOTE: not required, it's here only to improve code coverage
+ log:warn("subscribing to '.../0/test'")
assert(client:subscribe{topic=prefix.."/0/test", callback=function()
+ log:warn("subscription to '.../0/test' confirmed")
+ log:warn("now publishing 'initial' to '.../0/test'")
assert(client:publish{
topic = prefix.."/0/test",
payload = "initial",
@@ -250,8 +398,14 @@ describe("MQTT client", function()
if msg.topic == prefix.."/0/test" then
-- re-subscribe test
+ log:warn("received message on '.../0/test', payload: %s", msg.payload)
+ log:warn("unsubscribing from '.../0/test'")
assert(client:unsubscribe{topic=prefix.."/0/test", callback=function()
+ log:warn("unsubscribe from '.../0/test' confirmed")
+ log:warn("subscribing to '.../#'")
assert(client:subscribe{topic=prefix.."/#", qos=2, callback=function()
+ log:warn("subscription to '.../#' confirmed")
+ log:warn("now publishing 'testing QoS 1' to '.../1/test'")
assert(client:publish{
topic = prefix.."/1/test",
payload = "testing QoS 1",
@@ -259,32 +413,45 @@ describe("MQTT client", function()
properties = properties,
user_properties = user_properties,
callback = function()
+ log:warn("publishing 'testing QoS 1' to '.../1/test' confirmed")
acknowledge = true
if acknowledge and test_msg_2 then
-- done
+ log:warn("both `acknowledge` (by me) and `test_msg_2` are set, disconnecting now")
assert(client:disconnect())
+ else
+ log:warn("only `acknowledge` (by me) is set, not `test_msg_2`. So not disconnecting yet")
end
end,
})
end})
end})
elseif msg.topic == prefix.."/1/test" then
+ log:warn("received message on '.../1/test', payload: %s", msg.payload)
if case.args.version == mqtt.v50 then
assert(type(msg.properties) == "table")
assert.are.same(properties.message_expiry_interval, msg.properties.message_expiry_interval)
assert(type(msg.user_properties) == "table")
assert.are.same(user_properties.hello, msg.user_properties.hello)
end
+ log:warn("now publishing 'testing QoS 2' to '.../2/test'")
assert(client:publish{
topic = prefix.."/2/test",
payload = "testing QoS 2",
qos = 2,
+ callback = function()
+ log:warn("publishing 'testing QoS 2' to '.../2/test' confirmed")
+ end,
})
elseif msg.topic == prefix.."/2/test" then
+ log:warn("received message on '.../2/test', payload: %s", msg.payload)
test_msg_2 = true
if acknowledge and test_msg_2 then
-- done
+ log:warn("both `test_msg_2` (by me) and `acknowledge` (by me) are set, disconnecting now")
assert(client:disconnect())
+ else
+ log:warn("only `test_msg_2` (by me) is set, not `acknowledge`. So not disconnecting yet")
end
end
end,
@@ -295,15 +462,13 @@ describe("MQTT client", function()
close = function(conn)
close_reason = conn.close_reason
+ -- remove our client from the loop to make it exit.
+ require("mqtt.ioloop").get():remove(client)
end,
}
- -- and wait for connection to broker is closed
- if case.sync then
- mqtt.run_sync(client)
- else
- mqtt.run_ioloop(client)
- end
+ -- and wait for connection to broker to be closed
+ mqtt.run_ioloop(client)
assert.are.same({}, errors)
assert.is_true(acknowledge)
@@ -336,12 +501,14 @@ describe("last will message", function()
username = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9",
}
- local client1_ready, client2_ready
+ local client1_ready, client2_ready, clients_done
local function send_self_destroy()
if not client1_ready or not client2_ready then
+ log:warn("not self destroying, clients not ready")
return
end
+ log:warn("client1 publishing 'self-destructing-message' to '.../stop' topic")
assert(client1:publish{
topic = prefix.."/stop",
payload = "self-destructing-message",
@@ -351,14 +518,19 @@ describe("last will message", function()
client1:on{
connect = function()
-- subscribe, then send self-destructing message
+ log:warn("client1 is now connected")
+ log:warn("client1 subscribing to '.../stop' topic")
assert(client1:subscribe{topic=prefix.."/stop", callback=function()
client1_ready = true
+ log:warn("client1 subscription to '.../stop' topic confirmed, client 1 is ready for self destruction")
send_self_destroy()
end})
end,
message = function()
-- break connection with broker on any message
+ log:warn("client1 received a message and is now closing its connection")
client1:close_connection("self-destructed")
+ clients_done = (clients_done or 0)+1
end,
}
@@ -367,25 +539,40 @@ describe("last will message", function()
client2:on{
connect = function()
-- subscribe to will-message topic
+ log:warn("client2 is now connected")
+ log:warn("client2 subscribing to will-topic: '.../willtest' topic")
assert(client2:subscribe{topic=will_topic, callback=function()
client2_ready = true
+ log:warn("client2 subscription to will-topic '.../willtest' confirmed, client 2 is ready for self destruction")
send_self_destroy()
end})
end,
message = function(msg)
will_received = msg.topic == will_topic
+ log:warn("client2 received a message, topic is: '%s', client 2 is now closing its connection",tostring(msg.topic))
client2:disconnect()
+ clients_done = (clients_done or 0)+1
end,
}
- mqtt.run_ioloop(client1, client2)
+ local timer do
+ local timeout = socket.gettime() + 30
+ function timer()
+ if clients_done == 2 then
+ require("mqtt.ioloop").get():remove(timer)
+ end
+ assert(socket.gettime() < timeout, "test failed due to timeout")
+ end
+ end
+
+ mqtt.run_ioloop(client1, client2, timer)
assert.is_true(will_received)
end)
end)
-describe("no_local flag for subscription: ", function()
+describe("no_local flag for subscription:", function()
local mqtt = require("mqtt")
local prefix = "luamqtt/" .. tostring(math.floor(math.random()*1e13))
local no_local_topic = prefix .. "/no_local_test"
@@ -423,26 +610,34 @@ describe("no_local flag for subscription: ", function()
local function send()
if not s1.subscribed or not s2.subscribed then
+ log:warn("not sending because clients are not both subscribed")
return
end
+ log:warn("both clients are subscribed, now sending...")
+ socket.sleep(0.2) -- shouldn't be necessary, but test is flaky otherwise
+ log:warn("client1: publishing 'message' to topic '.../no_local_test'")
assert(c1:publish{
topic = no_local_topic,
payload = "message",
callback = function()
s1.published = s1.published + 1
+ log:warn("client1: publishing to topic '.../no_local_test' confirmed, count: %d", s1.published)
end
})
end
c1:on{
connect = function()
+ log:warn("client1: is now connected")
s1.connected = true
send()
+ log:warn("client1: subscribing to topic '.../#', with 'no_local'")
assert(c1:subscribe{
topic = prefix .. "/#",
no_local = true,
callback = function()
s1.subscribed = true
+ log:warn("client1: subscription to topic '.../#' with 'no_local' confirmed")
send()
end
})
@@ -450,13 +645,18 @@ describe("no_local flag for subscription: ", function()
message = function(msg)
s1.messages[#s1.messages + 1] = msg.payload
if msg.payload == "stop" then
+ log:warn("client1: received message, with payload 'stop'. Will now disconnect.")
assert(c1:disconnect())
+ else
+ log:warn("client1: received message, with payload '%s' (but waiting for 'stop')", msg.payload)
end
end,
error = function(err)
s1.errors[#s1.errors + 1] = err
+ log:warn("client1: received error: '%s'", tostring(err))
end,
close = function(conn)
+ log:warn("client1: closed, reason: %s", conn.close_reason)
s1.close_reason = conn.close_reason
end
}
@@ -464,11 +664,14 @@ describe("no_local flag for subscription: ", function()
c2:on{
connect = function()
s2.connected = true
+ log:warn("client2: is now connected")
+ log:warn("client2: subscribing to topic '.../#', without 'no_local'")
assert(c2:subscribe{
topic = no_local_topic,
no_local = false,
callback = function()
s2.subscribed = true
+ log:warn("client2: subscription to topic '.../#' without 'no_local' confirmed")
send()
end
})
@@ -476,15 +679,21 @@ describe("no_local flag for subscription: ", function()
message = function(msg)
s2.messages[#s2.messages + 1] = msg.payload
if msg.payload == "message" then
+ log:warn("client2: received message, with payload 'message'")
+ log:warn("client2: publishing to topic '.../no_local_test'', with payload 'stop'")
assert(c2:publish{
topic = no_local_topic,
payload = "stop",
callback = function()
s2.published = s2.published + 1
+ log:warn("client2: publishing to topic '.../no_local_test' confirmed, count: %d", s2.published)
end
})
elseif msg.payload == "stop" then
+ log:warn("client2: received message, with payload 'stop'. Will now disconnect.")
assert(c2:disconnect())
+ else
+ log:warn("client2: received message, with payload '%s' (but waiting for 'stop' or 'message')", msg.payload)
end
end,
error = function(err)
@@ -495,7 +704,17 @@ describe("no_local flag for subscription: ", function()
end
}
- mqtt.run_ioloop(c1, c2)
+ local timer do
+ local timeout = socket.gettime() + 30
+ function timer()
+ if s1.close_reason and s2.close_reason then
+ require("mqtt.ioloop").get():remove(timer)
+ end
+ assert(socket.gettime() < timeout, "test failed due to timeout")
+ end
+ end
+
+ mqtt.run_ioloop(c1, c2, timer)
assert.is_true(s1.connected, "client 1 is not connected")
assert.is_true(s2.connected, "client 2 is not connected")
@@ -510,70 +729,4 @@ describe("no_local flag for subscription: ", function()
end)
end)
-describe("#copas connector", function()
- local mqtt = require("mqtt")
- local copas = require("copas")
- local prefix = "luamqtt/" .. tostring(math.floor(math.random()*1e13))
-
- it("test", function()
- -- NOTE: more about flespi tokens:
- -- https://flespi.com/kb/tokens-access-keys-to-flespi-platform
- local flespi_token = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9"
-
- local client = mqtt.client{
- uri = "mqtt.flespi.io",
- clean = true,
- username = flespi_token,
- version = mqtt.v50,
-
- connector = require("mqtt.luasocket-copas"),
- }
-
- client:on{
- connect = function()
- -- print("client connected")
- assert(client:subscribe{topic=prefix.."/copas", qos=1, callback=function()
- -- print("subscribed")
- copas.sleep(2)
- assert(client:publish{
- topic = prefix.."/copas",
- payload = "copas test",
- qos = 1,
- })
- end})
- end,
-
- message = function(msg)
- assert(client:acknowledge(msg))
- -- print("received", msg)
- if msg.topic == prefix.."/copas" and msg.payload == "copas test" then
- -- print("disconnect")
- assert(client:disconnect())
- end
- end
- }
-
- local ticks = 0
- local mqtt_finished = false
-
- copas.addthread(function()
- mqtt.run_sync(client)
- mqtt_finished = true
- assert.is_true(ticks > 1, "expecting mqtt client run takes at least one tick")
- end)
-
- copas.addthread(function()
- for _ = 1, 3 do
- copas.sleep(1)
- ticks = ticks + 1
- end
- end)
-
- copas.loop()
-
- assert.is_true(mqtt_finished, "expecting mqtt client to finish its work")
- assert.is_true(ticks == 3, "expecting 3 ticks")
- end)
-end)
-
-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/tests/spec/ioloop.lua b/tests/spec/07-ioloop_spec.lua
similarity index 92%
rename from tests/spec/ioloop.lua
rename to tests/spec/07-ioloop_spec.lua
index dbba470..a2ae799 100644
--- a/tests/spec/ioloop.lua
+++ b/tests/spec/07-ioloop_spec.lua
@@ -1,8 +1,8 @@
--- busted -e 'package.path="./?/init.lua;./?.lua;"..package.path' tests/spec/module-basics.lua
-- DOC v3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
-- DOC v5.0: http://docs.oasis-open.org/mqtt/mqtt/v5.0/cos01/mqtt-v5.0-cos01.html
describe("ioloop", function()
+
-- load MQTT lua library
local mqtt = require("mqtt")
@@ -22,9 +22,14 @@ describe("ioloop", function()
-- configure MQTT client handlers
client:on{
+ error = function(err)
+ print("[error] ", err)
+ end,
connect = function()
+ --print "connected"
-- subscribe, then send signal message
assert(client:subscribe{topic=prefix.."/ioloop/signal", callback=function()
+ --print "subscribed"
assert(client:publish{
topic = prefix.."/ioloop/signal",
payload = "signal",
@@ -44,6 +49,7 @@ describe("ioloop", function()
if signal then
-- disconnect MQTT client, thus it will be removed from ioloop
client:disconnect()
+ mqtt.get_ioloop():remove(client)
-- and remove this function from ioloop to stop it (no more clients left)
mqtt.get_ioloop():remove(loop_func)
diff --git a/tests/spec/08-copas_spec.lua b/tests/spec/08-copas_spec.lua
new file mode 100644
index 0000000..14d2324
--- /dev/null
+++ b/tests/spec/08-copas_spec.lua
@@ -0,0 +1,73 @@
+local log = require("logging").defaultLogger()
+
+describe("copas connector", function()
+ local mqtt = require("mqtt")
+ local copas = require("copas")
+ local prefix = "luamqtt/" .. tostring(math.floor(math.random()*1e13))
+
+ for _, secure in ipairs { false, true } do
+
+ it("test 'secure = "..tostring(secure).."'", function()
+ -- NOTE: more about flespi tokens:
+ -- https://flespi.com/kb/tokens-access-keys-to-flespi-platform
+ local flespi_token = "stPwSVV73Eqw5LSv0iMXbc4EguS7JyuZR9lxU5uLxI5tiNM8ToTVqNpu85pFtJv9"
+
+ local client = mqtt.client{
+ uri = "mqtt.flespi.io",
+ clean = true,
+ secure = secure,
+ username = flespi_token,
+ version = mqtt.v50,
+
+ -- connector = require("mqtt.connector.copas"), -- will be auto-detected
+ }
+
+ local test_finished = false
+
+ client:on{
+ connect = function()
+ log:warn("client is now connected")
+ log:warn("client subscribing to topic '.../copas'")
+ assert(client:subscribe{topic=prefix.."/copas", qos=1, callback=function()
+ log:warn("client subscription to topic '.../copas' confirmed")
+ log:warn("client publishing 'copas test' to topic '.../copas' confirmed")
+ assert(client:publish{
+ topic = prefix.."/copas",
+ payload = "copas test",
+ qos = 1,
+ })
+ end})
+ end,
+
+ message = function(msg)
+ assert(client:acknowledge(msg))
+ if msg.topic == prefix.."/copas" and msg.payload == "copas test" then
+ log:warn("client received '%s' to topic '.../copas' confirmed", msg.payload)
+ assert(client:disconnect())
+ log:warn("disconnected now")
+ test_finished = true
+ end
+ end
+ }
+
+ copas.addthread(function()
+ while true do
+ local timeout = client:step()
+ if not timeout then
+ -- exited
+ return
+ end
+ if timeout > 0 then
+ copas.pause(timeout)
+ end
+ end
+ end)
+
+ copas.loop()
+
+ assert.is_true(test_finished, "expecting mqtt client to finish its work")
+ end)
+ end
+end)
+
+-- vim: ts=4 sts=4 sw=4 noet ft=lua
diff --git a/tests/spec/09-topics.lua b/tests/spec/09-topics.lua
new file mode 100644
index 0000000..c8bca35
--- /dev/null
+++ b/tests/spec/09-topics.lua
@@ -0,0 +1,310 @@
+local mqtt = require "mqtt"
+
+describe("topics", function()
+
+ describe("publish (plain)", function()
+ it("allows proper topics", function()
+ local ok, err
+ ok, err = mqtt.validate_publish_topic("hello/world")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_publish_topic("hello/world/")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_publish_topic("/hello/world")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_publish_topic("/")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_publish_topic("//////")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_publish_topic("/")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ end)
+
+ it("returns the topic passed in on success", function()
+ local ok = mqtt.validate_publish_topic("hello/world")
+ assert.are.equal("hello/world", ok)
+ end)
+
+ it("must be a string", function()
+ local ok, err = mqtt.validate_publish_topic(true)
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("minimum length 1", function()
+ local ok, err = mqtt.validate_publish_topic("")
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("wildcard '#' is not allowed", function()
+ local ok, err = mqtt.validate_publish_topic("hello/world/#")
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("wildcard '+' is not allowed", function()
+ local ok, err = mqtt.validate_publish_topic("hello/+/world")
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ end)
+
+
+
+ describe("subscribe (wildcarded)", function()
+
+ it("allows proper topics", function()
+ local ok, err
+ ok, err = mqtt.validate_subscribe_topic("hello/world")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("hello/world/")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("/hello/world")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("/")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("//////")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("#")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("/#")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("+")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("+/hello/#")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+
+ ok, err = mqtt.validate_subscribe_topic("+/+/+/+/+")
+ assert.is_nil(err)
+ assert.is.truthy(ok)
+ end)
+
+ it("returns the topic passed in on success", function()
+ local ok = mqtt.validate_subscribe_topic("hello/world")
+ assert.are.equal("hello/world", ok)
+ end)
+
+ it("must be a string", function()
+ local ok, err = mqtt.validate_subscribe_topic(true)
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("minimum length 1", function()
+ local ok, err = mqtt.validate_subscribe_topic("")
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("wildcard '#' is only allowed as last segment", function()
+ local ok, err = mqtt.validate_subscribe_topic("hello/#/world")
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("wildcard '+' is only allowed as full segment", function()
+ local ok, err = mqtt.validate_subscribe_topic("hello/+there/world")
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ end)
+
+
+
+ describe("pattern compiler & matcher", function()
+
+ it("escapes Lua pattern magic characters", function()
+ local t = mqtt.compile_topic_pattern("+/dash-dash/+/+/back\\slash/+")
+ assert.equal("^([^/]-)/dash%-dash/([^/]-)/([^/]-)/back%\\slash/([^/]-)$", t)
+ local h, m, d, w = ("hello/dash-dash/my/dear/back\\slash/world"):match(t)
+ assert.equal("hello", h)
+ assert.equal("my", m)
+ assert.equal("dear", d)
+ assert.equal("world", w)
+ end)
+
+ it("basic parsing works", function()
+ local opts = {
+ topic = "+/+",
+ pattern = nil,
+ keys = { "hello", "world"}
+ }
+ local res, varargs = mqtt.topic_match("hello/world", opts)
+ assert.is_same({}, varargs)
+ assert.same(res, {
+ "hello", "world",
+ hello = "hello",
+ world = "world",
+ })
+ -- compiled pattern is now added
+ assert.not_nil(opts.pattern)
+ end)
+
+ it("incoming topic is required", function()
+ local opts = {
+ topic = "+/+",
+ pattern = nil,
+ keys = { "hello", "world"}
+ }
+ local ok, err = mqtt.topic_match(nil, opts)
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("wildcard topic or pattern is required", function()
+ local opts = {
+ topic = nil,
+ pattern = nil,
+ keys = { "hello", "world"}
+ }
+ local ok, err = mqtt.topic_match("hello/world", opts)
+ assert.is_false(ok)
+ assert.is_string(err)
+ end)
+
+ it("pattern must match", function()
+ local opts = {
+ topic = "+/+/+", -- one too many
+ pattern = nil,
+ keys = { "hello", "world"}
+ }
+ local ok, err = mqtt.topic_match("hello/world", opts)
+ assert.is_false(ok)
+ assert.is_nil(err)
+ end)
+
+ it("pattern '+' works", function()
+ local opts = {
+ topic = "+",
+ pattern = nil,
+ keys = { "hello" }
+ }
+ -- matches topic
+ local res, varargs = mqtt.topic_match("hello", opts)
+ assert.are.same({}, varargs)
+ assert.same(res, {
+ "hello",
+ hello = "hello",
+ })
+ end)
+
+ it("wildcard '+' matches empty segments", function()
+ local opts = {
+ topic = "+/+/+",
+ pattern = nil,
+ keys = { "hello", "there", "world"}
+ }
+ local res, varargs = mqtt.topic_match("//", opts)
+ assert.are.same({}, varargs)
+ assert.same(res, {
+ "", "", "",
+ hello = "",
+ there = "",
+ world = "",
+ })
+ end)
+
+ it("pattern '#' matches all segments", function()
+ local opts = {
+ topic = "#",
+ pattern = nil,
+ keys = nil,
+ }
+ local res, var = mqtt.topic_match("hello/there/world", opts)
+ assert.same(res, {
+ "hello/there/world"
+ })
+ assert.same(var, {
+ "hello",
+ "there",
+ "world",
+ })
+ end)
+
+ it("pattern '/#' skips first segment", function()
+ local opts = {
+ topic = "/#",
+ pattern = nil,
+ keys = nil,
+ }
+ local res, var = mqtt.topic_match("/hello/world", opts)
+ assert.same(res, {
+ "hello/world"
+ })
+ assert.same(var, {
+ "hello",
+ "world",
+ })
+ end)
+
+ it("combined wildcards '+/+/#'", function()
+ local opts = {
+ topic = "+/+/#",
+ pattern = nil,
+ keys = nil,
+ }
+ local res, var = mqtt.topic_match("hello/there/my/world", opts)
+ assert.same(res, {
+ "hello",
+ "there",
+ "my/world"
+ })
+ assert.same(var, {
+ "my",
+ "world",
+ })
+ end)
+
+ it("trailing '/' in topic with '#'", function()
+ local opts = {
+ topic = "+/+/#",
+ pattern = nil,
+ keys = nil,
+ }
+ local res, var = mqtt.topic_match("hello/there/world/", opts)
+ assert.same(res, {
+ "hello",
+ "there",
+ "world/"
+ })
+ assert.same(var, {
+ "world",
+ "",
+ })
+ end)
+
+
+ end)
+
+end)