├── .gitignore ├── images ├── pebble.jpg ├── aiko_gateway.jpg ├── lua_mqtt_overview.dia ├── lua_mqtt_overview.jpg └── playstation_portable.jpg ├── mqtt-0.0-0.rockspec ├── lua ├── example │ ├── example_02.lua │ ├── mqtt_publish.lua │ ├── example_00.lua │ ├── example_01.lua │ ├── mqtt_test.lua │ └── mqtt_subscribe.lua ├── utility.lua └── mqtt_library.lua └── readme.markdown /.gitignore: -------------------------------------------------------------------------------- 1 | readme.html 2 | -------------------------------------------------------------------------------- /images/pebble.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekscape/mqtt_lua/HEAD/images/pebble.jpg -------------------------------------------------------------------------------- /images/aiko_gateway.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekscape/mqtt_lua/HEAD/images/aiko_gateway.jpg -------------------------------------------------------------------------------- /images/lua_mqtt_overview.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekscape/mqtt_lua/HEAD/images/lua_mqtt_overview.dia -------------------------------------------------------------------------------- /images/lua_mqtt_overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekscape/mqtt_lua/HEAD/images/lua_mqtt_overview.jpg -------------------------------------------------------------------------------- /images/playstation_portable.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geekscape/mqtt_lua/HEAD/images/playstation_portable.jpg -------------------------------------------------------------------------------- /mqtt-0.0-0.rockspec: -------------------------------------------------------------------------------- 1 | package = "mqtt" 2 | version = "0.0-0" 3 | 4 | source = { 5 | url = "http://github.com/downloads/geekscape/mqtt_lua/mqtt_lua-0.0.tar.gz" 6 | } 7 | 8 | description = { 9 | summary = "Lua MQTT client", 10 | detailed = [[ 11 | MQTT (Message Queue Telemetry Transport) client-side implementation ... 12 | http://mqtt.org 13 | Based on the "MQTT protocol specification 3.1" ... 14 | https://www.ibm.com/developerworks/webservices/library/ws-mqtt 15 | ]], 16 | 17 | homepage = "https://geekscape.github.com/mqtt_lua", 18 | license = "AGPLv3 or commercial", 19 | maintainer = "Andy Gelme (@geekscape)" 20 | } 21 | 22 | dependencies = { 23 | "lua >= 5.1", 24 | "luasocket >= 2.0.2" 25 | } 26 | 27 | build = { 28 | type = "builtin", 29 | 30 | modules = { 31 | lapp = "lua/lapp.lua", 32 | mqtt_library = "lua/mqtt_library.lua", 33 | mqtt_publish = "lua/example/mqtt_publish.lua", 34 | mqtt_subscribe = "lua/example/mqtt_subscribe.lua", 35 | mqtt_test = "lua/example/mqtt_test.lua", 36 | utility = "lua/utility.lua" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lua/example/example_02.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- ------------------------------------------------------------------------- -- 3 | -- example_02.lua 4 | -- ~~~~~~~~~~~~~~ 5 | -- Please do not remove the following notices. 6 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 7 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 8 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 9 | -- Version: 0.2 2012-06-01 10 | -- 11 | -- Description 12 | -- ~~~~~~~~~~~ 13 | -- Publish a sequence of messages to a specified topic. 14 | -- Used to control some coloured RGB LEDs. 15 | -- 16 | -- ToDo 17 | -- ~~~~ 18 | -- - On failure, automatically reconnect to MQTT server. 19 | -- ------------------------------------------------------------------------- -- 20 | 21 | function is_openwrt() 22 | return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT 23 | end 24 | 25 | -- ------------------------------------------------------------------------- -- 26 | 27 | if (not is_openwrt()) then require("luarocks.require") end 28 | local lapp = require("pl.lapp") 29 | 30 | local args = lapp [[ 31 | Subscribe to topic1 and publish all messages on topic2 32 | -H,--host (default localhost) MQTT server hostname 33 | -i,--id (default example_02) MQTT client identifier 34 | -p,--port (default 1883) MQTT server port number 35 | -s,--sleep (default 5.0) Sleep time between commands 36 | -t,--topic (default test/2) Topic on which to publish 37 | ]] 38 | 39 | local MQTT = require("mqtt_library") 40 | 41 | local mqtt_client = MQTT.client.create(args.host, args.port) 42 | 43 | mqtt_client:connect(args.id) 44 | 45 | local error_message = nil 46 | local index = 1 47 | local messages = { "c010000", "c000100", "c000001" } 48 | 49 | while (error_message == nil) do 50 | mqtt_client:publish(args.topic, messages[index]); 51 | 52 | index = index + 1 53 | if (index > #messages) then index = 1 end 54 | 55 | socket.sleep(args.sleep) -- seconds 56 | error_message = mqtt_client:handler() 57 | end 58 | 59 | if (error_message == nil) then 60 | mqtt_client:destroy() 61 | else 62 | print(error_message) 63 | end 64 | 65 | -- ------------------------------------------------------------------------- -- 66 | -------------------------------------------------------------------------------- /lua/example/mqtt_publish.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- ------------------------------------------------------------------------- -- 3 | -- mqtt_publish.lua 4 | -- ~~~~~~~~~~~~~~~~ 5 | -- Please do not remove the following notices. 6 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 7 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 8 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 9 | -- Version: 0.2 2012-06-01 10 | -- 11 | -- Description 12 | -- ~~~~~~~~~~~ 13 | -- Publish an MQTT message on the specified topic with an optional last will. 14 | -- 15 | -- References 16 | -- ~~~~~~~~~~ 17 | -- Lapp Framework: Lua command line parsing 18 | -- http://lua-users.org/wiki/LappFramework 19 | -- 20 | -- ToDo 21 | -- ~~~~ 22 | -- None, yet. 23 | -- ------------------------------------------------------------------------- -- 24 | 25 | function is_openwrt() 26 | return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT 27 | end 28 | 29 | -- ------------------------------------------------------------------------- -- 30 | 31 | print("[mqtt_publish v0.2 2012-06-01]") 32 | 33 | if (not is_openwrt()) then require("luarocks.require") end 34 | local lapp = require("pl.lapp") 35 | 36 | local args = lapp [[ 37 | Publish a message to a specified MQTT topic 38 | -d,--debug Verbose console logging 39 | -H,--host (default localhost) MQTT server hostname 40 | -i,--id (default mqtt_pub) MQTT client identifier 41 | -m,--message (string) Message to be published 42 | -p,--port (default 1883) MQTT server port number 43 | -t,--topic (string) Topic on which to publish 44 | -w,--will_message (default .) Last will and testament message 45 | -w,--will_qos (default 0) Last will and testament QOS 46 | -w,--will_retain (default 0) Last will and testament retention 47 | -w,--will_topic (default .) Last will and testament topic 48 | ]] 49 | 50 | local MQTT = require("mqtt_library") 51 | 52 | if (args.debug) then MQTT.Utility.set_debug(true) end 53 | 54 | local mqtt_client = MQTT.client.create(args.host, args.port) 55 | 56 | if (args.will_message == "." or args.will_topic == ".") then 57 | mqtt_client:connect(args.id) 58 | else 59 | mqtt_client:connect( 60 | args.id, args.will_topic, args.will_qos, args.will_retain, args.will_message 61 | ) 62 | end 63 | 64 | mqtt_client:publish(args.topic, args.message) 65 | 66 | mqtt_client:destroy() 67 | 68 | -- ------------------------------------------------------------------------- -- 69 | -------------------------------------------------------------------------------- /lua/example/example_00.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- ------------------------------------------------------------------------- -- 3 | -- example_00.lua 4 | -- ~~~~~~~~~~~~~~ 5 | -- Please do not remove the following notices. 6 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 7 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 8 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 9 | -- Version: 0.2 2012-06-01 10 | -- 11 | -- Description 12 | -- ~~~~~~~~~~~ 13 | -- Subscribe to a topic and publish all received messages on another topic. 14 | -- 15 | -- ToDo 16 | -- ~~~~ 17 | -- - On failure, automatically reconnect to MQTT server. 18 | -- - Error handling: MQTT.client.connect() 19 | -- - Error handling: MQTT.client.destroy() 20 | -- - Error handling: MQTT.client.disconnect() 21 | -- - Error handling: MQTT.client.handler() 22 | -- - Error handling: MQTT.client.publish() 23 | -- - Error handling: MQTT.client.subscribe() 24 | -- - Error handling: MQTT.client.unsubscribe() 25 | -- ------------------------------------------------------------------------- -- 26 | 27 | function callback( 28 | topic, -- string 29 | message) -- string 30 | 31 | print("Topic: " .. topic .. ", message: '" .. message .. "'") 32 | 33 | mqtt_client:publish(args.topic_p, message) 34 | end 35 | 36 | -- ------------------------------------------------------------------------- -- 37 | 38 | function is_openwrt() 39 | return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT 40 | end 41 | 42 | -- ------------------------------------------------------------------------- -- 43 | 44 | if (not is_openwrt()) then require("luarocks.require") end 45 | local lapp = require("pl.lapp") 46 | 47 | args = lapp [[ 48 | Subscribe to topic_s and publish all messages on topic_p 49 | -H,--host (default localhost) MQTT server hostname 50 | -i,--id (default example_00) MQTT client identifier 51 | -p,--port (default 1883) MQTT server port number 52 | -s,--topic_s (default test/1) Subscribe topic 53 | -t,--topic_p (default test/2) Publish topic 54 | ]] 55 | 56 | local MQTT = require("mqtt_library") 57 | 58 | mqtt_client = MQTT.client.create(args.host, args.port, callback) 59 | 60 | mqtt_client:connect(args.id) 61 | 62 | mqtt_client:subscribe({ args.topic_s }) 63 | 64 | local error_message = nil 65 | 66 | while (error_message == nil) do 67 | error_message = mqtt_client:handler() 68 | socket.sleep(1.0) -- seconds 69 | end 70 | 71 | if (error_message == nil) then 72 | mqtt_client:unsubscribe({ args.topic_s }) 73 | mqtt_client:destroy() 74 | else 75 | print(error_message) 76 | end 77 | 78 | -- ------------------------------------------------------------------------- -- 79 | -------------------------------------------------------------------------------- /lua/example/example_01.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- ------------------------------------------------------------------------- -- 3 | -- example_01.lua 4 | -- ~~~~~~~~~~~~~~ 5 | -- Please do not remove the following notices. 6 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 7 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 8 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 9 | -- Version: 0.2 2012-06-01 10 | -- 11 | -- Description 12 | -- ~~~~~~~~~~~ 13 | -- Subscribe to a topic on one MQTT server and publish all received messages 14 | -- to a topic on another MQTT server. 15 | -- 16 | -- ToDo 17 | -- ~~~~ 18 | -- - On failure, automatically reconnect to MQTT server(s). 19 | -- ------------------------------------------------------------------------- -- 20 | 21 | function callback( 22 | topic, -- string 23 | message) -- string 24 | 25 | print("Topic: " .. topic .. ", message: '" .. message .. "'") 26 | 27 | mqtt_client2:publish(args.topic_p, message) 28 | end 29 | 30 | -- ------------------------------------------------------------------------- -- 31 | 32 | function is_openwrt() 33 | return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT 34 | end 35 | 36 | -- ------------------------------------------------------------------------- -- 37 | 38 | if (not is_openwrt()) then require("luarocks.require") end 39 | local lapp = require("pl.lapp") 40 | 41 | args = lapp [[ 42 | Subscribe to topic_s and publish all messages on topic_p 43 | -g,--host_s (default localhost) Subscribe MQTT server hostname 44 | -H,--host_p (default localhost) Publish MQTT server hostname 45 | -i,--id (default example_01) MQTT client identifier 46 | -p,--port_s (default 1883) Subscribe MQTT server port number 47 | -q,--port_p (default 1883) Publish MQTT server port number 48 | -s,--topic_s (default test/1) Subscribe topic 49 | -t,--topic_p (default test/2) Publish topic 50 | ]] 51 | 52 | local MQTT = require("mqtt_library") 53 | 54 | mqtt_client1 = MQTT.client.create(args.host_s, args.port_s, callback) 55 | mqtt_client2 = MQTT.client.create(args.host_p, args.port_p) 56 | 57 | mqtt_client1:connect(args.id .. "a") 58 | mqtt_client2:connect(args.id .. "b") 59 | 60 | mqtt_client1:subscribe({ args.topic_s }) 61 | 62 | local error_message1 = nil 63 | local error_message2 = nil 64 | 65 | while (error_message1 == nil and error_message2 == nil) do 66 | error_message1 = mqtt_client1:handler() 67 | error_message2 = mqtt_client2:handler() 68 | socket.sleep(1.0) -- seconds 69 | end 70 | 71 | if (error_message1 == nil) then 72 | mqtt_client1:unsubscribe({ args.topic_s }) 73 | mqtt_client1:destroy() 74 | else 75 | print(error_message1) 76 | end 77 | 78 | if (error_message2 == nil) then 79 | mqtt_client2:destroy() 80 | else 81 | print(error_message2) 82 | end 83 | 84 | -- ------------------------------------------------------------------------- -- 85 | -------------------------------------------------------------------------------- /lua/example/mqtt_test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- ------------------------------------------------------------------------- -- 3 | -- mqtt_test.lua 4 | -- ~~~~~~~~~~~~~ 5 | -- Please do not remove the following notices. 6 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 7 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 8 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 9 | -- Version: 0.2 2012-06-01 10 | -- 11 | -- Description 12 | -- ~~~~~~~~~~~ 13 | -- Repetitively publishes MQTT messages on the topic_p, 14 | -- until the "quit" message is received on the topic_s. 15 | -- 16 | -- References 17 | -- ~~~~~~~~~~ 18 | -- Lapp Framework: Lua command line parsing 19 | -- http://lua-users.org/wiki/LappFramework 20 | -- 21 | -- ToDo 22 | -- ~~~~ 23 | -- - On failure, automatically reconnect to MQTT server. 24 | -- ------------------------------------------------------------------------- -- 25 | 26 | function callback( 27 | topic, -- string 28 | payload) -- string 29 | 30 | print("mqtt_test:callback(): " .. topic .. ": " .. payload) 31 | 32 | if (payload == "quit") then running = false end 33 | end 34 | 35 | -- ------------------------------------------------------------------------- -- 36 | 37 | function is_openwrt() 38 | return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT 39 | end 40 | 41 | -- ------------------------------------------------------------------------- -- 42 | 43 | print("[mqtt_test v0.2 2012-06-01]") 44 | 45 | if (not is_openwrt()) then require("luarocks.require") end 46 | local lapp = require("pl.lapp") 47 | 48 | local args = lapp [[ 49 | Test Lua MQTT client library 50 | -d,--debug Verbose console logging 51 | -i,--id (default mqtt_test) MQTT client identifier 52 | -p,--port (default 1883) MQTT server port number 53 | -s,--topic_s (default test/2) Subscribe topic 54 | -t,--topic_p (default test/1) Publish topic 55 | (default localhost) MQTT server hostname 56 | ]] 57 | 58 | local MQTT = require("mqtt_library") 59 | 60 | if (args.debug) then MQTT.Utility.set_debug(true) end 61 | 62 | local mqtt_client = MQTT.client.create(args.host, args.port, callback) 63 | 64 | mqtt_client:connect(args.id) 65 | 66 | mqtt_client:publish(args.topic_p, "*** Lua test start ***") 67 | mqtt_client:subscribe({ args.topic_s }) 68 | 69 | local error_message = nil 70 | local running = true 71 | 72 | while (error_message == nil and running) do 73 | error_message = mqtt_client:handler() 74 | 75 | if (error_message == nil) then 76 | mqtt_client:publish(args.topic_p, "*** Lua test message ***") 77 | socket.sleep(1.0) -- seconds 78 | end 79 | end 80 | 81 | if (error_message == nil) then 82 | mqtt_client:unsubscribe({ args.topic_s }) 83 | mqtt_client:destroy() 84 | else 85 | print(error_message) 86 | end 87 | 88 | -- ------------------------------------------------------------------------- -- 89 | -------------------------------------------------------------------------------- /lua/example/mqtt_subscribe.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- ------------------------------------------------------------------------- -- 3 | -- mqtt_subscribe.lua 4 | -- ~~~~~~~~~~~~~~~~~~ 5 | -- Please do not remove the following notices. 6 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 7 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 8 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 9 | -- Version: 0.2 2012-06-01 10 | -- 11 | -- Description 12 | -- ~~~~~~~~~~~ 13 | -- Subscribe to an MQTT topic and display any received messages. 14 | -- 15 | -- References 16 | -- ~~~~~~~~~~ 17 | -- Lapp Framework: Lua command line parsing 18 | -- http://lua-users.org/wiki/LappFramework 19 | -- 20 | -- ToDo 21 | -- ~~~~ 22 | -- None, yet. 23 | -- ------------------------------------------------------------------------- -- 24 | 25 | function callback( 26 | topic, -- string 27 | message) -- string 28 | 29 | print("Topic: " .. topic .. ", message: '" .. message .. "'") 30 | end 31 | 32 | -- ------------------------------------------------------------------------- -- 33 | 34 | function is_openwrt() 35 | return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT 36 | end 37 | 38 | -- ------------------------------------------------------------------------- -- 39 | 40 | print("[mqtt_subscribe v0.2 2012-06-01]") 41 | 42 | if (not is_openwrt()) then require("luarocks.require") end 43 | local lapp = require("pl.lapp") 44 | 45 | local args = lapp [[ 46 | Subscribe to a specified MQTT topic 47 | -d,--debug Verbose console logging 48 | -H,--host (default localhost) MQTT server hostname 49 | -i,--id (default mqtt_sub) MQTT client identifier 50 | -k,--keepalive (default 60) Send MQTT PING period (seconds) 51 | -p,--port (default 1883) MQTT server port number 52 | -t,--topic (string) Subscription topic 53 | -w,--will_message (default .) Last will and testament message 54 | -w,--will_qos (default 0) Last will and testament QOS 55 | -w,--will_retain (default 0) Last will and testament retention 56 | -w,--will_topic (default .) Last will and testament topic 57 | ]] 58 | 59 | local MQTT = require("mqtt_library") 60 | 61 | if (args.debug) then MQTT.Utility.set_debug(true) end 62 | 63 | if (args.keepalive) then MQTT.client.KEEP_ALIVE_TIME = args.keepalive end 64 | 65 | local mqtt_client = MQTT.client.create(args.host, args.port, callback) 66 | 67 | if (args.will_message == "." or args.will_topic == ".") then 68 | mqtt_client:connect(args.id) 69 | else 70 | mqtt_client:connect( 71 | args.id, args.will_topic, args.will_qos, args.will_retain, args.will_message 72 | ) 73 | end 74 | 75 | mqtt_client:subscribe({args.topic}) 76 | 77 | local error_message = nil 78 | 79 | while (error_message == nil) do 80 | error_message = mqtt_client:handler() 81 | socket.sleep(1.0) -- seconds 82 | end 83 | 84 | if (error_message == nil) then 85 | mqtt_client:unsubscribe({args.topic}) 86 | mqtt_client:destroy() 87 | else 88 | print(error_message) 89 | end 90 | 91 | -- ------------------------------------------------------------------------- -- 92 | -------------------------------------------------------------------------------- /lua/utility.lua: -------------------------------------------------------------------------------- 1 | -- utility.lua 2 | -- ~~~~~~~~~~~ 3 | -- Please do not remove the following notices. 4 | -- Copyright (c) 2011 by Geekscape Pty. Ltd. 5 | -- Documentation: http://http://geekscape.github.com/mqtt_lua 6 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 7 | -- Version: 0.2 2012-06-01 8 | -- 9 | -- Notes 10 | -- ~~~~~ 11 | -- - Works on the Sony PlayStation Portable (aka Sony PSP) ... 12 | -- See http://en.wikipedia.org/wiki/Lua_Player_HM 13 | -- 14 | -- ToDo 15 | -- ~~~~ 16 | -- - shift_left() should mask bits past the 8, 16, 32 and 64-bit boundaries. 17 | -- ------------------------------------------------------------------------- -- 18 | 19 | local function isPsp() return(Socket ~= nil) end 20 | 21 | if (isPsp()) then socket = Socket end -- Compatibility ! 22 | 23 | -- ------------------------------------------------------------------------- -- 24 | 25 | local debug_flag = false 26 | 27 | local function set_debug(value) debug_flag = value end 28 | 29 | local function debug(message) 30 | if (debug_flag) then print(message) end 31 | end 32 | 33 | -- ------------------------------------------------------------------------- -- 34 | 35 | local function dump_string(value) 36 | local index 37 | 38 | for index = 1, string.len(value) do 39 | print(string.format("%d: %02x", index, string.byte(value, index))) 40 | end 41 | end 42 | 43 | -- ------------------------------------------------------------------------- -- 44 | 45 | local timer 46 | 47 | if (isPsp()) then 48 | timer = Timer.new() 49 | timer:start() 50 | end 51 | 52 | local function get_time() 53 | if (isPsp()) then 54 | return(timer:time() / 1000) 55 | else 56 | return(socket.gettime()) 57 | end 58 | end 59 | 60 | local function expired(last_time, duration, type) 61 | local time_expired = get_time() >= (last_time + duration) 62 | 63 | if (time_expired) then debug("Event: " .. type) end 64 | return(time_expired) 65 | end 66 | 67 | -- ------------------------------------------------------------------------- -- 68 | 69 | local function shift_left(value, shift) 70 | return(value * 2 ^ shift) 71 | end 72 | 73 | local function shift_right(value, shift) 74 | return(math.floor(value / 2 ^ shift)) 75 | end 76 | 77 | -- ------------------------------------------------------------------------- -- 78 | 79 | local function socket_ready(socket_client) 80 | local ready, read_sockets, write_sockets, error_state = true, nil, nil, nil 81 | 82 | if (not isPsp()) then 83 | read_sockets, write_sockets, error_state = 84 | socket.select({socket_client}, nil, 0.001) 85 | 86 | if (#read_sockets == 0) then ready = false end 87 | end 88 | 89 | return(ready) 90 | end 91 | 92 | local function socket_receive(socket_client, byte_count) 93 | local response, buffer, error_message = nil, nil, nil 94 | 95 | byte_count = byte_count or 128 -- default 96 | 97 | if (isPsp()) then 98 | buffer = socket_client:recv(byte_count) 99 | else 100 | response, error_message, buffer = socket_client:receive("*a") 101 | 102 | if (error_message == "timeout") then error_message = nil end 103 | end 104 | 105 | return(error_message), (buffer) -- nil or "closed" 106 | end 107 | 108 | local function socket_wait_connected(socket_client) 109 | if (isPsp()) then 110 | while (socket_client:isConnected() == false) do 111 | System.sleep(100) 112 | end 113 | else 114 | socket_client:settimeout(0.001) -- So that socket.recieve doesn't block 115 | end 116 | end 117 | 118 | -- ------------------------------------------------------------------------- -- 119 | 120 | local function table_to_string(table) 121 | local result = '' 122 | 123 | if (type(table) == 'table') then 124 | result = '{ ' 125 | 126 | for index = 1, #table do 127 | result = result .. table_to_string(table[index]) 128 | if (index ~= #table) then 129 | result = result .. ', ' 130 | end 131 | end 132 | 133 | result = result .. ' }' 134 | else 135 | result = tostring(table) 136 | end 137 | 138 | return(result) 139 | end 140 | 141 | -- ------------------------------------------------------------------------- -- 142 | -- Define Utility "module" 143 | -- ~~~~~~~~~~~~~~~~~~~~~~~ 144 | 145 | local Utility = {} 146 | 147 | Utility.isPsp = isPsp 148 | Utility.set_debug = set_debug 149 | Utility.debug = debug 150 | Utility.dump_string = dump_string 151 | Utility.get_time = get_time 152 | Utility.expired = expired 153 | Utility.shift_left = shift_left 154 | Utility.shift_right = shift_right 155 | Utility.socket_ready = socket_ready 156 | Utility.socket_receive = socket_receive 157 | Utility.socket_wait_connected = socket_wait_connected 158 | Utility.table_to_string = table_to_string 159 | 160 | -- For ... Utility = require("utility") 161 | 162 | return(Utility) 163 | 164 | -- ------------------------------------------------------------------------- -- 165 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | Lua MQTT client library (version 0.2 2012-06-01) 2 | ======================= 3 | 4 | This project is part of the 5 | [Aiko Platform](https://sites.google.com/site/aikoplatform) 6 | 7 | Contents 8 | -------- 9 | - [Introduction](#introduction) 10 | - [Protocol implementation and restrictions](#restrictions) 11 | - [Download](#download) 12 | - [Feedback and issues](#feedback) 13 | - [Installation](#installation) 14 | - [Usage](#usage) 15 | - [Example code](#example) 16 | - [Library API](#api) 17 | - [Known problems](#problems) 18 | 19 | 20 | Introduction 21 | ------------ 22 | This project provides a client-side (only) implementation of the 23 | [MQTT protocol](http://mqtt.org), 24 | plus command-line utilities for publishing and subscribing to MQTT topics. 25 | Typically, one or more MQTT servers, such as 26 | [mosquitto](http://mosquitto.org) or 27 | [rsmb](http://www.alphaworks.ibm.com/tech/rsmb) 28 | will be running on host systems, with which the Lua MQTT client can interact. 29 | 30 | MQTT stands for "Message Queue Telemetry Transport", a protocol authored by 31 | [Dr. Andy Stanford-Clark](http://wikipedia.org/wiki/Andy_Stanford-Clark) 32 | and Arlen Nipper. 33 | The protocol is a message-based, publish/subscribe transport layer, 34 | which is optimized for simple telemetry applications running on small 35 | micro-controllers, such as an [Arduino](http://arduino.cc), 36 | over low-bandwidth connections. 37 | [MQTT libraries exist](http://mqtt.org/software) 38 | for most popular programming languages, so you can utilize MQTT 39 | on whatever server or device that you require. 40 | 41 | The Lua MQTT client library implements the client-side subset of the 42 | [MQTT protocol specification 3.1](https://www.ibm.com/developerworks/webservices/library/ws-mqtt). 43 | 44 | ![Lua MQTT overview](https://github.com/geekscape/mqtt_lua/raw/master/images/lua_mqtt_overview.jpg) 45 | 46 | A good use-case for this library is running on constrained systems, such as 47 | [OpenWRT](http://openwrt.org), 48 | and acting as a gateway between non-MQTT clients and MQTT servers. 49 | The [Aiko Platform](https://sites.google.com/site/aikoplatform) 50 | uses this approach for aggregating non-TCP/IP devices and forwarding 51 | messages onto TCP/IP networks. 52 | An advantage of using Lua is that only a text editor is required for rapid 53 | development of simple MQTT client applications on platforms such as OpenWRT. 54 | In constrast, working with the C programming language would comparatively 55 | require more effort, due to requiring a cross-platform development environment. 56 | 57 | The Lua MQTT client library also runs (unmodified) on a Sony PlayStation 58 | Portable using the 59 | [Lua Player HM](http://en.wikipedia.org/wiki/Lua_Player_HM) 60 | _(which requires your PSP to be able to run unsigned executables)._ 61 | 62 | ![PlayStation Portable](https://github.com/geekscape/mqtt_lua/raw/master/images/playstation_portable.jpg) 63 | 64 | 65 | Protocol implementation and restrictions 66 | ---------------------------------------- 67 | - Always assumes MQTT connection "clean session" enabled. 68 | - Supports connection last will and testament message. 69 | - Does not support connection username and password. 70 | - Fixed message header byte 1, only implements the "message type". 71 | - Only supports QOS (Quality Of Service) level 0. 72 | - Maximum payload length is 127 bytes (easily increased). 73 | - Publish message doesn't support "message identifier". 74 | - Subscribe acknowledgement messages don't check granted QOS level. 75 | - Outstanding subscribe acknowledgement messages aren't escalated. 76 | - Works on the Sony PlayStation Portable, using 77 | [Lua Player HM](http://en.wikipedia.org/wiki/Lua_Player_HM). 78 | 79 | 80 | Download 81 | -------- 82 | The Lua MQTT client library is cross-platform and should work on any 83 | platform that supports the Lua programming language and network sockets. 84 | 85 | - [Download Lua MQTT client library](https://github.com/geekscape/mqtt_lua/archives/master) 86 | 87 | 88 | Feedback and issues 89 | ------------------- 90 | Tracking is managed via GitHub ... 91 | 92 | - [Enhancements requests and issue tracking](https://github.com/geekscape/mqtt_lua/issues) 93 | 94 | 95 | Installation 96 | ------------ 97 | You may choose to install an MQTT server either on the same or a different 98 | system from the Lua MQTT client library, depending upon your deployment 99 | scenario. 100 | 101 | You can also install the Lua MQTT client library as part of the 102 | [Aiko Platform run-time environment](https://github.com/geekscape/aiko_runtime) 103 | 104 | Prerequisites ... 105 | 106 | - Install [Mosquitto MQTT server](http://mosquitto.org/download) 107 | or any other MQTT server 108 | - Install [Lua programming language](http://www.lua.org/download.html) 109 | - Install [LuaRocks package manager](http://luarocks.org/en/Download) 110 | - Install [LuaSocket](http://w3.impa.br/~diego/software/luasocket) 111 | - Install [PenLight](https://github.com/stevedonovan/Penlight) 112 | 113 | On Linux, Lua and LuaRocks can be installed via your Linux distribution 114 | package manager. 115 | On Mac OS X, Lua and LuaRocks can be installed viarDarwin ports. 116 | After that, LuaSocket and PenLight can be installed via LuaRocks. 117 | 118 | Lua MQTT client library as a LuaRock ... 119 | 120 | * luarocks --from=_server_ install mqtt 121 | 122 | Lua MQTT client library (source code) from GitHub ... 123 | 124 | * TODO 125 | 126 | 127 | Usage 128 | ----- 129 | The Lua MQTT client library comes with three command line utilites, 130 | which are useful for testing the library and acting as example code. 131 | These utilities require that Lua Penlight has been installed. 132 | 133 | #### mqtt\_test: Test publish and receive messages on different topics 134 | 135 | This command periodically publishes a message on topic "test/1" and 136 | subscribes to the topic "test/2". The command exits when the message 137 | "quit" is published on topic "test/2". 138 | 139 | cd $(LUA_MQTT_LIB) // where Lua MQTT library is installed 140 | example/mqtt_test -d localhost // Assume MQTT server is on "localhost" 141 | 142 | -d,--debug Verbose console logging 143 | -i,--id (default MQTT test) MQTT client identifier 144 | -p,--port (default 1883) MQTT server port number 145 | (default localhost) MQTT server hostname 146 | 147 | #### mqtt\_publish: Publish a single message to a specified topic 148 | 149 | This command publishes a single message and then exits. 150 | 151 | example/mqtt_publish -d -t test/1 -m "Test message" 152 | 153 | Only the _--topic_ and _--message_ parameters are required. 154 | 155 | -d,--debug Verbose console logging 156 | -H,--host (default localhost) MQTT server hostname 157 | -i,--id (default MQTT client) MQTT client identifier 158 | -m,--message (string) Message to be published 159 | -p,--port (default 1883) MQTT server port number 160 | -t,--topic (string) Topic on which to publish 161 | -w,--will_message Last will and testament message 162 | -w,--will_qos (default 0) Last will and testament QOS 163 | -w,--will_retain (default 0) Last will and testament retention 164 | -w,--will_topic Last will and testament topic 165 | 166 | #### mqtt\_subscribe: Subscribe to a topic 167 | 168 | This command subscribes to a topic and listens indefinitely for messages. 169 | Use ^C (or similar) to stop execution. 170 | 171 | example/mqtt_subscribe -d -t test/1 172 | 173 | Only the _--topic_ parameter is required. 174 | 175 | -d,--debug Verbose console logging 176 | -H,--host (default localhost) MQTT server hostname 177 | -i,--id (default MQTT client) MQTT client identifier 178 | -k,--keepalive (default 60) Send MQTT PING period (seconds) 179 | -p,--port (default 1883) MQTT server port number 180 | -t,--topic (string) Subscription topic 181 | -w,--will_message Last will and testament message 182 | -w,--will_qos (default 0) Last will and testament QOS 183 | -w,--will_retain (default 0) Last will and testament retention 184 | -w,--will_topic Last will and testament topic 185 | 186 | 187 | Example code 188 | ------------ 189 | The complete functioning code can be viewed here ... 190 | [mqtt_lua/lua/example/mqtt\_test.lua](https://github.com/geekscape/mqtt_lua/blob/master/lua/example/mqtt_test.lua) 191 | 192 | -- Define a function which is called by mqtt_client:handler(), 193 | -- whenever messages are received on the subscribed topics 194 | 195 | function callback(topic, message) 196 | print("Received: " .. topic .. ": " .. message) 197 | if (message == "quit") then running = false end 198 | end 199 | 200 | -- Create an MQTT client instance, connect to the MQTT server and 201 | -- subscribe to the topic called "test/2" 202 | 203 | MQTT = require("mqtt_library") 204 | MQTT.Utility.set_debug(true) 205 | mqtt_client = MQTT.client.create("localhost", nil, callback) 206 | mqtt_client:connect("lua mqtt client")) 207 | mqtt_client:subscribe({"test/2"}) 208 | 209 | -- Continously invoke mqtt_client:handler() to process the MQTT protocol and 210 | -- handle any received messages. Also, publish a message on topic "test/1" 211 | 212 | running = true 213 | 214 | while (running) do 215 | mqtt_client:handler() 216 | mqtt_client:publish("test/1", "test message") 217 | socket.sleep(1.0) -- seconds 218 | end 219 | 220 | -- Clean-up by unsubscribing from the topic and closing the MQTT connection 221 | 222 | mqtt_client:unsubscribe({"test/2"}) 223 | mqtt_client:destroy() 224 | 225 | There are also a number of Lua MQTT client examples in the _example/_ directory. 226 | They can be run from the _lua/_ parent directory, as follow ... 227 | 228 | cd mqtt_client/lua 229 | example/example_00.lua 230 | 231 | 232 | MQTT client Library API 233 | ----------------------- 234 | Once the MQTT client library has been included (via _require_), one or more 235 | MQTT server connections can be created. Using a server connection, the client 236 | may then publish messages directly on a specified topic. Or, subscribe to one 237 | or more topics, where received messages are passed to a callback function 238 | (defined when creating an MQTT client instance). Finally, the client can 239 | unsubscribe from one or more topics and disconnect from the MQTT server. 240 | 241 | Use the Lua _require_ statement to load the MQTT client library ... 242 | 243 | MQTT = require("mqtt_library") 244 | 245 | #### MQTT.Utility.set_debug(): Library debug console logging 246 | 247 | The following statement enables debug console logging for diagnosis. 248 | 249 | MQTT.Utility.set_debug(true) 250 | 251 | #### MQTT.client.create(): Create an MQTT client instance 252 | 253 | Create an MQTT client that will be connected to the specified host. 254 | 255 | mqtt_client = MQTT.client.create(hostname, port, callback) 256 | 257 | The _hostname_ must be provided, but both the _port_ and _callback function_ 258 | parameters are optional. This function returns an MQTT client instance 259 | that must be used for all subsequent MQTT operations for that server connection. 260 | 261 | hostname string: Host name or address of the MQTT broker 262 | port integer: Port number of the MQTT broker (default: 1883) 263 | callback function: Invoked when subscribed topic messages received 264 | 265 | The _callback function_ is defined as follows ... 266 | 267 | function callback(topic, payload) 268 | -- application specific code 269 | end 270 | 271 | topic -- string: Topic for the received message 272 | payload -- string: Message data 273 | 274 | #### MQTT.client:destroy(): Destroy an MQTT client instance 275 | 276 | When finished with a server connection, this statement cleans-up all resources 277 | allocated by the client. 278 | 279 | mqtt_client:destroy() 280 | 281 | #### MQTT.client:connect(): Make a connection to an MQTT server 282 | 283 | Before messages can be transmitted, the MQTT client must connect to the server. 284 | 285 | mqtt_client:connect(identifier) 286 | 287 | Each individual client connection must use a unique identifier. 288 | Only the _identifier_ parameter is required, the remaining parameters 289 | are optional. 290 | 291 | mqtt_client:connect(identifier, will_topic, will_qos, will_retain, will_message) 292 | 293 | MQTT also provides a "last will and testament" for clients, which is a message 294 | automatically sent by the server on behalf of the client, should the connection 295 | fail. 296 | 297 | identifier -- string: MQTT client identifier (maximum 23 characters) 298 | will_topic -- string: Last will and testament topic 299 | will_qos -- byte: Last will and testament Quality Of Service 300 | will_retain -- byte: Last will and testament retention status 301 | will_message -- string: Last will and testament message 302 | 303 | #### MQTT.client:disconnect(): Transmit MQTT Disconnect message 304 | 305 | Transmit an MQTT disconnect message to the server. 306 | 307 | mqtt_client:disconnect() 308 | 309 | #### MQTT.client:publish(): Transmit MQTT publish message 310 | 311 | Transmit a message on a specified topic. 312 | 313 | mqtt_client:publish(topic, payload) 314 | 315 | topic -- string: Topic for the published message 316 | payload -- string: Message data 317 | 318 | #### MQTT.client:subscribe(): Transmit MQTT Subscribe message 319 | 320 | Subscribe to one or more topics. Whenever a message is published to one of 321 | those topics, the callback function (defined above) will be invoked. 322 | 323 | mqtt_client:subscribe(topics) 324 | 325 | topics -- table of strings, e.g. { "topic1", "topic2" } 326 | 327 | #### MQTT.client:handler(): Handle received messages, maintain keep-alive messages 328 | 329 | The _handler()_ function must be called periodically to service incoming 330 | messages and to ensure that keep-alive messages (PING) are being sent 331 | when required. 332 | 333 | The default _KEEP\_ALIVE\_TIME_ is 60 seconds, therefore _handler()_ must be 334 | invoked more often than once per minute. 335 | 336 | Should any messages be received on the subscribed topics, then _handler()_ 337 | will invoke the callback function (defined above). 338 | 339 | mqtt_client:handler() 340 | 341 | #### MQTT.client:unsubscribe(): Transmit MQTT Unsubscribe message 342 | 343 | Unsubscribe from one or more topics, so that messages published to those 344 | topics are no longer received. 345 | 346 | topics -- table of strings, e.g. { "topic1", "topic2" } 347 | 348 | 349 | Known problems 350 | -------------- 351 | - Occasional "MQTT.client:handler(): Message length mismatch" errors, 352 | particularly when subscribed topics are transmitting many messages. 353 | 354 | - Not really a problem, but if you find that the MQTT socket connection is 355 | being closed for no apparent reason, particularly for subscribers ... 356 | then check that all MQTT clients are using a unique client identifier. 357 | -------------------------------------------------------------------------------- /lua/mqtt_library.lua: -------------------------------------------------------------------------------- 1 | -- mqtt_library.lua 2 | -- ~~~~~~~~~~~~~~~~ 3 | -- Please do not remove the following notices. 4 | -- Copyright (c) 2011-2012 by Geekscape Pty. Ltd. 5 | -- License: AGPLv3 http://geekscape.org/static/aiko_license.html 6 | -- Version: 0.2 2012-06-01 7 | -- 8 | -- Documentation 9 | -- ~~~~~~~~~~~~~ 10 | -- MQTT Lua web-site 11 | -- http://geekscape.github.com/mqtt_lua 12 | -- 13 | -- MQTT Lua repository notes 14 | -- https://github.com/geekscape/mqtt_lua/blob/master/readme.markdown 15 | -- 16 | -- Aiko Platform web-site 17 | -- https://sites.google.com/site/aikoplatform 18 | -- 19 | -- References 20 | -- ~~~~~~~~~~ 21 | -- MQTT web-site 22 | -- http://mqtt.org 23 | 24 | -- MQTT protocol specification 3.1 25 | -- https://www.ibm.com/developerworks/webservices/library/ws-mqtt 26 | -- http://mqtt.org/wiki/doku.php/mqtt_protocol # Clarifications 27 | -- 28 | -- Notes 29 | -- ~~~~~ 30 | -- - Always assumes MQTT connection "clean session" enabled. 31 | -- - Supports connection last will and testament message. 32 | -- - Does not support connection username and password. 33 | -- - Fixed message header byte 1, only implements the "message type". 34 | -- - Only supports QOS level 0. 35 | -- - Maximum payload length is 268,435,455 bytes (as per specification). 36 | -- - Publish message doesn't support "message identifier". 37 | -- - Subscribe acknowledgement messages don't check granted QOS level. 38 | -- - Outstanding subscribe acknowledgement messages aren't escalated. 39 | -- - Works on the Sony PlayStation Portable (aka Sony PSP) ... 40 | -- See http://en.wikipedia.org/wiki/Lua_Player_HM 41 | -- 42 | -- ToDo 43 | -- ~~~~ 44 | -- * Consider when payload needs to be an array of bytes (not characters). 45 | -- * Maintain both "last_activity_out" and "last_activity_in". 46 | -- * - http://mqtt.org/wiki/doku.php/keepalive_for_the_client 47 | -- * Update "last_activity_in" when messages are received. 48 | -- * When a PINGREQ is sent, must check for a PINGRESP, within KEEP_ALIVE_TIME.. 49 | -- * Otherwise, fail the connection. 50 | -- * When connecting, wait for CONACK, until KEEP_ALIVE_TIME, before failing. 51 | -- * Should MQTT.client:connect() be asynchronous with a callback ? 52 | -- * Review all public APIs for asynchronous callback behaviour. 53 | -- * Implement parse PUBACK message. 54 | -- * Handle failed subscriptions, i.e no subscription acknowledgement received. 55 | -- * Fix problem when KEEP_ALIVE_TIME is short, e.g. mqtt_publish -k 1 56 | -- MQTT.client:handler(): Message length mismatch 57 | -- - On socket error, optionally try reconnection to MQTT server. 58 | -- - Consider use of assert() and pcall() ? 59 | -- - Only expose public API functions, don't expose internal API functions. 60 | -- - Refactor "if self.connected()" to "self.checkConnected(error_message)". 61 | -- - Maintain and publish messaging statistics. 62 | -- - Memory heap/stack monitoring. 63 | -- - When debugging, why isn't mosquitto sending back CONACK error code ? 64 | -- - Subscription callbacks invoked by topic name (including wildcards). 65 | -- - Implement asynchronous state machine, rather than single-thread waiting. 66 | -- - After CONNECT, expect and wait for a CONACK. 67 | -- - Implement complete MQTT broker (server). 68 | -- - Consider using Copas http://keplerproject.github.com/copas/manual.html 69 | -- ------------------------------------------------------------------------- -- 70 | 71 | function isPsp() return(Socket ~= nil) end 72 | 73 | if (not isPsp()) then 74 | require("socket") 75 | require("io") 76 | require("ltn12") 77 | --require("ssl") 78 | end 79 | 80 | local MQTT = {} 81 | 82 | MQTT.Utility = require("utility") 83 | 84 | MQTT.VERSION = 0x03 85 | 86 | MQTT.ERROR_TERMINATE = false -- Message handler errors terminate process ? 87 | 88 | MQTT.DEFAULT_BROKER_HOSTNAME = "localhost" 89 | 90 | MQTT.client = {} 91 | MQTT.client.__index = MQTT.client 92 | 93 | MQTT.client.DEFAULT_PORT = 1883 94 | MQTT.client.KEEP_ALIVE_TIME = 60 -- seconds (maximum is 65535) 95 | MQTT.client.MAX_PAYLOAD_LENGTH = 268435455 -- bytes 96 | 97 | -- MQTT 3.1 Specification: Section 2.1: Fixed header, Message type 98 | 99 | MQTT.message = {} 100 | MQTT.message.TYPE_RESERVED = 0x00 101 | MQTT.message.TYPE_CONNECT = 0x01 102 | MQTT.message.TYPE_CONACK = 0x02 103 | MQTT.message.TYPE_PUBLISH = 0x03 104 | MQTT.message.TYPE_PUBACK = 0x04 105 | MQTT.message.TYPE_PUBREC = 0x05 106 | MQTT.message.TYPE_PUBREL = 0x06 107 | MQTT.message.TYPE_PUBCOMP = 0x07 108 | MQTT.message.TYPE_SUBSCRIBE = 0x08 109 | MQTT.message.TYPE_SUBACK = 0x09 110 | MQTT.message.TYPE_UNSUBSCRIBE = 0x0a 111 | MQTT.message.TYPE_UNSUBACK = 0x0b 112 | MQTT.message.TYPE_PINGREQ = 0x0c 113 | MQTT.message.TYPE_PINGRESP = 0x0d 114 | MQTT.message.TYPE_DISCONNECT = 0x0e 115 | MQTT.message.TYPE_RESERVED = 0x0f 116 | 117 | -- MQTT 3.1 Specification: Section 3.2: CONACK acknowledge connection errors 118 | -- http://mqtt.org/wiki/doku.php/extended_connack_codes 119 | 120 | MQTT.CONACK = {} 121 | MQTT.CONACK.error_message = { -- CONACK return code used as the index 122 | "Unacceptable protocol version", 123 | "Identifer rejected", 124 | "Server unavailable", 125 | "Bad user name or password", 126 | "Not authorized" 127 | --"Invalid will topic" -- Proposed 128 | } 129 | 130 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 131 | -- Create an MQTT client instance 132 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 133 | 134 | function MQTT.client.create( -- Public API 135 | hostname, -- string: Host name or address of the MQTT broker 136 | port, -- integer: Port number of the MQTT broker (default: 1883) 137 | callback) -- function: Invoked when subscribed topic messages received 138 | -- return: mqtt_client table 139 | 140 | local mqtt_client = {} 141 | 142 | setmetatable(mqtt_client, MQTT.client) 143 | 144 | mqtt_client.callback = callback -- function(topic, payload) 145 | mqtt_client.hostname = hostname 146 | mqtt_client.port = port or MQTT.client.DEFAULT_PORT 147 | 148 | mqtt_client.connected = false 149 | mqtt_client.destroyed = false 150 | mqtt_client.last_activity = 0 151 | mqtt_client.message_id = 0 152 | mqtt_client.outstanding = {} 153 | mqtt_client.socket_client = nil 154 | 155 | return(mqtt_client) 156 | end 157 | 158 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 159 | -- Transmit MQTT Client request a connection to an MQTT broker (server) 160 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 161 | -- MQTT 3.1 Specification: Section 3.1: CONNECT 162 | 163 | function MQTT.client:connect( -- Public API 164 | identifier, -- string: MQTT client identifier (maximum 23 characters) 165 | will_topic, -- string: Last will and testament topic 166 | will_qos, -- byte: Last will and testament Quality Of Service 167 | will_retain, -- byte: Last will and testament retention status 168 | will_message) -- string: Last will and testament message 169 | -- return: nil or error message 170 | 171 | if (self.connected) then 172 | return("MQTT.client:connect(): Already connected") 173 | end 174 | 175 | MQTT.Utility.debug("MQTT.client:connect(): " .. identifier) 176 | 177 | self.socket_client = socket.connect(self.hostname, self.port) 178 | 179 | if (self.socket_client == nil) then 180 | return("MQTT.client:connect(): Couldn't open MQTT broker connection") 181 | end 182 | 183 | MQTT.Utility.socket_wait_connected(self.socket_client) 184 | 185 | self.connected = true 186 | 187 | -- Construct CONNECT variable header fields (bytes 1 through 9) 188 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 189 | local payload 190 | payload = MQTT.client.encode_utf8("MQIsdp") 191 | payload = payload .. string.char(MQTT.VERSION) 192 | 193 | -- Connect flags (byte 10) 194 | -- ~~~~~~~~~~~~~ 195 | -- bit 7: Username flag = 0 -- recommended no more than 12 characters 196 | -- bit 6: Password flag = 0 -- ditto 197 | -- bit 5: Will retain = 0 198 | -- bits 4,3: Will QOS = 00 199 | -- bit 2: Will flag = 0 200 | -- bit 1: Clean session = 1 201 | -- bit 0: Unused = 0 202 | 203 | if (will_topic == nil) then 204 | payload = payload .. string.char(0x02) -- Clean session, no last will 205 | else 206 | local flags 207 | flags = MQTT.Utility.shift_left(will_retain, 5) 208 | flags = flags + MQTT.Utility.shift_left(will_qos, 3) + 0x06 209 | payload = payload .. string.char(flags) 210 | end 211 | 212 | -- Keep alive timer (bytes 11 LSB and 12 MSB, unit is seconds) 213 | -- ~~~~~~~~~~~~~~~~~ 214 | payload = payload .. string.char(math.floor(MQTT.client.KEEP_ALIVE_TIME / 256)) 215 | payload = payload .. string.char(MQTT.client.KEEP_ALIVE_TIME % 256) 216 | 217 | -- Client identifier 218 | -- ~~~~~~~~~~~~~~~~~ 219 | payload = payload .. MQTT.client.encode_utf8(identifier) 220 | 221 | -- Last will and testament 222 | -- ~~~~~~~~~~~~~~~~~~~~~~~ 223 | if (will_topic ~= nil) then 224 | payload = payload .. MQTT.client.encode_utf8(will_topic) 225 | payload = payload .. MQTT.client.encode_utf8(will_message) 226 | end 227 | 228 | -- Send MQTT message 229 | -- ~~~~~~~~~~~~~~~~~ 230 | return(self:message_write(MQTT.message.TYPE_CONNECT, payload)) 231 | end 232 | 233 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 234 | -- Destroy an MQTT client instance 235 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 236 | 237 | function MQTT.client:destroy() -- Public API 238 | MQTT.Utility.debug("MQTT.client:destroy()") 239 | 240 | if (self.destroyed == false) then 241 | self.destroyed = true -- Avoid recursion when message_write() fails 242 | 243 | if (self.connected) then self:disconnect() end 244 | 245 | self.callback = nil 246 | self.outstanding = nil 247 | end 248 | end 249 | 250 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 251 | -- Transmit MQTT Disconnect message 252 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 253 | -- MQTT 3.1 Specification: Section 3.14: Disconnect notification 254 | -- 255 | -- bytes 1,2: Fixed message header, see MQTT.client:message_write() 256 | 257 | function MQTT.client:disconnect() -- Public API 258 | MQTT.Utility.debug("MQTT.client:disconnect()") 259 | 260 | if (self.connected) then 261 | self:message_write(MQTT.message.TYPE_DISCONNECT, nil) 262 | self.socket_client:close() 263 | self.connected = false 264 | else 265 | error("MQTT.client:disconnect(): Already disconnected") 266 | end 267 | end 268 | 269 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 270 | -- Encode a message string using UTF-8 (for variable header) 271 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 272 | -- MQTT 3.1 Specification: Section 2.5: MQTT and UTF-8 273 | -- 274 | -- byte 1: String length MSB 275 | -- byte 2: String length LSB 276 | -- bytes 3-n: String encoded as UTF-8 277 | 278 | function MQTT.client.encode_utf8( -- Internal API 279 | input) -- string 280 | 281 | local output 282 | output = string.char(math.floor(#input / 256)) 283 | output = output .. string.char(#input % 256) 284 | output = output .. input 285 | 286 | return(output) 287 | end 288 | 289 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 290 | -- Handle received messages and maintain keep-alive PING messages 291 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 292 | -- This function must be invoked periodically (more often than the 293 | -- MQTT.client.KEEP_ALIVE_TIME) which maintains the connection and 294 | -- services the incoming subscribed topic messages. 295 | 296 | function MQTT.client:handler() -- Public API 297 | if (self.connected == false) then 298 | error("MQTT.client:handler(): Not connected") 299 | end 300 | 301 | MQTT.Utility.debug("MQTT.client:handler()") 302 | 303 | -- Transmit MQTT PING message 304 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~ 305 | -- MQTT 3.1 Specification: Section 3.13: PING request 306 | -- 307 | -- bytes 1,2: Fixed message header, see MQTT.client:message_write() 308 | 309 | local activity_timeout = self.last_activity + MQTT.client.KEEP_ALIVE_TIME 310 | 311 | if (MQTT.Utility.get_time() > activity_timeout) then 312 | MQTT.Utility.debug("MQTT.client:handler(): PINGREQ") 313 | 314 | self:message_write(MQTT.message.TYPE_PINGREQ, nil) 315 | end 316 | 317 | -- Check for available client socket data 318 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 319 | local ready = MQTT.Utility.socket_ready(self.socket_client) 320 | 321 | if (ready) then 322 | local error_message, buffer = 323 | MQTT.Utility.socket_receive(self.socket_client) 324 | 325 | if (error_message ~= nil) then 326 | self:destroy() 327 | error_message = "socket_client:receive(): " .. error_message 328 | MQTT.Utility.debug(error_message) 329 | return(error_message) 330 | end 331 | 332 | if (buffer ~= nil and #buffer > 0) then 333 | local index = 1 334 | 335 | -- Parse individual messages (each must be at least 2 bytes long) 336 | -- Decode "remaining length" (MQTT v3.1 specification pages 6 and 7) 337 | 338 | while (index < #buffer) do 339 | local message_type_flags = string.byte(buffer, index) 340 | local multiplier = 1 341 | local remaining_length = 0 342 | 343 | repeat 344 | index = index + 1 345 | local digit = string.byte(buffer, index) 346 | remaining_length = remaining_length + ((digit % 128) * multiplier) 347 | multiplier = multiplier * 128 348 | until digit < 128 -- check continuation bit 349 | 350 | local message = string.sub(buffer, index + 1, index + remaining_length) 351 | 352 | if (#message == remaining_length) then 353 | self:parse_message(message_type_flags, remaining_length, message) 354 | else 355 | MQTT.Utility.debug( 356 | "MQTT.client:handler(): Incorrect remaining length: " .. 357 | remaining_length .. " ~= message length: " .. #message 358 | ) 359 | end 360 | 361 | index = index + remaining_length + 1 362 | end 363 | 364 | -- Check for any left over bytes, i.e. partial message received 365 | 366 | if (index ~= (#buffer + 1)) then 367 | local error_message = 368 | "MQTT.client:handler(): Partial message received" .. 369 | index .. " ~= " .. (#buffer + 1) 370 | 371 | if (MQTT.ERROR_TERMINATE) then -- TODO: Refactor duplicate code 372 | self:destroy() 373 | error(error_message) 374 | else 375 | MQTT.Utility.debug(error_message) 376 | end 377 | end 378 | end 379 | end 380 | 381 | return(nil) 382 | end 383 | 384 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 385 | -- Transmit an MQTT message 386 | -- ~~~~~~~~~~~~~~~~~~~~~~~~ 387 | -- MQTT 3.1 Specification: Section 2.1: Fixed header 388 | -- 389 | -- byte 1: Message type and flags (DUP, QOS level, and Retain) fields 390 | -- bytes 2-5: Remaining length field (between one and four bytes long) 391 | -- bytes m- : Optional variable header and payload 392 | 393 | function MQTT.client:message_write( -- Internal API 394 | message_type, -- enumeration 395 | payload) -- string 396 | -- return: nil or error message 397 | 398 | -- TODO: Complete implementation of fixed header byte 1 399 | 400 | local message = string.char(MQTT.Utility.shift_left(message_type, 4)) 401 | 402 | if (payload == nil) then 403 | message = message .. string.char(0) -- Zero length, no payload 404 | else 405 | if (#payload > MQTT.client.MAX_PAYLOAD_LENGTH) then 406 | return( 407 | "MQTT.client:message_write(): Payload length = " .. #payload .. 408 | " exceeds maximum of " .. MQTT.client.MAX_PAYLOAD_LENGTH 409 | ) 410 | end 411 | 412 | -- Encode "remaining length" (MQTT v3.1 specification pages 6 and 7) 413 | 414 | local remaining_length = #payload 415 | 416 | repeat 417 | local digit = remaining_length % 128 418 | remaining_length = math.floor(remaining_length / 128) 419 | if (remaining_length > 0) then digit = digit + 128 end -- continuation bit 420 | message = message .. string.char(digit) 421 | until remaining_length == 0 422 | 423 | message = message .. payload 424 | end 425 | 426 | local status, error_message = self.socket_client:send(message) 427 | 428 | if (status == nil) then 429 | self:destroy() 430 | return("MQTT.client:message_write(): " .. error_message) 431 | end 432 | 433 | self.last_activity = MQTT.Utility.get_time() 434 | return(nil) 435 | end 436 | 437 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 438 | -- Parse MQTT message 439 | -- ~~~~~~~~~~~~~~~~~~ 440 | -- MQTT 3.1 Specification: Section 2.1: Fixed header 441 | -- 442 | -- byte 1: Message type and flags (DUP, QOS level, and Retain) fields 443 | -- bytes 2-5: Remaining length field (between one and four bytes long) 444 | -- bytes m- : Optional variable header and payload 445 | -- 446 | -- The message type/flags and remaining length are already parsed and 447 | -- removed from the message by the time this function is invoked. 448 | -- Leaving just the optional variable header and payload. 449 | 450 | function MQTT.client:parse_message( -- Internal API 451 | message_type_flags, -- byte 452 | remaining_length, -- integer 453 | message) -- string: Optional variable header and payload 454 | 455 | local message_type = MQTT.Utility.shift_right(message_type_flags, 4) 456 | 457 | -- TODO: MQTT.message.TYPE table should include "parser handler" function. 458 | -- This would nicely collapse the if .. then .. elseif .. end. 459 | 460 | if (message_type == MQTT.message.TYPE_CONACK) then 461 | self:parse_message_conack(message_type_flags, remaining_length, message) 462 | 463 | elseif (message_type == MQTT.message.TYPE_PUBLISH) then 464 | self:parse_message_publish(message_type_flags, remaining_length, message) 465 | 466 | elseif (message_type == MQTT.message.TYPE_PUBACK) then 467 | print("MQTT.client:parse_message(): PUBACK -- UNIMPLEMENTED --") -- TODO 468 | 469 | elseif (message_type == MQTT.message.TYPE_SUBACK) then 470 | self:parse_message_suback(message_type_flags, remaining_length, message) 471 | 472 | elseif (message_type == MQTT.message.TYPE_UNSUBACK) then 473 | self:parse_message_unsuback(message_type_flags, remaining_length, message) 474 | 475 | elseif (message_type == MQTT.message.TYPE_PINGREQ) then 476 | self:ping_response() 477 | 478 | elseif (message_type == MQTT.message.TYPE_PINGRESP) then 479 | self:parse_message_pingresp(message_type_flags, remaining_length, message) 480 | 481 | else 482 | local error_message = 483 | "MQTT.client:parse_message(): Unknown message type: " .. message_type 484 | 485 | if (MQTT.ERROR_TERMINATE) then -- TODO: Refactor duplicate code 486 | self:destroy() 487 | error(error_message) 488 | else 489 | MQTT.Utility.debug(error_message) 490 | end 491 | end 492 | end 493 | 494 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 495 | -- Parse MQTT CONACK message 496 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~ 497 | -- MQTT 3.1 Specification: Section 3.2: CONACK Acknowledge connection 498 | -- 499 | -- byte 1: Reserved value 500 | -- byte 2: Connect return code, see MQTT.CONACK.error_message[] 501 | 502 | function MQTT.client:parse_message_conack( -- Internal API 503 | message_type_flags, -- byte 504 | remaining_length, -- integer 505 | message) -- string 506 | 507 | local me = "MQTT.client:parse_message_conack()" 508 | MQTT.Utility.debug(me) 509 | 510 | if (remaining_length ~= 2) then 511 | error(me .. ": Invalid remaining length") 512 | end 513 | 514 | local return_code = string.byte(message, 2) 515 | 516 | if (return_code ~= 0) then 517 | local error_message = "Unknown return code" 518 | 519 | if (return_code <= table.getn(MQTT.CONACK.error_message)) then 520 | error_message = MQTT.CONACK.error_message[return_code] 521 | end 522 | 523 | error(me .. ": Connection refused: " .. error_message) 524 | end 525 | end 526 | 527 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 528 | -- Parse MQTT PINGRESP message 529 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 530 | -- MQTT 3.1 Specification: Section 3.13: PING response 531 | 532 | function MQTT.client:parse_message_pingresp( -- Internal API 533 | message_type_flags, -- byte 534 | remaining_length, -- integer 535 | message) -- string 536 | 537 | local me = "MQTT.client:parse_message_pingresp()" 538 | MQTT.Utility.debug(me) 539 | 540 | if (remaining_length ~= 0) then 541 | error(me .. ": Invalid remaining length") 542 | end 543 | 544 | -- ToDo: self.ping_response_outstanding = false 545 | end 546 | 547 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 548 | -- Parse MQTT PUBLISH message 549 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~ 550 | -- MQTT 3.1 Specification: Section 3.3: Publish message 551 | -- 552 | -- Variable header .. 553 | -- bytes 1- : Topic name and optional Message Identifier (if QOS > 0) 554 | -- bytes m- : Payload 555 | 556 | function MQTT.client:parse_message_publish( -- Internal API 557 | message_type_flags, -- byte 558 | remaining_length, -- integer 559 | message) -- string 560 | 561 | local me = "MQTT.client:parse_message_publish()" 562 | MQTT.Utility.debug(me) 563 | 564 | if (self.callback ~= nil) then 565 | if (remaining_length < 3) then 566 | error(me .. ": Invalid remaining length: " .. remaining_length) 567 | end 568 | 569 | local topic_length = string.byte(message, 1) * 256 570 | topic_length = topic_length + string.byte(message, 2) 571 | local topic = string.sub(message, 3, topic_length + 2) 572 | local index = topic_length + 3 573 | 574 | -- Handle optional Message Identifier, for QOS levels 1 and 2 575 | -- TODO: Enable Subscribe with QOS and deal with PUBACK, etc. 576 | 577 | local qos = MQTT.Utility.shift_left(message_type_flags, 1) % 3 578 | 579 | if (qos > 0) then 580 | local message_id = string.byte(message, index) * 256 581 | message_id = message_id + string.byte(message, index + 1) 582 | index = index + 2 583 | end 584 | 585 | local payload_length = remaining_length - index + 1 586 | local payload = string.sub(message, index, index + payload_length - 1) 587 | 588 | self.callback(topic, payload) 589 | end 590 | end 591 | 592 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 593 | -- Parse MQTT SUBACK message 594 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~ 595 | -- MQTT 3.1 Specification: Section 3.9: SUBACK Subscription acknowledgement 596 | -- 597 | -- bytes 1,2: Message Identifier 598 | -- bytes 3- : List of granted QOS for each subscribed topic 599 | 600 | function MQTT.client:parse_message_suback( -- Internal API 601 | message_type_flags, -- byte 602 | remaining_length, -- integer 603 | message) -- string 604 | 605 | local me = "MQTT.client:parse_message_suback()" 606 | MQTT.Utility.debug(me) 607 | 608 | if (remaining_length < 3) then 609 | error(me .. ": Invalid remaining length: " .. remaining_length) 610 | end 611 | 612 | local message_id = string.byte(message, 1) * 256 + string.byte(message, 2) 613 | local outstanding = self.outstanding[message_id] 614 | 615 | if (outstanding == nil) then 616 | error(me .. ": No outstanding message: " .. message_id) 617 | end 618 | 619 | self.outstanding[message_id] = nil 620 | 621 | if (outstanding[1] ~= "subscribe") then 622 | error(me .. ": Outstanding message wasn't SUBSCRIBE") 623 | end 624 | 625 | local topic_count = table.getn(outstanding[2]) 626 | 627 | if (topic_count ~= remaining_length - 2) then 628 | error(me .. ": Didn't received expected number of topics: " .. topic_count) 629 | end 630 | end 631 | 632 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 633 | -- Parse MQTT UNSUBACK message 634 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 635 | -- MQTT 3.1 Specification: Section 3.11: UNSUBACK Unsubscription acknowledgement 636 | -- 637 | -- bytes 1,2: Message Identifier 638 | 639 | function MQTT.client:parse_message_unsuback( -- Internal API 640 | message_type_flags, -- byte 641 | remaining_length, -- integer 642 | message) -- string 643 | 644 | local me = "MQTT.client:parse_message_unsuback()" 645 | MQTT.Utility.debug(me) 646 | 647 | if (remaining_length ~= 2) then 648 | error(me .. ": Invalid remaining length") 649 | end 650 | 651 | local message_id = string.byte(message, 1) * 256 + string.byte(message, 2) 652 | 653 | local outstanding = self.outstanding[message_id] 654 | 655 | if (outstanding == nil) then 656 | error(me .. ": No outstanding message: " .. message_id) 657 | end 658 | 659 | self.outstanding[message_id] = nil 660 | 661 | if (outstanding[1] ~= "unsubscribe") then 662 | error(me .. ": Outstanding message wasn't UNSUBSCRIBE") 663 | end 664 | end 665 | 666 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 667 | -- Transmit MQTT Ping response message 668 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 669 | -- MQTT 3.1 Specification: Section 3.13: PING response 670 | 671 | function MQTT.client:ping_response() -- Internal API 672 | MQTT.Utility.debug("MQTT.client:ping_response()") 673 | 674 | if (self.connected == false) then 675 | error("MQTT.client:ping_response(): Not connected") 676 | end 677 | 678 | self:message_write(MQTT.message.TYPE_PINGRESP, nil) 679 | end 680 | 681 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 682 | -- Transmit MQTT Publish message 683 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 684 | -- MQTT 3.1 Specification: Section 3.3: Publish message 685 | -- 686 | -- bytes 1,2: Fixed message header, see MQTT.client:message_write() 687 | -- Variable header .. 688 | -- bytes 3- : Topic name and optional Message Identifier (if QOS > 0) 689 | -- bytes m- : Payload 690 | 691 | function MQTT.client:publish( -- Public API 692 | topic, -- string 693 | payload) -- string 694 | 695 | if (self.connected == false) then 696 | error("MQTT.client:publish(): Not connected") 697 | end 698 | 699 | MQTT.Utility.debug("MQTT.client:publish(): " .. topic) 700 | 701 | local message = MQTT.client.encode_utf8(topic) .. payload 702 | 703 | self:message_write(MQTT.message.TYPE_PUBLISH, message) 704 | end 705 | 706 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 707 | -- Transmit MQTT Subscribe message 708 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 709 | -- MQTT 3.1 Specification: Section 3.8: Subscribe to named topics 710 | -- 711 | -- bytes 1,2: Fixed message header, see MQTT.client:message_write() 712 | -- Variable header .. 713 | -- bytes 3,4: Message Identifier 714 | -- bytes 5- : List of topic names and their QOS level 715 | 716 | function MQTT.client:subscribe( -- Public API 717 | topics) -- table of strings 718 | 719 | if (self.connected == false) then 720 | error("MQTT.client:subscribe(): Not connected") 721 | end 722 | 723 | self.message_id = self.message_id + 1 724 | 725 | local message 726 | message = string.char(math.floor(self.message_id / 256)) 727 | message = message .. string.char(self.message_id % 256) 728 | 729 | for index, topic in ipairs(topics) do 730 | MQTT.Utility.debug("MQTT.client:subscribe(): " .. topic) 731 | message = message .. MQTT.client.encode_utf8(topic) 732 | message = message .. string.char(0) -- QOS level 0 733 | end 734 | 735 | self:message_write(MQTT.message.TYPE_SUBSCRIBE, message) 736 | 737 | self.outstanding[self.message_id] = { "subscribe", topics } 738 | end 739 | 740 | -- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -- 741 | -- Transmit MQTT Unsubscribe message 742 | -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 743 | -- MQTT 3.1 Specification: Section 3.10: Unsubscribe from named topics 744 | -- 745 | -- bytes 1,2: Fixed message header, see MQTT.client:message_write() 746 | -- Variable header .. 747 | -- bytes 3,4: Message Identifier 748 | -- bytes 5- : List of topic names 749 | 750 | 751 | function MQTT.client:unsubscribe( -- Public API 752 | topics) -- table of strings 753 | 754 | if (self.connected == false) then 755 | error("MQTT.client:unsubscribe(): Not connected") 756 | end 757 | 758 | self.message_id = self.message_id + 1 759 | 760 | local message 761 | message = string.char(math.floor(self.message_id / 256)) 762 | message = message .. string.char(self.message_id % 256) 763 | 764 | for index, topic in ipairs(topics) do 765 | MQTT.Utility.debug("MQTT.client:unsubscribe(): " .. topic) 766 | message = message .. MQTT.client.encode_utf8(topic) 767 | end 768 | 769 | self:message_write(MQTT.message.TYPE_UNSUBSCRIBE, message) 770 | 771 | self.outstanding[self.message_id] = { "unsubscribe", topics } 772 | end 773 | 774 | -- For ... MQTT = require("mqtt_library") 775 | 776 | return(MQTT) 777 | --------------------------------------------------------------------------------