Initial commit

This commit is contained in:
Sebastiaan de Schaetzen 2024-07-10 21:51:06 +02:00
commit 43a8ebf800
20 changed files with 4012 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.zip
.vscode

View File

@ -0,0 +1,3 @@
function love.draw2()
love.graphics.setBackgroundColor(0, 1, 0)
end

5
controller-host/conf.lua Normal file
View File

@ -0,0 +1,5 @@
function love.conf(t)
t.version = "11.4"
t.window.title = "Spider Controller"
t.window.resizable = true
end

130
controller-host/main.lua Normal file
View File

@ -0,0 +1,130 @@
love.mqtt = {}
local mqttEventChannel
local mqttCommandChannel
local errorMessage = nil
local oldPrint = print
function print(...)
local string = string.format(...)
love.mqtt.send("controller/stdout", string)
oldPrint(...)
end
function printTable(table, indentation)
indentation = indentation or ""
for name, value in pairs(table) do
print(indentation .. tostring(name) .. ": " .. tostring(value))
end
end
function love.draw(...)
if love.draw2 then
love.draw2(...)
else
local text = "Awaiting payload..."
local font = love.graphics.getFont()
if errorMessage then
text = errorMessage
end
-- Calculate the center of the screen
local centerX = love.graphics.getWidth() / 2
local centerY = love.graphics.getHeight() / 2
-- Calculate textX and textY
local textX = math.floor(centerX - (font:getWidth(text) / 2))
local textY = math.floor(centerY - (font:getHeight(text) / 2))
local realText
if errorMessage then
realText = errorMessage
else
realText = "Awaiting payload" .. ("."):rep(math.floor(love.timer.getTime() * 4 % 4))
end
-- Render text
if errorMessage then
love.graphics.setBackgroundColor(0.2, 0, 0, 0)
else
love.graphics.setBackgroundColor(0, 0, 0.2, 0)
end
love.graphics.print(realText, textX, textY)
end
end
function love.update(...)
local message = mqttEventChannel:pop()
if message then
love.mqtt[message.target](unpack(message.args))
end
if love.update2 then
love.update2(...)
end
end
function love.mqtt.connect(connack)
if connack.rc ~= 0 then
print("Connection to broker failed:", connack:reason_string())
end
love.mqtt.subscribe("controller/payload")
print("Connected to MQTT")
printTable(connack)
end
function love.mqtt.message2(topic, payload)
if topic == "controller/payload" then
local success = love.filesystem.unmount("client.zip")
if not success then
print("Could not unmount client.zip")
end
local archive = love.filesystem.newFileData(payload, "client.zip")
local success = love.filesystem.mount(archive, "client", true)
if not success then
print("Failed to mount archive")
return
end
print("Archive mounted")
local chunk, errormsg = love.filesystem.load("client/main.lua", "bt")
if errormsg then
print(errormsg)
errorMessage = errormsg
return
end
love.load = nil
chunk()
if love.load then
love.load()
end
elseif love.mqtt.message then
love.mqtt.message(topic, payload)
end
end
function love.mqtt.send(topic, arg)
mqttCommandChannel:push {
command = "send",
topic = topic,
arg = arg
}
end
function love.mqtt.subscribe(topic)
mqttCommandChannel:push {
command = "subscribe",
topic = topic,
}
end
function love.load()
local requirePaths = love.filesystem.getRequirePath()
love.filesystem.setRequirePath(requirePaths .. ";client/?.lua;client/?/init.lua")
local mqttThread = love.thread.newThread("mqttthread.lua")
mqttThread:start()
mqttEventChannel = love.thread.getChannel("mqtt_event")
mqttCommandChannel = love.thread.getChannel("mqtt_command")
end

View File

@ -0,0 +1,18 @@
-- implementing some functions from BitOp (http://bitop.luajit.org/) on Lua 5.3
return {
lshift = function(x, n)
return x << n
end,
rshift = function(x, n)
return x >> n
end,
bor = function(x1, x2)
return x1 | x2
end,
band = function(x1, x2)
return x1 & x2
end,
}
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,11 @@
-- wrapper around BitOp module
if _VERSION == "Lua 5.1" or type(jit) == "table" then -- Lua 5.1 or LuaJIT (based on Lua 5.1)
return require("bit") -- custom module https://luarocks.org/modules/luarocks/luabitop
elseif _VERSION == "Lua 5.2" then
return require("bit32") -- standard Lua 5.2 module
else
return require("mqtt.bit53")
end
-- vim: ts=4 sts=4 sw=4 noet ft=lua

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
--- MQTT module
-- @module mqtt
--[[
MQTT protocol DOC: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
CONVENTIONS:
* errors:
* passing invalid arguments (like number instead of string) to function in this library will raise exception
* all other errors will be returned in format: false, "error-text"
* you can wrap function call into standard lua assert() to raise exception
]]
--- Module table
-- @field v311 MQTT v3.1.1 protocol version constant
-- @field v50 MQTT v5.0 protocol version constant
-- @field _VERSION luamqtt version string
-- @table mqtt
local mqtt = {
-- supported MQTT protocol versions
v311 = 4, -- supported protocol version, MQTT v3.1.1
v50 = 5, -- supported protocol version, MQTT v5.0
-- mqtt library version
_VERSION = "3.4.3",
}
-- load required stuff
local type = type
local select = select
local require = require
local client = require("mqtt.client")
local client_create = client.create
local ioloop_get = require("mqtt.ioloop").get
--- Create new MQTT client instance
-- @param ... Same as for mqtt.client.create(...)
-- @see mqtt.client.client_mt:__init
function mqtt.client(...)
return client_create(...)
end
--- Returns default ioloop instance
-- @function mqtt.get_ioloop
mqtt.get_ioloop = ioloop_get
--- Run default ioloop for given MQTT clients or functions
-- @param ... MQTT clients or lopp functions to add to ioloop
-- @see mqtt.ioloop.get
-- @see mqtt.ioloop.run_until_clients
function mqtt.run_ioloop(...)
local loop = ioloop_get()
for i = 1, select("#", ...) do
local cl = select(i, ...)
loop:add(cl)
if type(cl) ~= "function" then
cl:start_connecting()
end
end
return loop:run_until_clients()
end
--- Run synchronous input/output loop for only one given MQTT client.
-- Provided client's connection will be opened.
-- Client reconnect feature will not work, and keep_alive too.
-- @param cl MQTT client instance to run
function mqtt.run_sync(cl)
local ok, err = cl:start_connecting()
if not ok then
return false, err
end
while cl.connection do
ok, err = cl:_sync_iteration()
if not ok then
return false, err
end
end
end
-- export module table
return mqtt
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,180 @@
--- ioloop module
-- @module mqtt.ioloop
-- @alias ioloop
--[[
ioloop module
In short: allowing you to work with several MQTT clients in one script, and allowing them to maintain
a long-term connection to broker, using PINGs.
NOTE: this module will work only with MQTT clients using standard luasocket/luasocket_ssl connectors.
In long:
Providing an IO loop instance dealing with efficient (as much as possible in limited lua IO) network communication
for several MQTT clients in the same OS thread.
The main idea is that you are creating an ioloop instance, then adding created and connected MQTT clients to it.
The ioloop instance is setting a non-blocking mode for sockets in MQTT clients and setting a small timeout
for their receive/send operations. Then ioloop is starting an endless loop trying to receive/send data for all added MQTT clients.
You may add more or remove some MQTT clients from the ioloop after it's created and started.
Using that ioloop is allowing you to run a MQTT client for long time, through sending PINGREQ packets to broker
in keepAlive interval to maintain long-term connection.
Also, any function can be added to the ioloop instance, and it will be called in the same endless loop over and over
alongside with added MQTT clients to provide you a piece of processor time to run your own logic (like running your own
network communications or any other thing good working in an io-loop)
]]
-- module table
local ioloop = {}
-- load required stuff
local next = next
local type = type
local ipairs = ipairs
local require = require
local setmetatable = setmetatable
local table = require("table")
local tbl_remove = table.remove
--- ioloop instances metatable
-- @type ioloop_mt
local ioloop_mt = {}
ioloop_mt.__index = ioloop_mt
--- Initialize ioloop instance
-- @tparam table args ioloop creation arguments table
-- @tparam[opt=0.005] number args.timeout network operations timeout in seconds
-- @tparam[opt=0] number args.sleep sleep interval after each iteration
-- @tparam[opt] function args.sleep_function custom sleep function to call after each iteration
-- @treturn ioloop_mt ioloop instance
function ioloop_mt:__init(args)
args = args or {}
args.timeout = args.timeout or 0.005
args.sleep = args.sleep or 0
args.sleep_function = args.sleep_function or require("socket").sleep
self.args = args
self.clients = {}
self.running = false --ioloop running flag, used by MQTT clients which are adding after this ioloop started to run
end
--- Add MQTT client or a loop function to the ioloop instance
-- @tparam client_mt|function client MQTT client or a loop function to add to ioloop
-- @return true on success or false and error message on failure
function ioloop_mt:add(client)
local clients = self.clients
if clients[client] then
return false, "such MQTT client or loop function is already added to this ioloop"
end
clients[#clients + 1] = client
clients[client] = true
-- associate ioloop with adding MQTT client
if type(client) ~= "function" then
client:set_ioloop(self)
end
return true
end
--- Remove MQTT client or a loop function from the ioloop instance
-- @tparam client_mt|function client MQTT client or a loop function to remove from ioloop
-- @return true on success or false and error message on failure
function ioloop_mt:remove(client)
local clients = self.clients
if not clients[client] then
return false, "no such MQTT client or loop function was added to ioloop"
end
clients[client] = nil
-- search an index of client to remove
for i, item in ipairs(clients) do
if item == client then
tbl_remove(clients, i)
break
end
end
-- unlink ioloop from MQTT client
if type(client) ~= "function" then
client:set_ioloop(nil)
end
return true
end
--- Perform one ioloop iteration
function ioloop_mt:iteration()
self.timeouted = false
for _, client in ipairs(self.clients) do
if type(client) ~= "function" then
client:_ioloop_iteration()
else
client()
end
end
-- sleep a bit
local args = self.args
local sleep = args.sleep
if sleep > 0 then
args.sleep_function(sleep)
end
end
--- Perform sleep if no one of the network operation in current iteration was not timeouted
function ioloop_mt:can_sleep()
if not self.timeouted then
local args = self.args
args.sleep_function(args.timeout)
self.timeouted = true
end
end
--- Run ioloop until at least one client are in ioloop
function ioloop_mt:run_until_clients()
self.running = true
while next(self.clients) do
self:iteration()
end
self.running = false
end
-------
--- Create IO loop instance with given options
-- @see ioloop_mt:__init
-- @treturn ioloop_mt ioloop instance
local function ioloop_create(args)
local inst = setmetatable({}, ioloop_mt)
inst:__init(args)
return inst
end
ioloop.create = ioloop_create
-- Default ioloop instance
local ioloop_instance
--- Returns default ioloop instance
-- @tparam[opt=true] boolean autocreate Automatically create ioloop instance
-- @tparam[opt] table args Arguments for creating ioloop instance
-- @treturn ioloop_mt ioloop instance
function ioloop.get(autocreate, args)
if autocreate == nil then
autocreate = true
end
if autocreate then
if not ioloop_instance then
ioloop_instance = ioloop_create(args)
end
end
return ioloop_instance
end
-------
-- export module table
return ioloop
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,48 @@
-- DOC: https://keplerproject.github.io/copas/
-- NOTE: you will need to install copas like this: luarocks install copas
-- module table
local connector = {}
local socket = require("socket")
local copas = require("copas")
-- Open network connection to .host and .port in conn table
-- Store opened socket to conn table
-- Returns true on success, or false and error text on failure
function connector.connect(conn)
local sock, err = socket.connect(conn.host, conn.port)
if not sock then
return false, "socket.connect failed: "..err
end
conn.sock = sock
return true
end
-- Shutdown network connection
function connector.shutdown(conn)
conn.sock:shutdown()
end
-- Send data to network connection
function connector.send(conn, data, i, j)
local ok, err = copas.send(conn.sock, data, i, j)
return ok, err
end
-- Receive given amount of data from network connection
function connector.receive(conn, size)
local ok, err = copas.receive(conn.sock, size)
return ok, err
end
-- Set connection's socket to non-blocking mode and set a timeout for it
function connector.settimeout(conn, timeout)
conn.timeout = timeout
conn.sock:settimeout(0)
end
-- export module table
return connector
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,52 @@
-- DOC: http://w3.impa.br/~diego/software/luasocket/tcp.html
-- module table
local luasocket = {}
local socket = require("socket")
-- Open network connection to .host and .port in conn table
-- Store opened socket to conn table
-- Returns true on success, or false and error text on failure
function luasocket.connect(conn)
local sock, err = socket.connect(conn.host, conn.port)
if not sock then
return false, "socket.connect failed: "..err
end
conn.sock = sock
return true
end
-- Shutdown network connection
function luasocket.shutdown(conn)
conn.sock:shutdown()
end
-- Send data to network connection
function luasocket.send(conn, data, i, j)
local ok, err = conn.sock:send(data, i, j)
-- print(" luasocket.send:", ok, err, require("mqtt.tools").hex(data))
return ok, err
end
-- Receive given amount of data from network connection
function luasocket.receive(conn, size)
local ok, err = conn.sock:receive(size)
-- if ok then
-- print(" luasocket.receive:", size, require("mqtt.tools").hex(ok))
-- elseif err ~= "timeout" then
-- print(" luasocket.receive:", ok, err)
-- end
return ok, err
end
-- Set connection's socket to non-blocking mode and set a timeout for it
function luasocket.settimeout(conn, timeout)
conn.timeout = timeout
conn.sock:settimeout(timeout, "t")
end
-- export module table
return luasocket
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,56 @@
-- DOC: http://w3.impa.br/~diego/software/luasocket/tcp.html
-- module table
local luasocket_ssl = {}
local type = type
local assert = assert
local luasocket = require("mqtt.luasocket")
-- Open network connection to .host and .port in conn table
-- Store opened socket to conn table
-- Returns true on success, or false and error text on failure
function luasocket_ssl.connect(conn)
assert(type(conn.secure_params) == "table", "expecting .secure_params to be a table")
-- open usual TCP connection
local ok, err = luasocket.connect(conn)
if not ok then
return false, "luasocket connect failed: "..err
end
local wrapped
-- load right ssl module
local ssl = require(conn.ssl_module or "ssl")
-- TLS/SSL initialization
wrapped, err = ssl.wrap(conn.sock, conn.secure_params)
if not wrapped then
conn.sock:shutdown()
return false, "ssl.wrap() failed: "..err
end
ok = wrapped:dohandshake()
if not ok then
conn.sock:shutdown()
return false, "ssl dohandshake failed"
end
-- replace sock in connection table with wrapped secure socket
conn.sock = wrapped
return true
end
-- Shutdown network connection
function luasocket_ssl.shutdown(conn)
conn.sock:close()
end
-- Copy original methods from mqtt.luasocket module
luasocket_ssl.send = luasocket.send
luasocket_ssl.receive = luasocket.receive
luasocket_ssl.settimeout = luasocket.settimeout
-- export module table
return luasocket_ssl
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,55 @@
-- module table
-- thanks to @irimiab: https://github.com/xHasKx/luamqtt/issues/13
local ngxsocket = {}
-- load required stuff
local string_sub = string.sub
local ngx_socket_tcp = ngx.socket.tcp -- luacheck: ignore
-- Open network connection to .host and .port in conn table
-- Store opened socket to conn table
-- Returns true on success, or false and error text on failure
function ngxsocket.connect(conn)
local socket = ngx_socket_tcp()
socket:settimeout(0x7FFFFFFF)
local sock, err = socket:connect(conn.host, conn.port)
if not sock then
return false, "socket:connect failed: "..err
end
if conn.secure then
socket:sslhandshake()
end
conn.sock = socket
return true
end
-- Shutdown network connection
function ngxsocket.shutdown(conn)
conn.sock:close()
end
-- Send data to network connection
function ngxsocket.send(conn, data, i, j)
if i then
return conn.sock:send(string_sub(data, i, j))
else
return conn.sock:send(data)
end
end
-- Receive given amount of data from network connection
function ngxsocket.receive(conn, size)
return conn.sock:receive(size)
end
-- Set connection's socket to non-blocking mode and set a timeout for it
function ngxsocket.settimeout(conn, timeout)
if not timeout then
conn.sock:settimeout(0x7FFFFFFF)
else
conn.sock:settimeout(timeout * 1000)
end
end
-- export module table
return ngxsocket

View File

@ -0,0 +1,525 @@
--- MQTT generic protocol components module
-- @module mqtt.protocol
--[[
Here is a generic implementation of MQTT protocols of all supported versions.
MQTT v3.1.1 documentation (DOCv3.1.1):
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
MQTT v5.0 documentation (DOCv5.0):
http://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
CONVENTIONS:
* read_func - function to read data from some stream-like object (like network connection).
We are calling it with one argument: number of bytes to read.
Use currying/closures to pass other arguments to this function.
This function should return string of given size on success.
On failure it should return false/nil and an error message.
]]
-- module table
local protocol = {}
-- load required stuff
local type = type
local error = error
local assert = assert
local require = require
local _VERSION = _VERSION -- lua interpreter version, not a mqtt._VERSION
local tostring = tostring
local setmetatable = setmetatable
local table = require("table")
local tbl_concat = table.concat
local unpack = unpack or table.unpack
local string = require("string")
local str_sub = string.sub
local str_char = string.char
local str_byte = string.byte
local str_format = string.format
local bit = require("mqtt.bitwrap")
local bor = bit.bor
local band = bit.band
local lshift = bit.lshift
local rshift = bit.rshift
local tools = require("mqtt.tools")
local div = tools.div
-- Create uint8 value data
local function make_uint8(val)
if val < 0 or val > 0xFF then
error("value is out of range to encode as uint8: "..tostring(val))
end
return str_char(val)
end
protocol.make_uint8 = make_uint8
-- Create uint16 value data
local function make_uint16(val)
if val < 0 or val > 0xFFFF then
error("value is out of range to encode as uint16: "..tostring(val))
end
return str_char(rshift(val, 8), band(val, 0xFF))
end
protocol.make_uint16 = make_uint16
-- Create uint32 value data
function protocol.make_uint32(val)
if val < 0 or val > 0xFFFFFFFF then
error("value is out of range to encode as uint32: "..tostring(val))
end
return str_char(rshift(val, 24), band(rshift(val, 16), 0xFF), band(rshift(val, 8), 0xFF), band(val, 0xFF))
end
-- Create UTF-8 string data
-- DOCv3.1.1: 1.5.3 UTF-8 encoded strings
-- DOCv5.0: 1.5.4 UTF-8 Encoded String
function protocol.make_string(str)
return make_uint16(str:len())..str
end
-- Returns bytes of given integer value encoded as variable length field
-- DOCv3.1.1: 2.2.3 Remaining Length
-- DOCv5.0: 2.1.4 Remaining Length
local function make_var_length(len)
if len < 0 or len > 268435455 then
error("value is invalid for encoding as variable length field: "..tostring(len))
end
local bytes = {}
local i = 1
repeat
local byte = len % 128
len = div(len, 128)
if len > 0 then
byte = bor(byte, 128)
end
bytes[i] = byte
i = i + 1
until len <= 0
return unpack(bytes)
end
protocol.make_var_length = make_var_length
-- Make data for 1-byte property with only 0 or 1 value
function protocol.make_uint8_0_or_1(value)
if value ~= 0 and value ~= 1 then
error("expecting 0 or 1 as value")
end
return make_uint8(value)
end
-- Make data for 2-byte property with nonzero value check
function protocol.make_uint16_nonzero(value)
if value == 0 then
error("expecting nonzero value")
end
return make_uint16(value)
end
-- Make data for variable length property with nonzero value check
function protocol.make_var_length_nonzero(value)
if value == 0 then
error("expecting nonzero value")
end
return make_var_length(value)
end
-- Read string using given read_func function
-- Returns false plus error message on failure
-- Returns parsed string on success
function protocol.parse_string(read_func)
assert(type(read_func) == "function", "expecting read_func to be a function")
local len, err = read_func(2)
if not len then
return false, "failed to read string length: "..err
end
-- convert len string from 2-byte integer
local byte1, byte2 = str_byte(len, 1, 2)
len = bor(lshift(byte1, 8), byte2)
-- and return string if parsed length
return read_func(len)
end
-- Parse uint8 value using given read_func
local function parse_uint8(read_func)
assert(type(read_func) == "function", "expecting read_func to be a function")
local value, err = read_func(1)
if not value then
return false, "failed to read 1 byte for uint8: "..err
end
return str_byte(value, 1, 1)
end
protocol.parse_uint8 = parse_uint8
-- Parse uint8 value with only 0 or 1 value
function protocol.parse_uint8_0_or_1(read_func)
local value, err = parse_uint8(read_func)
if not value then
return false, err
end
if value ~= 0 and value ~= 1 then
return false, "expecting only 0 or 1 but got: "..value
end
return value
end
-- Parse uint16 value using given read_func
local function parse_uint16(read_func)
assert(type(read_func) == "function", "expecting read_func to be a function")
local value, err = read_func(2)
if not value then
return false, "failed to read 2 byte for uint16: "..err
end
local byte1, byte2 = str_byte(value, 1, 2)
return lshift(byte1, 8) + byte2
end
protocol.parse_uint16 = parse_uint16
-- Parse uint16 non-zero value using given read_func
function protocol.parse_uint16_nonzero(read_func)
local value, err = parse_uint16(read_func)
if not value then
return false, err
end
if value == 0 then
return false, "expecting non-zero value"
end
return value
end
-- Parse uint32 value using given read_func
function protocol.parse_uint32(read_func)
assert(type(read_func) == "function", "expecting read_func to be a function")
local value, err = read_func(4)
if not value then
return false, "failed to read 4 byte for uint32: "..err
end
local byte1, byte2, byte3, byte4 = str_byte(value, 1, 4)
if _VERSION < "Lua 5.3" then
return byte1 * (2 ^ 24) + lshift(byte2, 16) + lshift(byte3, 8) + byte4
else
return lshift(byte1, 24) + lshift(byte2, 16) + lshift(byte3, 8) + byte4
end
end
-- Max variable length integer value
local max_mult = 128 * 128 * 128
-- Returns variable length field value calling read_func function read data, DOC: 2.2.3 Remaining Length
local function parse_var_length(read_func)
assert(type(read_func) == "function", "expecting read_func to be a function")
local mult = 1
local val = 0
repeat
local byte, err = read_func(1)
if not byte then
return false, err
end
byte = str_byte(byte, 1, 1)
val = val + band(byte, 127) * mult
if mult > max_mult then
return false, "malformed variable length field data"
end
mult = mult * 128
until band(byte, 128) == 0
return val
end
protocol.parse_var_length = parse_var_length
-- Parse Variable Byte Integer with non-zero constraint
function protocol.parse_var_length_nonzero(read_func)
local value, err = parse_var_length(read_func)
if not value then
return false, err
end
if value == 0 then
return false, "expecting non-zero value"
end
return value
end
-- Create fixed packet header data
-- DOCv3.1.1: 2.2 Fixed header
-- DOCv5.0: 2.1.1 Fixed Header
function protocol.make_header(ptype, flags, len)
local byte1 = bor(lshift(ptype, 4), band(flags, 0x0F))
return str_char(byte1, make_var_length(len))
end
-- Returns true if given value is a valid QoS
function protocol.check_qos(val)
return (val == 0) or (val == 1) or (val == 2)
end
-- Returns true if given value is a valid Packet Identifier
-- DOCv3.1.1: 2.3.1 Packet Identifier
-- DOCv5.0: 2.2.1 Packet Identifier
function protocol.check_packet_id(val)
return val >= 1 and val <= 0xFFFF
end
-- Returns the next Packet Identifier value relative to given current value
-- DOCv3.1.1: 2.3.1 Packet Identifier
-- DOCv5.0: 2.2.1 Packet Identifier
function protocol.next_packet_id(curr)
if not curr then
return 1
end
assert(type(curr) == "number", "expecting curr to be a number")
assert(curr >= 1, "expecting curr to be >= 1")
curr = curr + 1
if curr > 0xFFFF then
curr = 1
end
return curr
end
-- MQTT protocol fixed header packet types
-- DOCv3.1.1: 2.2.1 MQTT Control Packet type
-- DOCv5.0: 2.1.2 MQTT Control Packet type
local packet_type = {
CONNECT = 1,
CONNACK = 2,
PUBLISH = 3,
PUBACK = 4,
PUBREC = 5,
PUBREL = 6,
PUBCOMP = 7,
SUBSCRIBE = 8,
SUBACK = 9,
UNSUBSCRIBE = 10,
UNSUBACK = 11,
PINGREQ = 12,
PINGRESP = 13,
DISCONNECT = 14,
AUTH = 15, -- NOTE: new in MQTTv5.0
[1] = "CONNECT",
[2] = "CONNACK",
[3] = "PUBLISH",
[4] = "PUBACK",
[5] = "PUBREC",
[6] = "PUBREL",
[7] = "PUBCOMP",
[8] = "SUBSCRIBE",
[9] = "SUBACK",
[10] = "UNSUBSCRIBE",
[11] = "UNSUBACK",
[12] = "PINGREQ",
[13] = "PINGRESP",
[14] = "DISCONNECT",
[15] = "AUTH", -- NOTE: new in MQTTv5.0
}
protocol.packet_type = packet_type
-- Packet types requiring packet identifier field
-- DOCv3.1.1: 2.3.1 Packet Identifier
-- DOCv5.0: 2.2.1 Packet Identifier
local packets_requiring_packet_id = {
[packet_type.PUBACK] = true,
[packet_type.PUBREC] = true,
[packet_type.PUBREL] = true,
[packet_type.PUBCOMP] = true,
[packet_type.SUBSCRIBE] = true,
[packet_type.SUBACK] = true,
[packet_type.UNSUBSCRIBE] = true,
[packet_type.UNSUBACK] = true,
}
-- CONNACK return code/reason code strings
local connack_rc = {
-- MQTT v3.1.1 Connect return codes, DOCv3.1.1: 3.2.2.3 Connect Return code
[0] = "Connection Accepted",
[1] = "Connection Refused, unacceptable protocol version",
[2] = "Connection Refused, identifier rejected",
[3] = "Connection Refused, Server unavailable",
[4] = "Connection Refused, bad user name or password",
[5] = "Connection Refused, not authorized",
-- MQTT v5.0 Connect reason codes, DOCv5.0: 3.2.2.2 Connect Reason Code
[0x80] = "Unspecified error",
[0x81] = "Malformed Packet",
[0x82] = "Protocol Error",
[0x83] = "Implementation specific error",
[0x84] = "Unsupported Protocol Version",
[0x85] = "Client Identifier not valid",
[0x86] = "Bad User Name or Password",
[0x87] = "Not authorized",
[0x88] = "Server unavailable",
[0x89] = "Server busy",
[0x8A] = "Banned",
[0x8C] = "Bad authentication method",
[0x90] = "Topic Name invalid",
[0x95] = "Packet too large",
[0x97] = "Quota exceeded",
[0x99] = "Payload format invalid",
[0x9A] = "Retain not supported",
[0x9B] = "QoS not supported",
[0x9C] = "Use another server",
[0x9D] = "Server moved",
[0x9F] = "Connection rate exceeded",
}
protocol.connack_rc = connack_rc
-- Returns true if Packet Identifier field are required for given packet
function protocol.packet_id_required(args)
assert(type(args) == "table", "expecting args to be a table")
assert(type(args.type) == "number", "expecting .type to be a number")
local ptype = args.type
if ptype == packet_type.PUBLISH and args.qos and args.qos > 0 then
return true
end
return packets_requiring_packet_id[ptype]
end
-- Metatable for combined data packet, should looks like a string
local combined_packet_mt = {
-- Convert combined data packet to string
__tostring = function(self)
local strings = {}
for i, part in ipairs(self) do
strings[i] = tostring(part)
end
return tbl_concat(strings)
end,
-- Get length of combined data packet
len = function(self)
local len = 0
for _, part in ipairs(self) do
len = len + part:len()
end
return len
end,
-- Append part to the end of combined data packet
append = function(self, part)
self[#self + 1] = part
end
}
-- Make combined_packet_mt table works like a class
combined_packet_mt.__index = function(_, key)
return combined_packet_mt[key]
end
-- Combine several data parts into one
function protocol.combine(...)
return setmetatable({...}, combined_packet_mt)
end
-- Convert any value to string, respecting strings and tables
local function value_tostring(value)
local t = type(value)
if t == "string" then
return str_format("%q", value)
elseif t == "table" then
local res = {}
for k, v in pairs(value) do
if type(k) == "number" then
res[#res + 1] = value_tostring(v)
else
if k:match("^[a-zA-Z_][_%w]*$") then
res[#res + 1] = str_format("%s=%s", k, value_tostring(v))
else
res[#res + 1] = str_format("[%q]=%s", k, value_tostring(v))
end
end
end
return str_format("{%s}", tbl_concat(res, ", "))
else
return tostring(value)
end
end
-- Convert packet to string representation
local function packet_tostring(packet)
local res = {}
for k, v in pairs(packet) do
res[#res + 1] = str_format("%s=%s", k, value_tostring(v))
end
return str_format("%s{%s}", tostring(packet_type[packet.type]), tbl_concat(res, ", "))
end
protocol.packet_tostring = packet_tostring
-- Parsed packet metatable
protocol.packet_mt = {
__tostring = packet_tostring,
}
-- Parsed CONNACK packet metatable
protocol.connack_packet_mt = {
__tostring = packet_tostring,
}
protocol.connack_packet_mt.__index = protocol.connack_packet_mt
--- Returns reason string for CONNACK packet
-- @treturn string Reason string according packet's rc field
function protocol.connack_packet_mt:reason_string()
return connack_rc[self.rc]
end
--- Start parsing a new packet
-- @tparam function read_func - function to read data from the network connection
-- @treturn number packet_type
-- @treturn number flags
-- @treturn table input - a table with fields "read_func" and "available" representing a stream-like object
-- to read already received packet data in chunks
-- @return false and error_message on failure
function protocol.start_parse_packet(read_func)
assert(type(read_func) == "function", "expecting read_func to be a function")
local byte1, err, len, data
-- parse fixed header
-- DOC[v3.1.1]: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html#_Toc442180832
-- DOC[v5.0]: https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901020
byte1, err = read_func(1)
if not byte1 then
return false, "failed to read first byte: "..err
end
byte1 = str_byte(byte1, 1, 1)
local ptype = rshift(byte1, 4)
local flags = band(byte1, 0xF)
len, err = parse_var_length(read_func)
if not len then
return false, "failed to parse remaining length: "..err
end
-- create packet parser instance (aka input)
local input = {1, available = 0} -- input data offset and available size
if len > 0 then
data, err = read_func(len)
else
data = ""
end
if not data then
return false, "failed to read packet data: "..err
end
input.available = data:len()
-- read data function for the input instance
input.read_func = function(size)
if size > input.available then
return false, "not enough data to read size: "..size
end
local off = input[1]
local res = str_sub(data, off, off + size - 1)
input[1] = off + size
input.available = input.available - size
return res
end
return ptype, flags, input
end
-- export module table
return protocol
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,427 @@
--[[
Here is a MQTT v3.1.1 protocol implementation
MQTT v3.1.1 documentation (DOC):
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/errata01/os/mqtt-v3.1.1-errata01-os-complete.html
]]
-- module table
local protocol4 = {}
-- load required stuff
local type = type
local error = error
local assert = assert
local require = require
local tostring = tostring
local setmetatable = setmetatable
local bit = require("mqtt.bitwrap")
local bor = bit.bor
local band = bit.band
local lshift = bit.lshift
local rshift = bit.rshift
local protocol = require("mqtt.protocol")
local make_uint8 = protocol.make_uint8
local make_uint16 = protocol.make_uint16
local make_string = protocol.make_string
local make_header = protocol.make_header
local check_qos = protocol.check_qos
local check_packet_id = protocol.check_packet_id
local combine = protocol.combine
local packet_type = protocol.packet_type
local packet_mt = protocol.packet_mt
local connack_packet_mt = protocol.connack_packet_mt
local start_parse_packet = protocol.start_parse_packet
local parse_uint8 = protocol.parse_uint8
local parse_uint16 = protocol.parse_uint16
-- Create Connect Flags data, DOC: 3.1.2.3 Connect Flags
local function make_connect_flags(args)
local byte = 0 -- bit 0 should be zero
-- DOC: 3.1.2.4 Clean Session
if args.clean ~= nil then
assert(type(args.clean) == "boolean", "expecting .clean to be a boolean")
if args.clean then
byte = bor(byte, lshift(1, 1))
end
end
-- DOC: 3.1.2.5 Will Flag
if args.will ~= nil then
-- check required args are presented
assert(type(args.will) == "table", "expecting .will to be a table")
assert(type(args.will.payload) == "string", "expecting .will.payload to be a string")
assert(type(args.will.topic) == "string", "expecting .will.topic to be a string")
if args.will.qos ~= nil then
assert(type(args.will.qos) == "number", "expecting .will.qos to be a number")
assert(check_qos(args.will.qos), "expecting .will.qos to be a valid QoS value")
end
if args.will.retain ~= nil then
assert(type(args.will.retain) == "boolean", "expecting .will.retain to be a boolean")
end
-- will flag should be set to 1
byte = bor(byte, lshift(1, 2))
-- DOC: 3.1.2.6 Will QoS
byte = bor(byte, lshift(args.will.qos or 0, 3))
-- DOC: 3.1.2.7 Will Retain
if args.will.retain then
byte = bor(byte, lshift(1, 5))
end
end
-- DOC: 3.1.2.8 User Name Flag
if args.username ~= nil then
assert(type(args.username) == "string", "expecting .username to be a string")
byte = bor(byte, lshift(1, 7))
end
-- DOC: 3.1.2.9 Password Flag
if args.password ~= nil then
assert(type(args.password) == "string", "expecting .password to be a string")
assert(args.username, "the .username is required to set .password")
byte = bor(byte, lshift(1, 6))
end
return make_uint8(byte)
end
-- Create CONNECT packet, DOC: 3.1 CONNECT Client requests a connection to a Server
local function make_packet_connect(args)
-- check args
assert(type(args.id) == "string", "expecting .id to be a string with MQTT client id")
-- DOC: 3.1.2.10 Keep Alive
local keep_alive_ival = 0
if args.keep_alive then
assert(type(args.keep_alive) == "number")
keep_alive_ival = args.keep_alive
end
-- DOC: 3.1.2 Variable header
local variable_header = combine(
make_string("MQTT"), -- DOC: 3.1.2.1 Protocol Name
make_uint8(4), -- DOC: 3.1.2.2 Protocol Level (4 is for MQTT v3.1.1)
make_connect_flags(args), -- DOC: 3.1.2.3 Connect Flags
make_uint16(keep_alive_ival) -- DOC: 3.1.2.10 Keep Alive
)
-- DOC: 3.1.3 Payload
-- DOC: 3.1.3.1 Client Identifier
local payload = combine(
make_string(args.id)
)
if args.will then
-- DOC: 3.1.3.2 Will Topic
assert(type(args.will.topic) == "string", "expecting will.topic to be a string")
payload:append(make_string(args.will.topic))
-- DOC: 3.1.3.3 Will Message
assert(args.will.payload == nil or type(args.will.payload) == "string", "expecting will.payload to be a string or nil")
payload:append(make_string(args.will.payload or ""))
end
if args.username then
-- DOC: 3.1.3.4 User Name
payload:append(make_string(args.username))
if args.password then
-- DOC: 3.1.3.5 Password
payload:append(make_string(args.password))
end
end
-- DOC: 3.1.1 Fixed header
local header = make_header(packet_type.CONNECT, 0, variable_header:len() + payload:len())
return combine(header, variable_header, payload)
end
-- Create PUBLISH packet, DOC: 3.3 PUBLISH Publish message
local function make_packet_publish(args)
-- check args
assert(type(args.topic) == "string", "expecting .topic to be a string")
if args.payload ~= nil then
assert(type(args.payload) == "string", "expecting .payload to be a string")
end
if args.qos ~= nil then
assert(type(args.qos) == "number", "expecting .qos to be a number")
assert(check_qos(args.qos), "expecting .qos to be a valid QoS value")
end
if args.retain ~= nil then
assert(type(args.retain) == "boolean", "expecting .retain to be a boolean")
end
if args.dup ~= nil then
assert(type(args.dup) == "boolean", "expecting .dup to be a boolean")
end
-- DOC: 3.3.1 Fixed header
local flags = 0
-- 3.3.1.3 RETAIN
if args.retain then
flags = bor(flags, 0x1)
end
-- DOC: 3.3.1.2 QoS
flags = bor(flags, lshift(args.qos or 0, 1))
-- DOC: 3.3.1.1 DUP
if args.dup then
flags = bor(flags, lshift(1, 3))
end
-- DOC: 3.3.2 Variable header
local variable_header = combine(
make_string(args.topic)
)
-- DOC: 3.3.2.2 Packet Identifier
if args.qos and args.qos > 0 then
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
variable_header:append(make_uint16(args.packet_id))
end
local payload
if args.payload then
payload = args.payload
else
payload = ""
end
-- DOC: 3.3.1 Fixed header
local header = make_header(packet_type.PUBLISH, flags, variable_header:len() + payload:len())
return combine(header, variable_header, payload)
end
-- Create PUBACK packet, DOC: 3.4 PUBACK Publish acknowledgement
local function make_packet_puback(args)
-- check args
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
-- DOC: 3.4.1 Fixed header
local header = make_header(packet_type.PUBACK, 0, 2)
-- DOC: 3.4.2 Variable header
local variable_header = make_uint16(args.packet_id)
return combine(header, variable_header)
end
-- Create PUBREC packet, DOC: 3.5 PUBREC Publish received (QoS 2 publish received, part 1)
local function make_packet_pubrec(args)
-- check args
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
-- DOC: 3.5.1 Fixed header
local header = make_header(packet_type.PUBREC, 0, 2)
-- DOC: 3.5.2 Variable header
local variable_header = make_uint16(args.packet_id)
return combine(header, variable_header)
end
-- Create PUBREL packet, DOC: 3.6 PUBREL Publish release (QoS 2 publish received, part 2)
local function make_packet_pubrel(args)
-- check args
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
-- DOC: 3.6.1 Fixed header
local header = make_header(packet_type.PUBREL, 0x2, 2) -- flags are 0x2 == 0010 bits (fixed value)
-- DOC: 3.6.2 Variable header
local variable_header = make_uint16(args.packet_id)
return combine(header, variable_header)
end
-- Create PUBCOMP packet, DOC: 3.7 PUBCOMP Publish complete (QoS 2 publish received, part 3)
local function make_packet_pubcomp(args)
-- check args
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
-- DOC: 3.7.1 Fixed header
local header = make_header(packet_type.PUBCOMP, 0, 2)
-- DOC: 3.7.2 Variable header
local variable_header = make_uint16(args.packet_id)
return combine(header, variable_header)
end
-- Create SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe to topics
local function make_packet_subscribe(args)
-- check args
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
-- DOC: 3.8.2 Variable header
local variable_header = combine(
make_uint16(args.packet_id)
)
-- DOC: 3.8.3 Payload
local payload = combine()
for i, subscription in ipairs(args.subscriptions) do
assert(type(subscription) == "table", "expecting .subscriptions["..i.."] to be a table")
assert(type(subscription.topic) == "string", "expecting .subscriptions["..i.."].topic to be a string")
if subscription.qos ~= nil then
assert(type(subscription.qos) == "number", "expecting .subscriptions["..i.."].qos to be a number")
assert(check_qos(subscription.qos), "expecting .subscriptions["..i.."].qos to be a valid QoS value")
end
payload:append(make_string(subscription.topic))
payload:append(make_uint8(subscription.qos or 0))
end
-- DOC: 3.8.1 Fixed header
local header = make_header(packet_type.SUBSCRIBE, 2, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x2
return combine(header, variable_header, payload)
end
-- Create UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE Unsubscribe from topics
local function make_packet_unsubscribe(args)
-- check args
assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
-- DOC: 3.10.2 Variable header
local variable_header = combine(
make_uint16(args.packet_id)
)
-- DOC: 3.10.3 Payload
local payload = combine()
for i, subscription in ipairs(args.subscriptions) do
assert(type(subscription) == "string", "expecting .subscriptions["..i.."] to be a string")
payload:append(make_string(subscription))
end
-- DOC: 3.10.1 Fixed header
local header = make_header(packet_type.UNSUBSCRIBE, 2, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x2
return combine(header, variable_header, payload)
end
-- Create packet of given {type: number} in args
function protocol4.make_packet(args)
assert(type(args) == "table", "expecting args to be a table")
assert(type(args.type) == "number", "expecting .type number in args")
local ptype = args.type
if ptype == packet_type.CONNECT then
return make_packet_connect(args)
elseif ptype == packet_type.PUBLISH then
return make_packet_publish(args)
elseif ptype == packet_type.PUBACK then
return make_packet_puback(args)
elseif ptype == packet_type.PUBREC then
return make_packet_pubrec(args)
elseif ptype == packet_type.PUBREL then
return make_packet_pubrel(args)
elseif ptype == packet_type.PUBCOMP then
return make_packet_pubcomp(args)
elseif ptype == packet_type.SUBSCRIBE then
return make_packet_subscribe(args)
elseif ptype == packet_type.UNSUBSCRIBE then
return make_packet_unsubscribe(args)
elseif ptype == packet_type.PINGREQ then
-- DOC: 3.12 PINGREQ PING request
return combine("\192\000") -- 192 == 0xC0, type == 12, flags == 0
elseif ptype == packet_type.DISCONNECT then
-- DOC: 3.14 DISCONNECT Disconnect notification
return combine("\224\000") -- 224 == 0xD0, type == 14, flags == 0
else
error("unexpected packet type to make: "..ptype)
end
end
-- Parse packet using given read_func
-- Returns packet on success or false and error message on failure
function protocol4.parse_packet(read_func)
local ptype, flags, input = start_parse_packet(read_func)
if not ptype then
return false, flags
end
-- parse readed data according type in fixed header
if ptype == packet_type.CONNACK then
-- DOC: 3.2 CONNACK Acknowledge connection request
if input.available ~= 2 then
return false, "expecting data of length 2 bytes"
end
local byte1, byte2 = parse_uint8(input.read_func), parse_uint8(input.read_func)
local sp = (band(byte1, 0x1) ~= 0)
return setmetatable({type=ptype, sp=sp, rc=byte2}, connack_packet_mt)
elseif ptype == packet_type.PUBLISH then
-- DOC: 3.3 PUBLISH Publish message
-- DOC: 3.3.1.1 DUP
local dup = (band(flags, 0x8) ~= 0)
-- DOC: 3.3.1.2 QoS
local qos = band(rshift(flags, 1), 0x3)
-- DOC: 3.3.1.3 RETAIN
local retain = (band(flags, 0x1) ~= 0)
-- DOC: 3.3.2.1 Topic Name
if input.available < 2 then
return false, "expecting data of length at least 2 bytes"
end
local topic_len = parse_uint16(input.read_func)
if input.available < topic_len then
return false, "malformed PUBLISH packet: not enough data to parse topic"
end
local topic = input.read_func(topic_len)
-- DOC: 3.3.2.2 Packet Identifier
local packet_id
if qos > 0 then
-- DOC: 3.3.2.2 Packet Identifier
if input.available < 2 then
return false, "malformed PUBLISH packet: not enough data to parse packet_id"
end
packet_id = parse_uint16(input.read_func)
end
-- DOC: 3.3.3 Payload
local payload
if input.available > 0 then
payload = input.read_func(input.available)
end
return setmetatable({type=ptype, dup=dup, qos=qos, retain=retain, packet_id=packet_id, topic=topic, payload=payload}, packet_mt)
elseif ptype == packet_type.PUBACK then
-- DOC: 3.4 PUBACK Publish acknowledgement
if input.available ~= 2 then
return false, "expecting data of length 2 bytes"
end
-- DOC: 3.4.2 Variable header
local packet_id = parse_uint16(input.read_func)
return setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
elseif ptype == packet_type.PUBREC then
-- DOC: 3.5 PUBREC Publish received (QoS 2 publish received, part 1)
if input.available ~= 2 then
return false, "expecting data of length 2 bytes"
end
-- DOC: 3.5.2 Variable header
local packet_id = parse_uint16(input.read_func)
return setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
elseif ptype == packet_type.PUBREL then
-- DOC: 3.6 PUBREL Publish release (QoS 2 publish received, part 2)
if input.available ~= 2 then
return false, "expecting data of length 2 bytes"
end
-- also flags should be checked to equals 2 by the server
-- DOC: 3.6.2 Variable header
local packet_id = parse_uint16(input.read_func)
return setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
elseif ptype == packet_type.PUBCOMP then
-- 3.7 PUBCOMP Publish complete (QoS 2 publish received, part 3)
if input.available ~= 2 then
return false, "expecting data of length 2 bytes"
end
-- DOC: 3.7.2 Variable header
local packet_id = parse_uint16(input.read_func)
return setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
elseif ptype == packet_type.SUBACK then
-- DOC: 3.9 SUBACK Subscribe acknowledgement
if input.available < 3 then
return false, "expecting data of length at least 3 bytes"
end
-- DOC: 3.9.2 Variable header
-- DOC: 3.9.3 Payload
local packet_id = parse_uint16(input.read_func)
local rc = {} -- DOC: The payload contains a list of return codes.
while input.available > 0 do
rc[#rc + 1] = parse_uint8(input.read_func)
end
return setmetatable({type=ptype, packet_id=packet_id, rc=rc}, packet_mt)
elseif ptype == packet_type.UNSUBACK then
-- DOC: 3.11 UNSUBACK Unsubscribe acknowledgement
if input.available ~= 2 then
return false, "expecting data of length 2 bytes"
end
-- DOC: 3.11.2 Variable header
local packet_id = parse_uint16(input.read_func)
return setmetatable({type=ptype, packet_id=packet_id}, packet_mt)
elseif ptype == packet_type.PINGRESP then
-- DOC: 3.13 PINGRESP PING response
if input.available ~= 0 then
return false, "expecting data of length 0 bytes"
end
return setmetatable({type=ptype}, packet_mt)
else
return false, "unexpected packet type received: "..tostring(ptype)
end
end
-- export module table
return protocol4
-- vim: ts=4 sts=4 sw=4 noet ft=lua

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
-- module table
local tools = {}
-- load required stuff
local require = require
local string = require("string")
local str_format = string.format
local str_byte = string.byte
local table = require("table")
local tbl_concat = table.concat
local math = require("math")
local math_floor = math.floor
-- Returns string encoded as HEX
function tools.hex(str)
local res = {}
for i = 1, #str do
res[i] = str_format("%02X", str_byte(str, i))
end
return tbl_concat(res)
end
-- Integer division function
function tools.div(x, y)
return math_floor(x / y)
end
-- export module table
return tools
-- vim: ts=4 sts=4 sw=4 noet ft=lua

View File

@ -0,0 +1,70 @@
-- Load MQTT library
local mqtt = require("mqtt")
local client
local eventChannel = love.thread.getChannel("mqtt_event")
local commandChannel = love.thread.getChannel("mqtt_command")
local function call(target, ...)
local args = {...}
eventChannel:push {
target = target,
args = args
}
end
local function onConnect(connack)
print("On connect")
call("connect", connack)
end
local function onMessage(message)
if message.topic:sub(0, 7) == "spider/" then
message.topic = message.topic:sub(8)
end
print(message.topic)
call("message2", message.topic, message.payload)
end
local function onCommand(command)
if command.command == "send" then
client:publish {
topic = "spider/" .. command.topic,
payload = command.arg,
qos = 0
}
elseif command.command == "subscribe" then
local topic = "spider/" .. command.topic
assert(client:subscribe {
topic = topic
})
print("Subribed to " .. topic)
end
end
local function main()
client = mqtt.client {
uri = "mqtt.seeseepuff.be",
username = "mqtt_controller",
clean = true,
reconnect = 5,
}
client:on {
connect = onConnect,
message = onMessage
}
print("Connecting")
local ioloop = mqtt.get_ioloop()
local i = 0
ioloop:add(function()
local command = commandChannel:pop()
if command then
onCommand(command)
end
end)
mqtt.run_ioloop(client)
end
main()

54
upload.lua Normal file
View File

@ -0,0 +1,54 @@
package.path = package.path .. ';./controller-host/?/init.lua;./controller-host/?.lua'
local mqtt = require("mqtt")
local client
local file = ...
local fh = io.open(file, "rb")
local contents = fh:read("a")
fh:close()
function printTable(table, indentation)
indentation = indentation or ""
for name, value in pairs(table) do
print(indentation .. tostring(name) .. ": " .. tostring(value))
end
end
local function onMessage(data)
print(data.payload)
end
local function onConnect(connack)
if connack.rc ~= 0 then
print("Connection to broker failed:", connack:reason_string())
os.exit(1)
end
print("Connected to MQTT")
assert(client:subscribe{
topic = "spider/controller/stdout"
})
io.write("Sending payload...")
assert(client:publish {
topic = "spider/controller/payload",
payload = contents,
qos = 0
})
print(" DONE!")
end
client = mqtt.client {
uri = "mqtt.seeseepuff.be",
username = "mqtt_controller",
clean = true,
reconnect = 5,
}
client:on {
connect = onConnect,
message = onMessage,
}
print("Connecting")
mqtt.run_ioloop(client)

7
upload.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
set -e
cd controller-client
rm -vf ../controller-client.zip
zip -r ../controller-client.zip *
cd ..
lua upload.lua controller-client.zip