Initial commit of MQTT Lua client for Eclipse Paho.
diff --git a/images/aiko_gateway.jpg b/images/aiko_gateway.jpg
new file mode 100644
index 0000000..be791a1
--- /dev/null
+++ b/images/aiko_gateway.jpg
Binary files differ
diff --git a/images/lua_mqtt_overview.dia b/images/lua_mqtt_overview.dia
new file mode 100644
index 0000000..08e4299
--- /dev/null
+++ b/images/lua_mqtt_overview.dia
Binary files differ
diff --git a/images/lua_mqtt_overview.jpg b/images/lua_mqtt_overview.jpg
new file mode 100644
index 0000000..844a104
--- /dev/null
+++ b/images/lua_mqtt_overview.jpg
Binary files differ
diff --git a/images/pebble.jpg b/images/pebble.jpg
new file mode 100644
index 0000000..f439ec3
--- /dev/null
+++ b/images/pebble.jpg
Binary files differ
diff --git a/images/playstation_portable.jpg b/images/playstation_portable.jpg
new file mode 100644
index 0000000..2fec195
--- /dev/null
+++ b/images/playstation_portable.jpg
Binary files differ
diff --git a/lua/example/example_00.lua b/lua/example/example_00.lua
new file mode 100755
index 0000000..3c2286c
--- /dev/null
+++ b/lua/example/example_00.lua
@@ -0,0 +1,84 @@
+#!/usr/bin/lua
+--
+-- example_00.lua
+-- ~~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- ------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Description
+-- ~~~~~~~~~~~
+-- Subscribe to a topic and publish all received messages on another topic.
+--
+-- ToDo
+-- ~~~~
+-- - On failure, automatically reconnect to MQTT server.
+-- - Error handling: MQTT.client.connect()
+-- - Error handling: MQTT.client.destroy()
+-- - Error handling: MQTT.client.disconnect()
+-- - Error handling: MQTT.client.handler()
+-- - Error handling: MQTT.client.publish()
+-- - Error handling: MQTT.client.subscribe()
+-- - Error handling: MQTT.client.unsubscribe()
+-- ------------------------------------------------------------------------- --
+
+function callback(
+ topic, -- string
+ message) -- string
+
+ print("Topic: " .. topic .. ", message: '" .. message .. "'")
+
+ mqtt_client:publish(args.topic_p, message)
+end
+
+-- ------------------------------------------------------------------------- --
+
+function is_openwrt()
+ return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT
+end
+
+-- ------------------------------------------------------------------------- --
+
+if (not is_openwrt()) then require("luarocks.require") end
+local lapp = require("pl.lapp")
+
+args = lapp [[
+ Subscribe to topic_s and publish all messages on topic_p
+ -H,--host (default localhost) MQTT server hostname
+ -i,--id (default example_00) MQTT client identifier
+ -p,--port (default 1883) MQTT server port number
+ -s,--topic_s (default test/1) Subscribe topic
+ -t,--topic_p (default test/2) Publish topic
+]]
+
+local MQTT = require("mqtt_library")
+
+mqtt_client = MQTT.client.create(args.host, args.port, callback)
+
+mqtt_client:connect(args.id)
+
+mqtt_client:subscribe({ args.topic_s })
+
+local error_message = nil
+
+while (error_message == nil) do
+ error_message = mqtt_client:handler()
+ socket.sleep(1.0) -- seconds
+end
+
+if (error_message == nil) then
+ mqtt_client:unsubscribe({ args.topic_s })
+ mqtt_client:destroy()
+else
+ print(error_message)
+end
+
+-- ------------------------------------------------------------------------- --
diff --git a/lua/example/example_01.lua b/lua/example/example_01.lua
new file mode 100755
index 0000000..9b8bfdd
--- /dev/null
+++ b/lua/example/example_01.lua
@@ -0,0 +1,90 @@
+#!/usr/bin/lua
+--
+-- example_01.lua
+-- ~~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- ------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Description
+-- ~~~~~~~~~~~
+-- Subscribe to a topic on one MQTT server and publish all received messages
+-- to a topic on another MQTT server.
+--
+-- ToDo
+-- ~~~~
+-- - On failure, automatically reconnect to MQTT server(s).
+-- ------------------------------------------------------------------------- --
+
+function callback(
+ topic, -- string
+ message) -- string
+
+ print("Topic: " .. topic .. ", message: '" .. message .. "'")
+
+ mqtt_client2:publish(args.topic_p, message)
+end
+
+-- ------------------------------------------------------------------------- --
+
+function is_openwrt()
+ return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT
+end
+
+-- ------------------------------------------------------------------------- --
+
+if (not is_openwrt()) then require("luarocks.require") end
+local lapp = require("pl.lapp")
+
+args = lapp [[
+ Subscribe to topic_s and publish all messages on topic_p
+ -g,--host_s (default localhost) Subscribe MQTT server hostname
+ -H,--host_p (default localhost) Publish MQTT server hostname
+ -i,--id (default example_01) MQTT client identifier
+ -p,--port_s (default 1883) Subscribe MQTT server port number
+ -q,--port_p (default 1883) Publish MQTT server port number
+ -s,--topic_s (default test/1) Subscribe topic
+ -t,--topic_p (default test/2) Publish topic
+]]
+
+local MQTT = require("mqtt_library")
+
+mqtt_client1 = MQTT.client.create(args.host_s, args.port_s, callback)
+mqtt_client2 = MQTT.client.create(args.host_p, args.port_p)
+
+mqtt_client1:connect(args.id .. "a")
+mqtt_client2:connect(args.id .. "b")
+
+mqtt_client1:subscribe({ args.topic_s })
+
+local error_message1 = nil
+local error_message2 = nil
+
+while (error_message1 == nil and error_message2 == nil) do
+ error_message1 = mqtt_client1:handler()
+ error_message2 = mqtt_client2:handler()
+ socket.sleep(1.0) -- seconds
+end
+
+if (error_message1 == nil) then
+ mqtt_client1:unsubscribe({ args.topic_s })
+ mqtt_client1:destroy()
+else
+ print(error_message1)
+end
+
+if (error_message2 == nil) then
+ mqtt_client2:destroy()
+else
+ print(error_message2)
+end
+
+-- ------------------------------------------------------------------------- --
diff --git a/lua/example/example_02.lua b/lua/example/example_02.lua
new file mode 100755
index 0000000..575c411
--- /dev/null
+++ b/lua/example/example_02.lua
@@ -0,0 +1,71 @@
+#!/usr/bin/lua
+--
+-- example_02.lua
+-- ~~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- ------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Description
+-- ~~~~~~~~~~~
+-- Publish a sequence of messages to a specified topic.
+-- Used to control some coloured RGB LEDs.
+--
+-- ToDo
+-- ~~~~
+-- - On failure, automatically reconnect to MQTT server.
+-- ------------------------------------------------------------------------- --
+
+function is_openwrt()
+ return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT
+end
+
+-- ------------------------------------------------------------------------- --
+
+if (not is_openwrt()) then require("luarocks.require") end
+local lapp = require("pl.lapp")
+
+local args = lapp [[
+ Subscribe to topic1 and publish all messages on topic2
+ -H,--host (default localhost) MQTT server hostname
+ -i,--id (default example_02) MQTT client identifier
+ -p,--port (default 1883) MQTT server port number
+ -s,--sleep (default 5.0) Sleep time between commands
+ -t,--topic (default test/2) Topic on which to publish
+]]
+
+local MQTT = require("mqtt_library")
+
+local mqtt_client = MQTT.client.create(args.host, args.port)
+
+mqtt_client:connect(args.id)
+
+local error_message = nil
+local index = 1
+local messages = { "c010000", "c000100", "c000001" }
+
+while (error_message == nil) do
+ mqtt_client:publish(args.topic, messages[index]);
+
+ index = index + 1
+ if (index > #messages) then index = 1 end
+
+ socket.sleep(args.sleep) -- seconds
+ error_message = mqtt_client:handler()
+end
+
+if (error_message == nil) then
+ mqtt_client:destroy()
+else
+ print(error_message)
+end
+
+-- ------------------------------------------------------------------------- --
diff --git a/lua/example/mqtt_publish.lua b/lua/example/mqtt_publish.lua
new file mode 100755
index 0000000..d39a1a7
--- /dev/null
+++ b/lua/example/mqtt_publish.lua
@@ -0,0 +1,74 @@
+#!/usr/bin/lua
+--
+-- mqtt_publish.lua
+-- ~~~~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- ------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Description
+-- ~~~~~~~~~~~
+-- Publish an MQTT message on the specified topic with an optional last will.
+--
+-- References
+-- ~~~~~~~~~~
+-- Lapp Framework: Lua command line parsing
+-- http://lua-users.org/wiki/LappFramework
+--
+-- ToDo
+-- ~~~~
+-- None, yet.
+-- ------------------------------------------------------------------------- --
+
+function is_openwrt()
+ return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT
+end
+
+-- ------------------------------------------------------------------------- --
+
+print("[mqtt_publish v0.2 2012-06-01]")
+
+if (not is_openwrt()) then require("luarocks.require") end
+local lapp = require("pl.lapp")
+
+local args = lapp [[
+ Publish a message to a specified MQTT topic
+ -d,--debug Verbose console logging
+ -H,--host (default localhost) MQTT server hostname
+ -i,--id (default mqtt_pub) MQTT client identifier
+ -m,--message (string) Message to be published
+ -p,--port (default 1883) MQTT server port number
+ -t,--topic (string) Topic on which to publish
+ -w,--will_message (default .) Last will and testament message
+ -w,--will_qos (default 0) Last will and testament QOS
+ -w,--will_retain (default 0) Last will and testament retention
+ -w,--will_topic (default .) Last will and testament topic
+]]
+
+local MQTT = require("mqtt_library")
+
+if (args.debug) then MQTT.Utility.set_debug(true) end
+
+local mqtt_client = MQTT.client.create(args.host, args.port)
+
+if (args.will_message == "." or args.will_topic == ".") then
+ mqtt_client:connect(args.id)
+else
+ mqtt_client:connect(
+ args.id, args.will_topic, args.will_qos, args.will_retain, args.will_message
+ )
+end
+
+mqtt_client:publish(args.topic, args.message)
+
+mqtt_client:destroy()
+
+-- ------------------------------------------------------------------------- --
diff --git a/lua/example/mqtt_subscribe.lua b/lua/example/mqtt_subscribe.lua
new file mode 100755
index 0000000..e1c87ae
--- /dev/null
+++ b/lua/example/mqtt_subscribe.lua
@@ -0,0 +1,97 @@
+#!/usr/bin/lua
+--
+-- mqtt_subscribe.lua
+-- ~~~~~~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- ------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Description
+-- ~~~~~~~~~~~
+-- Subscribe to an MQTT topic and display any received messages.
+--
+-- References
+-- ~~~~~~~~~~
+-- Lapp Framework: Lua command line parsing
+-- http://lua-users.org/wiki/LappFramework
+--
+-- ToDo
+-- ~~~~
+-- None, yet.
+-- ------------------------------------------------------------------------- --
+
+function callback(
+ topic, -- string
+ message) -- string
+
+ print("Topic: " .. topic .. ", message: '" .. message .. "'")
+end
+
+-- ------------------------------------------------------------------------- --
+
+function is_openwrt()
+ return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT
+end
+
+-- ------------------------------------------------------------------------- --
+
+print("[mqtt_subscribe v0.2 2012-06-01]")
+
+if (not is_openwrt()) then require("luarocks.require") end
+local lapp = require("pl.lapp")
+
+local args = lapp [[
+ Subscribe to a specified MQTT topic
+ -d,--debug Verbose console logging
+ -H,--host (default localhost) MQTT server hostname
+ -i,--id (default mqtt_sub) MQTT client identifier
+ -k,--keepalive (default 60) Send MQTT PING period (seconds)
+ -p,--port (default 1883) MQTT server port number
+ -t,--topic (string) Subscription topic
+ -w,--will_message (default .) Last will and testament message
+ -w,--will_qos (default 0) Last will and testament QOS
+ -w,--will_retain (default 0) Last will and testament retention
+ -w,--will_topic (default .) Last will and testament topic
+]]
+
+local MQTT = require("mqtt_library")
+
+if (args.debug) then MQTT.Utility.set_debug(true) end
+
+if (args.keepalive) then MQTT.client.KEEP_ALIVE_TIME = args.keepalive end
+
+local mqtt_client = MQTT.client.create(args.host, args.port, callback)
+
+if (args.will_message == "." or args.will_topic == ".") then
+ mqtt_client:connect(args.id)
+else
+ mqtt_client:connect(
+ args.id, args.will_topic, args.will_qos, args.will_retain, args.will_message
+ )
+end
+
+mqtt_client:subscribe({args.topic})
+
+local error_message = nil
+
+while (error_message == nil) do
+ error_message = mqtt_client:handler()
+ socket.sleep(1.0) -- seconds
+end
+
+if (error_message == nil) then
+ mqtt_client:unsubscribe({args.topic})
+ mqtt_client:destroy()
+else
+ print(error_message)
+end
+
+-- ------------------------------------------------------------------------- --
diff --git a/lua/example/mqtt_test.lua b/lua/example/mqtt_test.lua
new file mode 100755
index 0000000..33a6a82
--- /dev/null
+++ b/lua/example/mqtt_test.lua
@@ -0,0 +1,94 @@
+#!/usr/bin/lua
+--
+-- mqtt_test.lua
+-- ~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- ------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Description
+-- ~~~~~~~~~~~
+-- Repetitively publishes MQTT messages on the topic_p,
+-- until the "quit" message is received on the topic_s.
+--
+-- References
+-- ~~~~~~~~~~
+-- Lapp Framework: Lua command line parsing
+-- http://lua-users.org/wiki/LappFramework
+--
+-- ToDo
+-- ~~~~
+-- - On failure, automatically reconnect to MQTT server.
+-- ------------------------------------------------------------------------- --
+
+function callback(
+ topic, -- string
+ payload) -- string
+
+ print("mqtt_test:callback(): " .. topic .. ": " .. payload)
+
+ if (payload == "quit") then running = false end
+end
+
+-- ------------------------------------------------------------------------- --
+
+function is_openwrt()
+ return(os.getenv("USER") == "root") -- Assume logged in as "root" on OpenWRT
+end
+
+-- ------------------------------------------------------------------------- --
+
+print("[mqtt_test v0.2 2012-06-01]")
+
+if (not is_openwrt()) then require("luarocks.require") end
+local lapp = require("pl.lapp")
+
+local args = lapp [[
+ Test Lua MQTT client library
+ -d,--debug Verbose console logging
+ -i,--id (default mqtt_test) MQTT client identifier
+ -p,--port (default 1883) MQTT server port number
+ -s,--topic_s (default test/2) Subscribe topic
+ -t,--topic_p (default test/1) Publish topic
+ <host> (default localhost) MQTT server hostname
+]]
+
+local MQTT = require("mqtt_library")
+
+if (args.debug) then MQTT.Utility.set_debug(true) end
+
+local mqtt_client = MQTT.client.create(args.host, args.port, callback)
+
+mqtt_client:connect(args.id)
+
+mqtt_client:publish(args.topic_p, "*** Lua test start ***")
+mqtt_client:subscribe({ args.topic_s })
+
+local error_message = nil
+local running = true
+
+while (error_message == nil and running) do
+ error_message = mqtt_client:handler()
+
+ if (error_message == nil) then
+ mqtt_client:publish(args.topic_p, "*** Lua test message ***")
+ socket.sleep(1.0) -- seconds
+ end
+end
+
+if (error_message == nil) then
+ mqtt_client:unsubscribe({ args.topic_s })
+ mqtt_client:destroy()
+else
+ print(error_message)
+end
+
+-- ------------------------------------------------------------------------- --
diff --git a/lua/mqtt_library.lua b/lua/mqtt_library.lua
new file mode 100644
index 0000000..1f53c2e
--- /dev/null
+++ b/lua/mqtt_library.lua
@@ -0,0 +1,777 @@
+-- mqtt_library.lua
+-- ~~~~~~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- -------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial API and implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Documentation
+-- ~~~~~~~~~~~~~
+-- MQTT Lua web-site
+-- http://eclipse.org/paho/... -- FILL ME IN !
+--
+-- References
+-- ~~~~~~~~~~
+-- MQTT web-site
+-- http://mqtt.org
+
+-- MQTT protocol specification 3.1
+-- https://www.ibm.com/developerworks/webservices/library/ws-mqtt
+-- http://mqtt.org/wiki/doku.php/mqtt_protocol # Clarifications
+--
+-- Notes
+-- ~~~~~
+-- - Always assumes MQTT connection "clean session" enabled.
+-- - Supports connection last will and testament message.
+-- - Does not support connection username and password.
+-- - Fixed message header byte 1, only implements the "message type".
+-- - Only supports QOS level 0.
+-- - Maximum payload length is 268,435,455 bytes (as per specification).
+-- - Publish message doesn't support "message identifier".
+-- - Subscribe acknowledgement messages don't check granted QOS level.
+-- - Outstanding subscribe acknowledgement messages aren't escalated.
+-- - Works on the Sony PlayStation Portable (aka Sony PSP) ...
+-- See http://en.wikipedia.org/wiki/Lua_Player_HM
+--
+-- ToDo
+-- ~~~~
+-- * Consider when payload needs to be an array of bytes (not characters).
+-- * Maintain both "last_activity_out" and "last_activity_in".
+-- * - http://mqtt.org/wiki/doku.php/keepalive_for_the_client
+-- * Update "last_activity_in" when messages are received.
+-- * When a PINGREQ is sent, must check for a PINGRESP, within KEEP_ALIVE_TIME..
+-- * Otherwise, fail the connection.
+-- * When connecting, wait for CONACK, until KEEP_ALIVE_TIME, before failing.
+-- * Should MQTT.client:connect() be asynchronous with a callback ?
+-- * Review all public APIs for asynchronous callback behaviour.
+-- * Implement parse PUBACK message.
+-- * Handle failed subscriptions, i.e no subscription acknowledgement received.
+-- * Fix problem when KEEP_ALIVE_TIME is short, e.g. mqtt_publish -k 1
+-- MQTT.client:handler(): Message length mismatch
+-- - On socket error, optionally try reconnection to MQTT server.
+-- - Consider use of assert() and pcall() ?
+-- - Only expose public API functions, don't expose internal API functions.
+-- - Refactor "if self.connected()" to "self.checkConnected(error_message)".
+-- - Maintain and publish messaging statistics.
+-- - Memory heap/stack monitoring.
+-- - When debugging, why isn't mosquitto sending back CONACK error code ?
+-- - Subscription callbacks invoked by topic name (including wildcards).
+-- - Implement asynchronous state machine, rather than single-thread waiting.
+-- - After CONNECT, expect and wait for a CONACK.
+-- - Implement complete MQTT broker (server).
+-- - Consider using Copas http://keplerproject.github.com/copas/manual.html
+-- ------------------------------------------------------------------------- --
+
+function isPsp() return(Socket ~= nil) end
+
+if (not isPsp()) then
+ require("socket")
+ require("io")
+ require("ltn12")
+--require("ssl")
+end
+
+local MQTT = {}
+
+MQTT.Utility = require("utility")
+
+MQTT.VERSION = 0x03
+
+MQTT.ERROR_TERMINATE = false -- Message handler errors terminate process ?
+
+MQTT.DEFAULT_BROKER_HOSTNAME = "localhost"
+
+MQTT.client = {}
+MQTT.client.__index = MQTT.client
+
+MQTT.client.DEFAULT_PORT = 1883
+MQTT.client.KEEP_ALIVE_TIME = 60 -- seconds (maximum is 65535)
+MQTT.client.MAX_PAYLOAD_LENGTH = 268435455 -- bytes
+
+-- MQTT 3.1 Specification: Section 2.1: Fixed header, Message type
+
+MQTT.message = {}
+MQTT.message.TYPE_RESERVED = 0x00
+MQTT.message.TYPE_CONNECT = 0x01
+MQTT.message.TYPE_CONACK = 0x02
+MQTT.message.TYPE_PUBLISH = 0x03
+MQTT.message.TYPE_PUBACK = 0x04
+MQTT.message.TYPE_PUBREC = 0x05
+MQTT.message.TYPE_PUBREL = 0x06
+MQTT.message.TYPE_PUBCOMP = 0x07
+MQTT.message.TYPE_SUBSCRIBE = 0x08
+MQTT.message.TYPE_SUBACK = 0x09
+MQTT.message.TYPE_UNSUBSCRIBE = 0x0a
+MQTT.message.TYPE_UNSUBACK = 0x0b
+MQTT.message.TYPE_PINGREQ = 0x0c
+MQTT.message.TYPE_PINGRESP = 0x0d
+MQTT.message.TYPE_DISCONNECT = 0x0e
+MQTT.message.TYPE_RESERVED = 0x0f
+
+-- MQTT 3.1 Specification: Section 3.2: CONACK acknowledge connection errors
+-- http://mqtt.org/wiki/doku.php/extended_connack_codes
+
+MQTT.CONACK = {}
+MQTT.CONACK.error_message = { -- CONACK return code used as the index
+ "Unacceptable protocol version",
+ "Identifer rejected",
+ "Server unavailable",
+ "Bad user name or password",
+ "Not authorized"
+--"Invalid will topic" -- Proposed
+}
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Create an MQTT client instance
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+function MQTT.client.create( -- Public API
+ hostname, -- string: Host name or address of the MQTT broker
+ port, -- integer: Port number of the MQTT broker (default: 1883)
+ callback) -- function: Invoked when subscribed topic messages received
+ -- return: mqtt_client table
+
+ local mqtt_client = {}
+
+ setmetatable(mqtt_client, MQTT.client)
+
+ mqtt_client.callback = callback -- function(topic, payload)
+ mqtt_client.hostname = hostname
+ mqtt_client.port = port or MQTT.client.DEFAULT_PORT
+
+ mqtt_client.connected = false
+ mqtt_client.destroyed = false
+ mqtt_client.last_activity = 0
+ mqtt_client.message_id = 0
+ mqtt_client.outstanding = {}
+ mqtt_client.socket_client = nil
+
+ return(mqtt_client)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit MQTT Client request a connection to an MQTT broker (server)
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.1: CONNECT
+
+function MQTT.client:connect( -- Public API
+ identifier, -- string: MQTT client identifier (maximum 23 characters)
+ will_topic, -- string: Last will and testament topic
+ will_qos, -- byte: Last will and testament Quality Of Service
+ will_retain, -- byte: Last will and testament retention status
+ will_message) -- string: Last will and testament message
+ -- return: nil or error message
+
+ if (self.connected) then
+ return("MQTT.client:connect(): Already connected")
+ end
+
+ MQTT.Utility.debug("MQTT.client:connect(): " .. identifier)
+
+ self.socket_client = socket.connect(self.hostname, self.port)
+
+ if (self.socket_client == nil) then
+ return("MQTT.client:connect(): Couldn't open MQTT broker connection")
+ end
+
+ MQTT.Utility.socket_wait_connected(self.socket_client)
+
+ self.connected = true
+
+-- Construct CONNECT variable header fields (bytes 1 through 9)
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ local payload
+ payload = MQTT.client.encode_utf8("MQIsdp")
+ payload = payload .. string.char(MQTT.VERSION)
+
+-- Connect flags (byte 10)
+-- ~~~~~~~~~~~~~
+-- bit 7: Username flag = 0 -- recommended no more than 12 characters
+-- bit 6: Password flag = 0 -- ditto
+-- bit 5: Will retain = 0
+-- bits 4,3: Will QOS = 00
+-- bit 2: Will flag = 0
+-- bit 1: Clean session = 1
+-- bit 0: Unused = 0
+
+ if (will_topic == nil) then
+ payload = payload .. string.char(0x02) -- Clean session, no last will
+ else
+ local flags
+ flags = MQTT.Utility.shift_left(will_retain, 5)
+ flags = flags + MQTT.Utility.shift_left(will_qos, 3) + 0x06
+ payload = payload .. string.char(flags)
+ end
+
+-- Keep alive timer (bytes 11 LSB and 12 MSB, unit is seconds)
+-- ~~~~~~~~~~~~~~~~~
+ payload = payload .. string.char(math.floor(MQTT.client.KEEP_ALIVE_TIME / 256))
+ payload = payload .. string.char(MQTT.client.KEEP_ALIVE_TIME % 256)
+
+-- Client identifier
+-- ~~~~~~~~~~~~~~~~~
+ payload = payload .. MQTT.client.encode_utf8(identifier)
+
+-- Last will and testament
+-- ~~~~~~~~~~~~~~~~~~~~~~~
+ if (will_topic ~= nil) then
+ payload = payload .. MQTT.client.encode_utf8(will_topic)
+ payload = payload .. MQTT.client.encode_utf8(will_message)
+ end
+
+-- Send MQTT message
+-- ~~~~~~~~~~~~~~~~~
+ return(self:message_write(MQTT.message.TYPE_CONNECT, payload))
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Destroy an MQTT client instance
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+function MQTT.client:destroy() -- Public API
+ MQTT.Utility.debug("MQTT.client:destroy()")
+
+ if (self.destroyed == false) then
+ self.destroyed = true -- Avoid recursion when message_write() fails
+
+ if (self.connected) then self:disconnect() end
+
+ self.callback = nil
+ self.outstanding = nil
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit MQTT Disconnect message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.14: Disconnect notification
+--
+-- bytes 1,2: Fixed message header, see MQTT.client:message_write()
+
+function MQTT.client:disconnect() -- Public API
+ MQTT.Utility.debug("MQTT.client:disconnect()")
+
+ if (self.connected) then
+ self:message_write(MQTT.message.TYPE_DISCONNECT, nil)
+ self.socket_client:close()
+ self.connected = false
+ else
+ error("MQTT.client:disconnect(): Already disconnected")
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Encode a message string using UTF-8 (for variable header)
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 2.5: MQTT and UTF-8
+--
+-- byte 1: String length MSB
+-- byte 2: String length LSB
+-- bytes 3-n: String encoded as UTF-8
+
+function MQTT.client.encode_utf8( -- Internal API
+ input) -- string
+
+ local output
+ output = string.char(math.floor(#input / 256))
+ output = output .. string.char(#input % 256)
+ output = output .. input
+
+ return(output)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Handle received messages and maintain keep-alive PING messages
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- This function must be invoked periodically (more often than the
+-- MQTT.client.KEEP_ALIVE_TIME) which maintains the connection and
+-- services the incoming subscribed topic messages.
+
+function MQTT.client:handler() -- Public API
+ if (self.connected == false) then
+ error("MQTT.client:handler(): Not connected")
+ end
+
+ MQTT.Utility.debug("MQTT.client:handler()")
+
+-- Transmit MQTT PING message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.13: PING request
+--
+-- bytes 1,2: Fixed message header, see MQTT.client:message_write()
+
+ local activity_timeout = self.last_activity + MQTT.client.KEEP_ALIVE_TIME
+
+ if (MQTT.Utility.get_time() > activity_timeout) then
+ MQTT.Utility.debug("MQTT.client:handler(): PINGREQ")
+
+ self:message_write(MQTT.message.TYPE_PINGREQ, nil)
+ end
+
+-- Check for available client socket data
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ local ready = MQTT.Utility.socket_ready(self.socket_client)
+
+ if (ready) then
+ local error_message, buffer =
+ MQTT.Utility.socket_receive(self.socket_client)
+
+ if (error_message ~= nil) then
+ self:destroy()
+ error_message = "socket_client:receive(): " .. error_message
+ MQTT.Utility.debug(error_message)
+ return(error_message)
+ end
+
+ if (buffer ~= nil and #buffer > 0) then
+ local index = 1
+
+ -- Parse individual messages (each must be at least 2 bytes long)
+ -- Decode "remaining length" (MQTT v3.1 specification pages 6 and 7)
+
+ while (index < #buffer) do
+ local message_type_flags = string.byte(buffer, index)
+ local multiplier = 1
+ local remaining_length = 0
+
+ repeat
+ index = index + 1
+ local digit = string.byte(buffer, index)
+ remaining_length = remaining_length + ((digit % 128) * multiplier)
+ multiplier = multiplier * 128
+ until digit < 128 -- check continuation bit
+
+ local message = string.sub(buffer, index + 1, index + remaining_length)
+
+ if (#message == remaining_length) then
+ self:parse_message(message_type_flags, remaining_length, message)
+ else
+ MQTT.Utility.debug(
+ "MQTT.client:handler(): Incorrect remaining length: " ..
+ remaining_length .. " ~= message length: " .. #message
+ )
+ end
+
+ index = index + remaining_length + 1
+ end
+
+ -- Check for any left over bytes, i.e. partial message received
+
+ if (index ~= (#buffer + 1)) then
+ local error_message =
+ "MQTT.client:handler(): Partial message received" ..
+ index .. " ~= " .. (#buffer + 1)
+
+ if (MQTT.ERROR_TERMINATE) then -- TODO: Refactor duplicate code
+ self:destroy()
+ error(error_message)
+ else
+ MQTT.Utility.debug(error_message)
+ end
+ end
+ end
+ end
+
+ return(nil)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit an MQTT message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 2.1: Fixed header
+--
+-- byte 1: Message type and flags (DUP, QOS level, and Retain) fields
+-- bytes 2-5: Remaining length field (between one and four bytes long)
+-- bytes m- : Optional variable header and payload
+
+function MQTT.client:message_write( -- Internal API
+ message_type, -- enumeration
+ payload) -- string
+ -- return: nil or error message
+
+-- TODO: Complete implementation of fixed header byte 1
+
+ local message = string.char(MQTT.Utility.shift_left(message_type, 4))
+
+ if (payload == nil) then
+ message = message .. string.char(0) -- Zero length, no payload
+ else
+ if (#payload > MQTT.client.MAX_PAYLOAD_LENGTH) then
+ return(
+ "MQTT.client:message_write(): Payload length = " .. #payload ..
+ " exceeds maximum of " .. MQTT.client.MAX_PAYLOAD_LENGTH
+ )
+ end
+
+ -- Encode "remaining length" (MQTT v3.1 specification pages 6 and 7)
+
+ local remaining_length = #payload
+
+ repeat
+ local digit = remaining_length % 128
+ remaining_length = math.floor(remaining_length / 128)
+ if (remaining_length > 0) then digit = digit + 128 end -- continuation bit
+ message = message .. string.char(digit)
+ until remaining_length == 0
+
+ message = message .. payload
+ end
+
+ local status, error_message = self.socket_client:send(message)
+
+ if (status == nil) then
+ self:destroy()
+ return("MQTT.client:message_write(): " .. error_message)
+ end
+
+ self.last_activity = MQTT.Utility.get_time()
+ return(nil)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Parse MQTT message
+-- ~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 2.1: Fixed header
+--
+-- byte 1: Message type and flags (DUP, QOS level, and Retain) fields
+-- bytes 2-5: Remaining length field (between one and four bytes long)
+-- bytes m- : Optional variable header and payload
+--
+-- The message type/flags and remaining length are already parsed and
+-- removed from the message by the time this function is invoked.
+-- Leaving just the optional variable header and payload.
+
+function MQTT.client:parse_message( -- Internal API
+ message_type_flags, -- byte
+ remaining_length, -- integer
+ message) -- string: Optional variable header and payload
+
+ local message_type = MQTT.Utility.shift_right(message_type_flags, 4)
+
+-- TODO: MQTT.message.TYPE table should include "parser handler" function.
+-- This would nicely collapse the if .. then .. elseif .. end.
+
+ if (message_type == MQTT.message.TYPE_CONACK) then
+ self:parse_message_conack(message_type_flags, remaining_length, message)
+
+ elseif (message_type == MQTT.message.TYPE_PUBLISH) then
+ self:parse_message_publish(message_type_flags, remaining_length, message)
+
+ elseif (message_type == MQTT.message.TYPE_PUBACK) then
+ print("MQTT.client:parse_message(): PUBACK -- UNIMPLEMENTED --") -- TODO
+
+ elseif (message_type == MQTT.message.TYPE_SUBACK) then
+ self:parse_message_suback(message_type_flags, remaining_length, message)
+
+ elseif (message_type == MQTT.message.TYPE_UNSUBACK) then
+ self:parse_message_unsuback(message_type_flags, remaining_length, message)
+
+ elseif (message_type == MQTT.message.TYPE_PINGREQ) then
+ self:ping_response()
+
+ elseif (message_type == MQTT.message.TYPE_PINGRESP) then
+ self:parse_message_pingresp(message_type_flags, remaining_length, message)
+
+ else
+ local error_message =
+ "MQTT.client:parse_message(): Unknown message type: " .. message_type
+
+ if (MQTT.ERROR_TERMINATE) then -- TODO: Refactor duplicate code
+ self:destroy()
+ error(error_message)
+ else
+ MQTT.Utility.debug(error_message)
+ end
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Parse MQTT CONACK message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.2: CONACK Acknowledge connection
+--
+-- byte 1: Reserved value
+-- byte 2: Connect return code, see MQTT.CONACK.error_message[]
+
+function MQTT.client:parse_message_conack( -- Internal API
+ message_type_flags, -- byte
+ remaining_length, -- integer
+ message) -- string
+
+ local me = "MQTT.client:parse_message_conack()"
+ MQTT.Utility.debug(me)
+
+ if (remaining_length ~= 2) then
+ error(me .. ": Invalid remaining length")
+ end
+
+ local return_code = string.byte(message, 2)
+
+ if (return_code ~= 0) then
+ local error_message = "Unknown return code"
+
+ if (return_code <= table.getn(MQTT.CONACK.error_message)) then
+ error_message = MQTT.CONACK.error_message[return_code]
+ end
+
+ error(me .. ": Connection refused: " .. error_message)
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Parse MQTT PINGRESP message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.13: PING response
+
+function MQTT.client:parse_message_pingresp( -- Internal API
+ message_type_flags, -- byte
+ remaining_length, -- integer
+ message) -- string
+
+ local me = "MQTT.client:parse_message_pingresp()"
+ MQTT.Utility.debug(me)
+
+ if (remaining_length ~= 0) then
+ error(me .. ": Invalid remaining length")
+ end
+
+-- ToDo: self.ping_response_outstanding = false
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Parse MQTT PUBLISH message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.3: Publish message
+--
+-- Variable header ..
+-- bytes 1- : Topic name and optional Message Identifier (if QOS > 0)
+-- bytes m- : Payload
+
+function MQTT.client:parse_message_publish( -- Internal API
+ message_type_flags, -- byte
+ remaining_length, -- integer
+ message) -- string
+
+ local me = "MQTT.client:parse_message_publish()"
+ MQTT.Utility.debug(me)
+
+ if (self.callback ~= nil) then
+ if (remaining_length < 3) then
+ error(me .. ": Invalid remaining length: " .. remaining_length)
+ end
+
+ local topic_length = string.byte(message, 1) * 256
+ topic_length = topic_length + string.byte(message, 2)
+ local topic = string.sub(message, 3, topic_length + 2)
+ local index = topic_length + 3
+
+-- Handle optional Message Identifier, for QOS levels 1 and 2
+-- TODO: Enable Subscribe with QOS and deal with PUBACK, etc.
+
+ local qos = MQTT.Utility.shift_right(message_type_flags, 1) % 3
+
+ if (qos > 0) then
+ local message_id = string.byte(message, index) * 256
+ message_id = message_id + string.byte(message, index + 1)
+ index = index + 2
+ end
+
+ local payload_length = remaining_length - index + 1
+ local payload = string.sub(message, index, index + payload_length - 1)
+
+ self.callback(topic, payload)
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Parse MQTT SUBACK message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.9: SUBACK Subscription acknowledgement
+--
+-- bytes 1,2: Message Identifier
+-- bytes 3- : List of granted QOS for each subscribed topic
+
+function MQTT.client:parse_message_suback( -- Internal API
+ message_type_flags, -- byte
+ remaining_length, -- integer
+ message) -- string
+
+ local me = "MQTT.client:parse_message_suback()"
+ MQTT.Utility.debug(me)
+
+ if (remaining_length < 3) then
+ error(me .. ": Invalid remaining length: " .. remaining_length)
+ end
+
+ local message_id = string.byte(message, 1) * 256 + string.byte(message, 2)
+ local outstanding = self.outstanding[message_id]
+
+ if (outstanding == nil) then
+ error(me .. ": No outstanding message: " .. message_id)
+ end
+
+ self.outstanding[message_id] = nil
+
+ if (outstanding[1] ~= "subscribe") then
+ error(me .. ": Outstanding message wasn't SUBSCRIBE")
+ end
+
+ local topic_count = table.getn(outstanding[2])
+
+ if (topic_count ~= remaining_length - 2) then
+ error(me .. ": Didn't received expected number of topics: " .. topic_count)
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Parse MQTT UNSUBACK message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.11: UNSUBACK Unsubscription acknowledgement
+--
+-- bytes 1,2: Message Identifier
+
+function MQTT.client:parse_message_unsuback( -- Internal API
+ message_type_flags, -- byte
+ remaining_length, -- integer
+ message) -- string
+
+ local me = "MQTT.client:parse_message_unsuback()"
+ MQTT.Utility.debug(me)
+
+ if (remaining_length ~= 2) then
+ error(me .. ": Invalid remaining length")
+ end
+
+ local message_id = string.byte(message, 1) * 256 + string.byte(message, 2)
+
+ local outstanding = self.outstanding[message_id]
+
+ if (outstanding == nil) then
+ error(me .. ": No outstanding message: " .. message_id)
+ end
+
+ self.outstanding[message_id] = nil
+
+ if (outstanding[1] ~= "unsubscribe") then
+ error(me .. ": Outstanding message wasn't UNSUBSCRIBE")
+ end
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit MQTT Ping response message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.13: PING response
+
+function MQTT.client:ping_response() -- Internal API
+ MQTT.Utility.debug("MQTT.client:ping_response()")
+
+ if (self.connected == false) then
+ error("MQTT.client:ping_response(): Not connected")
+ end
+
+ self:message_write(MQTT.message.TYPE_PINGRESP, nil)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit MQTT Publish message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.3: Publish message
+--
+-- bytes 1,2: Fixed message header, see MQTT.client:message_write()
+-- Variable header ..
+-- bytes 3- : Topic name and optional Message Identifier (if QOS > 0)
+-- bytes m- : Payload
+
+function MQTT.client:publish( -- Public API
+ topic, -- string
+ payload) -- string
+
+ if (self.connected == false) then
+ error("MQTT.client:publish(): Not connected")
+ end
+
+ MQTT.Utility.debug("MQTT.client:publish(): " .. topic)
+
+ local message = MQTT.client.encode_utf8(topic) .. payload
+
+ self:message_write(MQTT.message.TYPE_PUBLISH, message)
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit MQTT Subscribe message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.8: Subscribe to named topics
+--
+-- bytes 1,2: Fixed message header, see MQTT.client:message_write()
+-- Variable header ..
+-- bytes 3,4: Message Identifier
+-- bytes 5- : List of topic names and their QOS level
+
+function MQTT.client:subscribe( -- Public API
+ topics) -- table of strings
+
+ if (self.connected == false) then
+ error("MQTT.client:subscribe(): Not connected")
+ end
+
+ self.message_id = self.message_id + 1
+
+ local message
+ message = string.char(math.floor(self.message_id / 256))
+ message = message .. string.char(self.message_id % 256)
+
+ for index, topic in ipairs(topics) do
+ MQTT.Utility.debug("MQTT.client:subscribe(): " .. topic)
+ message = message .. MQTT.client.encode_utf8(topic)
+ message = message .. string.char(0) -- QOS level 0
+ end
+
+ self:message_write(MQTT.message.TYPE_SUBSCRIBE, message)
+
+ self.outstanding[self.message_id] = { "subscribe", topics }
+end
+
+-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --
+-- Transmit MQTT Unsubscribe message
+-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+-- MQTT 3.1 Specification: Section 3.10: Unsubscribe from named topics
+--
+-- bytes 1,2: Fixed message header, see MQTT.client:message_write()
+-- Variable header ..
+-- bytes 3,4: Message Identifier
+-- bytes 5- : List of topic names
+
+
+function MQTT.client:unsubscribe( -- Public API
+ topics) -- table of strings
+
+ if (self.connected == false) then
+ error("MQTT.client:unsubscribe(): Not connected")
+ end
+
+ self.message_id = self.message_id + 1
+
+ local message
+ message = string.char(math.floor(self.message_id / 256))
+ message = message .. string.char(self.message_id % 256)
+
+ for index, topic in ipairs(topics) do
+ MQTT.Utility.debug("MQTT.client:unsubscribe(): " .. topic)
+ message = message .. MQTT.client.encode_utf8(topic)
+ end
+
+ self:message_write(MQTT.message.TYPE_UNSUBSCRIBE, message)
+
+ self.outstanding[self.message_id] = { "unsubscribe", topics }
+end
+
+-- For ... MQTT = require("mqtt_library")
+
+return(MQTT)
diff --git a/lua/utility.lua b/lua/utility.lua
new file mode 100644
index 0000000..385cb5a
--- /dev/null
+++ b/lua/utility.lua
@@ -0,0 +1,170 @@
+-- utility.lua
+-- ~~~~~~~~~~~
+-- Version: 0.2 2012-06-01
+-- -------------------------------------------------------------------------- --
+-- Copyright (c) 2011-2012 Geekscape Pty. Ltd.
+-- All rights reserved. This program and the accompanying materials
+-- are made available under the terms of the Eclipse Public License v1.0
+-- which accompanies this distribution, and is available at
+-- http://www.eclipse.org/legal/epl-v10.html
+--
+-- Contributors:
+-- Andy Gelme - Initial API and implementation
+-- -------------------------------------------------------------------------- --
+--
+-- Notes
+-- ~~~~~
+-- - Works on the Sony PlayStation Portable (aka Sony PSP) ...
+-- See http://en.wikipedia.org/wiki/Lua_Player_HM
+--
+-- ToDo
+-- ~~~~
+-- - shift_left() should mask bits past the 8, 16, 32 and 64-bit boundaries.
+-- ------------------------------------------------------------------------- --
+
+local function isPsp() return(Socket ~= nil) end
+
+if (isPsp()) then socket = Socket end -- Compatibility !
+
+-- ------------------------------------------------------------------------- --
+
+local debug_flag = false
+
+local function set_debug(value) debug_flag = value end
+
+local function debug(message)
+ if (debug_flag) then print(message) end
+end
+
+-- ------------------------------------------------------------------------- --
+
+local function dump_string(value)
+ local index
+
+ for index = 1, string.len(value) do
+ print(string.format("%d: %02x", index, string.byte(value, index)))
+ end
+end
+
+-- ------------------------------------------------------------------------- --
+
+local timer
+
+if (isPsp()) then
+ timer = Timer.new()
+ timer:start()
+end
+
+local function get_time()
+ if (isPsp()) then
+ return(timer:time() / 1000)
+ else
+ return(socket.gettime())
+ end
+end
+
+local function expired(last_time, duration, type)
+ local time_expired = get_time() >= (last_time + duration)
+
+ if (time_expired) then debug("Event: " .. type) end
+ return(time_expired)
+end
+
+-- ------------------------------------------------------------------------- --
+
+local function shift_left(value, shift)
+ return(value * 2 ^ shift)
+end
+
+local function shift_right(value, shift)
+ return(math.floor(value / 2 ^ shift))
+end
+
+-- ------------------------------------------------------------------------- --
+
+local function socket_ready(socket_client)
+ local ready, read_sockets, write_sockets, error_state = true, nil, nil, nil
+
+ if (not isPsp()) then
+ read_sockets, write_sockets, error_state =
+ socket.select({socket_client}, nil, 0.001)
+
+ if (#read_sockets == 0) then ready = false end
+ end
+
+ return(ready)
+end
+
+local function socket_receive(socket_client, byte_count)
+ local response, buffer, error_message = nil, nil, nil
+
+ byte_count = byte_count or 128 -- default
+
+ if (isPsp()) then
+ buffer = socket_client:recv(byte_count)
+ else
+ response, error_message, buffer = socket_client:receive("*a")
+
+ if (error_message == "timeout") then error_message = nil end
+ end
+
+ return(error_message), (buffer) -- nil or "closed"
+end
+
+local function socket_wait_connected(socket_client)
+ if (isPsp()) then
+ while (socket_client:isConnected() == false) do
+ System.sleep(100)
+ end
+ else
+ socket_client:settimeout(0.001) -- So that socket.recieve doesn't block
+ end
+end
+
+-- ------------------------------------------------------------------------- --
+
+local function table_to_string(table)
+ local result = ''
+
+ if (type(table) == 'table') then
+ result = '{ '
+
+ for index = 1, #table do
+ result = result .. table_to_string(table[index])
+ if (index ~= #table) then
+ result = result .. ', '
+ end
+ end
+
+ result = result .. ' }'
+ else
+ result = tostring(table)
+ end
+
+ return(result)
+end
+
+-- ------------------------------------------------------------------------- --
+-- Define Utility "module"
+-- ~~~~~~~~~~~~~~~~~~~~~~~
+
+local Utility = {}
+
+Utility.isPsp = isPsp
+Utility.set_debug = set_debug
+Utility.debug = debug
+Utility.dump_string = dump_string
+Utility.get_time = get_time
+Utility.expired = expired
+Utility.shift_left = shift_left
+Utility.shift_right = shift_right
+Utility.socket_ready = socket_ready
+Utility.socket_receive = socket_receive
+Utility.socket_wait_connected = socket_wait_connected
+Utility.table_to_string = table_to_string
+
+-- For ... Utility = require("utility")
+
+return(Utility)
+
+-- ------------------------------------------------------------------------- --
diff --git a/readme.markdown b/readme.markdown
new file mode 100644
index 0000000..d8de103
--- /dev/null
+++ b/readme.markdown
@@ -0,0 +1,346 @@
+Lua MQTT client library (version 0.2 2012-06-01)
+=======================
+
+This project is part of the
+[Eclipse Paho project](http://eclipse.org/paho)
+
+Contents
+--------
+- [Introduction](#introduction)
+- [Protocol implementation and restrictions](#restrictions)
+- [Download](#download)
+- [Feedback and issues](#feedback)
+- [Installation](#installation)
+- [Usage](#usage)
+- [Example code](#example)
+- [Library API](#api)
+- [Known problems](#problems)
+
+<a name="introduction" />
+Introduction
+------------
+This project provides a client-side (only) implementation of the
+[MQTT protocol](http://mqtt.org),
+plus command-line utilities for publishing and subscribing to MQTT topics.
+Typically, one or more MQTT servers, such as
+[mosquitto](http://mosquitto.org) or
+[rsmb](http://www.alphaworks.ibm.com/tech/rsmb)
+will be running on host systems, with which the Lua MQTT client can interact.
+
+MQTT stands for "Message Queue Telemetry Transport", a protocol authored by
+[Dr. Andy Stanford-Clark](http://wikipedia.org/wiki/Andy_Stanford-Clark)
+and Arlen Nipper.
+The protocol is a message-based, publish/subscribe transport layer,
+which is optimized for simple telemetry applications running on small
+micro-controllers, such as an [Arduino](http://arduino.cc),
+over low-bandwidth connections.
+[MQTT libraries exist](http://mqtt.org/software)
+for most popular programming languages, so you can utilize MQTT
+on whatever server or device that you require.
+
+The Lua MQTT client library implements the client-side subset of the
+[MQTT protocol specification 3.1](https://www.ibm.com/developerworks/webservices/library/ws-mqtt).
+
+![Lua MQTT overview](https://github.com/geekscape/mqtt_lua/raw/master/images/lua_mqtt_overview.jpg)
+
+A good use-case for this library is running on constrained systems, such as
+[OpenWRT](http://openwrt.org),
+and acting as a gateway between non-MQTT clients and MQTT servers.
+An advantage of using Lua is that only a text editor is required for rapid
+development of simple MQTT client applications on platforms such as OpenWRT.
+In constrast, working with the C programming language would comparatively
+require more effort, due to requiring a cross-platform development environment.
+
+The Lua MQTT client library also runs (unmodified) on a Sony PlayStation
+Portable using the
+[Lua Player HM](http://en.wikipedia.org/wiki/Lua_Player_HM)
+_(which requires your PSP to be able to run unsigned executables)._
+
+![PlayStation Portable](https://github.com/geekscape/mqtt_lua/raw/master/images/playstation_portable.jpg)
+
+<a name="restrictions" />
+Protocol implementation and restrictions
+----------------------------------------
+- Always assumes MQTT connection "clean session" enabled.
+- Supports connection last will and testament message.
+- Does not support connection username and password.
+- Fixed message header byte 1, only implements the "message type".
+- Only supports QOS (Quality Of Service) level 0.
+- Maximum payload length is 127 bytes (easily increased).
+- Publish message doesn't support "message identifier".
+- Subscribe acknowledgement messages don't check granted QOS level.
+- Outstanding subscribe acknowledgement messages aren't escalated.
+- Works on the Sony PlayStation Portable, using
+ [Lua Player HM](http://en.wikipedia.org/wiki/Lua_Player_HM).
+
+<a name="download" />
+Download
+--------
+The Lua MQTT client library is cross-platform and should work on any
+platform that supports the Lua programming language and network sockets.
+
+- [Download Lua MQTT client library](https://github.com/geekscape/mqtt_lua/archives/master)
+
+<a name="feedback" />
+Feedback and issues
+-------------------
+Tracking is managed via GitHub ...
+
+- [Enhancements requests and issue tracking](https://github.com/geekscape/mqtt_lua/issues)
+
+<a name="installation" />
+Installation
+------------
+You may choose to install an MQTT server either on the same or a different
+system from the Lua MQTT client library, depending upon your deployment
+scenario.
+
+Prerequisites ...
+
+- Install [Mosquitto MQTT server](http://mosquitto.org/download)
+or any other MQTT server
+- Install [Lua programming language](http://www.lua.org/download.html)
+- Install [LuaRocks package manager](http://luarocks.org/en/Download)
+- Install [LuaSocket](http://w3.impa.br/~diego/software/luasocket)
+- Install [PenLight](https://github.com/stevedonovan/Penlight)
+
+On Linux, Lua and LuaRocks can be installed via your Linux distribution
+package manager.
+On Mac OS X, Lua and LuaRocks can be installed viarDarwin ports.
+After that, LuaSocket and PenLight can be installed via LuaRocks.
+
+Lua MQTT client library (source code) from GitHub ...
+
+* TODO
+
+<a name="usage" />
+Usage
+-----
+The Lua MQTT client library comes with three command line utilites,
+which are useful for testing the library and acting as example code.
+These utilities require that Lua Penlight has been installed.
+
+#### mqtt\_test: Test publish and receive messages on different topics
+
+This command periodically publishes a message on topic "test/1" and
+subscribes to the topic "test/2". The command exits when the message
+"quit" is published on topic "test/2".
+
+ cd $(LUA_MQTT_LIB) // where Lua MQTT library is installed
+ example/mqtt_test -d localhost // Assume MQTT server is on "localhost"
+
+ -d,--debug Verbose console logging
+ -i,--id (default MQTT test) MQTT client identifier
+ -p,--port (default 1883) MQTT server port number
+ <host> (default localhost) MQTT server hostname
+
+#### mqtt\_publish: Publish a single message to a specified topic
+
+This command publishes a single message and then exits.
+
+ example/mqtt_publish -d -t test/1 -m "Test message"
+
+Only the _--topic_ and _--message_ parameters are required.
+
+ -d,--debug Verbose console logging
+ -H,--host (default localhost) MQTT server hostname
+ -i,--id (default MQTT client) MQTT client identifier
+ -m,--message (string) Message to be published
+ -p,--port (default 1883) MQTT server port number
+ -t,--topic (string) Topic on which to publish
+ -w,--will_message Last will and testament message
+ -w,--will_qos (default 0) Last will and testament QOS
+ -w,--will_retain (default 0) Last will and testament retention
+ -w,--will_topic Last will and testament topic
+
+#### mqtt\_subscribe: Subscribe to a topic
+
+This command subscribes to a topic and listens indefinitely for messages.
+Use ^C (or similar) to stop execution.
+
+ example/mqtt_subscribe -d -t test/1
+
+Only the _--topic_ parameter is required.
+
+ -d,--debug Verbose console logging
+ -H,--host (default localhost) MQTT server hostname
+ -i,--id (default MQTT client) MQTT client identifier
+ -k,--keepalive (default 60) Send MQTT PING period (seconds)
+ -p,--port (default 1883) MQTT server port number
+ -t,--topic (string) Subscription topic
+ -w,--will_message Last will and testament message
+ -w,--will_qos (default 0) Last will and testament QOS
+ -w,--will_retain (default 0) Last will and testament retention
+ -w,--will_topic Last will and testament topic
+
+<a name="example" />
+Example code
+------------
+The complete functioning code can be viewed here ...
+[mqtt_lua/lua/example/mqtt\_test.lua](https://github.com/geekscape/mqtt_lua/blob/master/lua/example/mqtt_test.lua)
+
+ -- Define a function which is called by mqtt_client:handler(),
+ -- whenever messages are received on the subscribed topics
+
+ function callback(topic, message)
+ print("Received: " .. topic .. ": " .. message)
+ if (message == "quit") then running = false end
+ end
+
+ -- Create an MQTT client instance, connect to the MQTT server and
+ -- subscribe to the topic called "test/2"
+
+ MQTT = require("mqtt_library")
+ MQTT.Utility.set_debug(true)
+ mqtt_client = MQTT.client.create("localhost", nil, callback)
+ mqtt_client:connect("lua mqtt client"))
+ mqtt_client:subscribe({"test/2"})
+
+ -- Continously invoke mqtt_client:handler() to process the MQTT protocol and
+ -- handle any received messages. Also, publish a message on topic "test/1"
+
+ running = true
+
+ while (running) do
+ mqtt_client:handler()
+ mqtt_client:publish("test/1", "test message")
+ socket.sleep(1.0) -- seconds
+ end
+
+ -- Clean-up by unsubscribing from the topic and closing the MQTT connection
+
+ mqtt_client:unsubscribe({"test/2"})
+ mqtt_client:destroy()
+
+There are also a number of Lua MQTT client examples in the _example/_ directory.
+They can be run from the _lua/_ parent directory, as follow ...
+
+ cd mqtt_client/lua
+ example/example_00.lua
+
+<a name="api" />
+MQTT client Library API
+-----------------------
+Once the MQTT client library has been included (via _require_), one or more
+MQTT server connections can be created. Using a server connection, the client
+may then publish messages directly on a specified topic. Or, subscribe to one
+or more topics, where received messages are passed to a callback function
+(defined when creating an MQTT client instance). Finally, the client can
+unsubscribe from one or more topics and disconnect from the MQTT server.
+
+Use the Lua _require_ statement to load the MQTT client library ...
+
+ MQTT = require("mqtt_library")
+
+#### MQTT.Utility.set_debug(): Library debug console logging
+
+The following statement enables debug console logging for diagnosis.
+
+ MQTT.Utility.set_debug(true)
+
+#### MQTT.client.create(): Create an MQTT client instance
+
+Create an MQTT client that will be connected to the specified host.
+
+ mqtt_client = MQTT.client.create(hostname, port, callback)
+
+The _hostname_ must be provided, but both the _port_ and _callback function_
+parameters are optional. This function returns an MQTT client instance
+that must be used for all subsequent MQTT operations for that server connection.
+
+ hostname string: Host name or address of the MQTT broker
+ port integer: Port number of the MQTT broker (default: 1883)
+ callback function: Invoked when subscribed topic messages received
+
+The _callback function_ is defined as follows ...
+
+ function callback(topic, payload)
+ -- application specific code
+ end
+
+ topic -- string: Topic for the received message
+ payload -- string: Message data
+
+#### MQTT.client:destroy(): Destroy an MQTT client instance
+
+When finished with a server connection, this statement cleans-up all resources
+allocated by the client.
+
+ mqtt_client:destroy()
+
+#### MQTT.client:connect(): Make a connection to an MQTT server
+
+Before messages can be transmitted, the MQTT client must connect to the server.
+
+ mqtt_client:connect(identifier)
+
+Each individual client connection must use a unique identifier.
+Only the _identifier_ parameter is required, the remaining parameters
+are optional.
+
+ mqtt_client:connect(identifier, will_topic, will_qos, will_retain, will_message)
+
+MQTT also provides a "last will and testament" for clients, which is a message
+automatically sent by the server on behalf of the client, should the connection
+fail.
+
+ identifier -- string: MQTT client identifier (maximum 23 characters)
+ will_topic -- string: Last will and testament topic
+ will_qos -- byte: Last will and testament Quality Of Service
+ will_retain -- byte: Last will and testament retention status
+ will_message -- string: Last will and testament message
+
+#### MQTT.client:disconnect(): Transmit MQTT Disconnect message
+
+Transmit an MQTT disconnect message to the server.
+
+ mqtt_client:disconnect()
+
+#### MQTT.client:publish(): Transmit MQTT publish message
+
+Transmit a message on a specified topic.
+
+ mqtt_client:publish(topic, payload)
+
+ topic -- string: Topic for the published message
+ payload -- string: Message data
+
+#### MQTT.client:subscribe(): Transmit MQTT Subscribe message
+
+Subscribe to one or more topics. Whenever a message is published to one of
+those topics, the callback function (defined above) will be invoked.
+
+ mqtt_client:subscribe(topics)
+
+ topics -- table of strings, e.g. { "topic1", "topic2" }
+
+#### MQTT.client:handler(): Handle received messages, maintain keep-alive messages
+
+The _handler()_ function must be called periodically to service incoming
+messages and to ensure that keep-alive messages (PING) are being sent
+when required.
+
+The default _KEEP\_ALIVE\_TIME_ is 60 seconds, therefore _handler()_ must be
+invoked more often than once per minute.
+
+Should any messages be received on the subscribed topics, then _handler()_
+will invoke the callback function (defined above).
+
+ mqtt_client:handler()
+
+#### MQTT.client:unsubscribe(): Transmit MQTT Unsubscribe message
+
+Unsubscribe from one or more topics, so that messages published to those
+topics are no longer received.
+
+ topics -- table of strings, e.g. { "topic1", "topic2" }
+
+<a name="problems" />
+Known problems
+--------------
+- Occasional "MQTT.client:handler(): Message length mismatch" errors,
+ particularly when subscribed topics are transmitting many messages.
+
+- Not really a problem, but if you find that the MQTT socket connection is
+ being closed for no apparent reason, particularly for subscribers ...
+ then check that all MQTT clients are using a unique client identifier.