mineqtt/mqtt.lua

309 lines
6.6 KiB
Lua
Raw Normal View History

2024-09-09 01:19:47 +02:00
local computer = require("computer")
2024-09-05 02:33:40 +02:00
local mqtt = {}
2024-09-05 01:20:57 +02:00
2024-09-05 17:51:40 +02:00
local safeRead = function (conn, n)
local success, result, err = pcall(conn.read, conn, n)
if success then
return result, err
end
return nil, result
end
2024-09-05 17:00:57 +02:00
local readVarint = function (conn, first_byte)
2024-09-05 21:30:44 +02:00
local b, data, err
2024-09-05 14:52:59 +02:00
if first_byte == nil then
2024-09-05 21:30:44 +02:00
data, err = safeRead(conn, 1)
if err ~= nil then
return nil, err
end
b = string.byte(data)
2024-09-05 14:52:59 +02:00
else
b = first_byte
end
local n, s = 0, 0
2024-09-05 21:30:44 +02:00
while b & 0x80 == 0x80 do
2024-09-05 14:52:59 +02:00
if s > 21 then
2024-09-05 21:30:44 +02:00
return nil, "number too large"
2024-09-05 14:52:59 +02:00
end
n = n + ((b & 0x7F) << s)
s = s + 7
2024-09-05 17:51:40 +02:00
2024-09-05 21:30:44 +02:00
data, err = safeRead(conn, 1)
if err ~= nil then
return nil, err
end
2024-09-05 14:52:59 +02:00
2024-09-05 21:30:44 +02:00
b = string.byte(data)
2024-09-05 14:52:59 +02:00
end
return n + (b << s), nil
2024-09-05 01:20:57 +02:00
end
2024-09-05 21:31:52 +02:00
local encodeVarint = function (n)
if n > 268435455 then
return nil, "number too large"
end
local data = ""
while n > 127 do
data = data .. string.char(0x80 | n & 0x7F)
n = n >> 7
end
return data .. string.char(n), nil
end
2024-09-05 02:33:40 +02:00
local MqttClient = {}
2024-09-05 01:20:57 +02:00
2024-09-05 02:33:40 +02:00
function mqtt.open (address, port)
2024-09-05 17:00:36 +02:00
local conn, err = require("internet").open(address, port)
2024-09-05 14:52:59 +02:00
if conn == nil then
2024-09-05 17:00:36 +02:00
return nil, err
2024-09-05 14:52:59 +02:00
end
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
return MqttClient:new(conn), nil
2024-09-05 01:20:57 +02:00
end
function MqttClient:new (conn)
2024-09-05 14:52:59 +02:00
local c = c or {}
setmetatable(c, self)
self.__index = self
2024-09-05 01:20:57 +02:00
2024-09-05 17:00:57 +02:00
conn.readVarint = readVarint
2024-09-05 17:51:40 +02:00
conn.safeRead = safeRead
2024-09-09 01:19:47 +02:00
conn:setTimeout(0)
2024-09-05 16:59:50 +02:00
2024-09-05 14:52:59 +02:00
c.conn = conn
c.is_connecting = false
c.is_connected = false
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
return c
2024-09-05 01:20:57 +02:00
end
function MqttClient:handle ()
if not (self.is_connecting or self.is_connected) then
return "no connection"
end
2024-09-05 17:51:40 +02:00
local data, err = self.conn:safeRead(2)
2024-09-09 01:19:47 +02:00
2024-09-05 17:51:40 +02:00
if err ~= nil then
2024-09-09 01:19:47 +02:00
if (string.find(err, "timeout")) then
return nil
end
2024-09-05 17:51:40 +02:00
return err
2024-09-05 14:52:59 +02:00
end
2024-09-05 17:52:07 +02:00
local ptype, length, _ = string.unpack("B B", data)
2024-09-05 14:52:59 +02:00
2024-09-05 17:00:57 +02:00
local length, err = self.conn:readVarint(length)
2024-09-05 14:52:59 +02:00
if err ~= nil then
return err
end
if length > 0 then
2024-09-05 17:51:40 +02:00
data, err = self.conn:safeRead(length)
if err ~= nil then
return err
2024-09-05 14:52:59 +02:00
end
else
data = ""
end
if ptype & 0xF0 == 0x20 then -- CONNACK
if ptype ~= 0x20 or length < 2 then
self:disconnect(0x81)
return "malformed packet"
end
local flags, reason, _ = string.unpack("B B", data)
if flags ~= 0 then
self:disconnect(0x81)
2024-09-05 22:31:26 +02:00
return "invalid flags"
2024-09-05 14:52:59 +02:00
end
if reason > 127 then
self.is_connecting = false
self.is_connected = false
self.conn:close()
self.conn = nil
return "connection closed by server"
elseif reason ~= 0 then
self:disconnect(0x81)
2024-09-05 22:31:26 +02:00
return "unrecognized reason"
2024-09-05 14:52:59 +02:00
end
self.is_connecting = false
self.is_connected = true
2024-09-05 21:45:14 +02:00
elseif ptype & 0xF0 == 0x40 then -- PUBACK
2024-09-05 14:52:59 +02:00
-- TODO
2024-09-09 01:19:47 +02:00
elseif ptype & 0xF0 == 0x90 then -- SUBACK
-- TODO
2024-09-05 21:45:14 +02:00
elseif ptype & 0xF0 == 0xD0 then -- PINGRESP
2024-09-05 14:52:59 +02:00
-- TODO
2024-09-09 01:19:47 +02:00
elseif ptype & 0xF0 == 0x30 then -- PUBLISH
local topic, _, next = string.unpack("> s2 B", data)
local message = string.sub(data, next)
computer.pushSignal("mqtt_message", topic, message)
2024-09-05 21:45:14 +02:00
elseif ptype & 0xF0 == 0xE0 then -- DISCONNECT
if ptype ~= 0xE0 then
self:disconnect(0x81)
return "malformed packet"
end
self.is_connecting = false
self.is_connected = false
if length > 0 then
reason, _ = string.unpack("B", data)
if reason ~= 0 then
return "disconnect with error"
end
end
return "disconnect"
2024-09-05 14:52:59 +02:00
end
return nil
2024-09-05 01:20:57 +02:00
end
function MqttClient:connect (username, password)
2024-09-05 14:52:59 +02:00
if self.is_connecting or self.is_connected then
return "already connected"
2024-09-05 14:52:59 +02:00
end
2024-09-05 17:52:07 +02:00
local length = 13
local flags = 2
2024-09-05 14:52:59 +02:00
if username ~= nil then
length = length + 2 + #username
flags = flags | 0x80
end
if password ~= nil then
length = length + 2 + #password
flags = flags | 0x40
end
local data = string.char(0x10) .. encodeVarint(length) .. string.pack("> s2 B B I2 B s2", "MQTT", 5, flags, 0, 0, "")
2024-09-05 14:52:59 +02:00
if username ~= nil then
data = data .. string.pack("> s2", username)
end
if password ~= nil then
data = data .. string.pack("> s2", password)
end
local _, err = self.conn:write(data)
if err ~= nil then
return err
end
2024-09-05 16:59:16 +02:00
local _, err = self.conn:flush()
if err ~= nil then
return err
end
2024-09-05 14:52:59 +02:00
self.is_connecting = true
return nil
2024-09-05 01:20:57 +02:00
end
2024-09-05 22:02:45 +02:00
function MqttClient:publish (topic, payload)
if not (self.is_connecting or self.is_connected) then
return "no connection"
end
if not topic or #topic == 0 then
return "topic is required"
end
if not payload then
payload = ""
end
local flags = 0
local length = 3 + #topic + #payload
local data = string.char(0x30 | flags) .. encodeVarint(length) .. string.pack("> s2 B", topic, 0) .. payload
local _, err = self.conn:write(data)
if err ~= nil then
return err
end
local _, err = self.conn:flush()
if err ~= nil then
return err
end
return nil
end
2024-09-09 01:19:47 +02:00
function MqttClient:subscribe (...)
if not (self.is_connecting or self.is_connected) then
return "no connection"
end
local count = select('#', ...)
if count == 0 then
return "topic is required"
end
local packet_id = 1
local data = string.pack("> I2 B", packet_id, 0)
for i = 1, count do
data = data .. string.pack("> s2 B", select(i, ...), 0)
end
local header = string.char(0x82) .. encodeVarint(#data)
local _, err = self.conn:write(header .. data)
if err ~= nil then
return err
end
local _, err = self.conn:flush()
if err ~= nil then
return err
end
return nil
end
2024-09-05 01:20:57 +02:00
function MqttClient:disconnect (reason)
2024-09-05 14:52:59 +02:00
if not (self.is_connecting or self.is_connected) then
return "no connection"
2024-09-05 14:52:59 +02:00
end
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
if reason == nil then
reason = 0
end
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
local data = string.pack("> B B B", 0xE0, 1, reason)
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
local _, err = self.conn:write(data)
if err ~= nil then
return err
end
2024-09-05 01:20:57 +02:00
2024-09-05 16:59:16 +02:00
local _, err = self.conn:flush()
if err ~= nil then
return err
end
2024-09-05 14:52:59 +02:00
self.is_connecting = false
self.is_connected = false
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
self.conn:close()
self.conn = nil
2024-09-05 01:20:57 +02:00
2024-09-05 14:52:59 +02:00
return nil
2024-09-05 01:20:57 +02:00
end
2024-09-05 02:33:40 +02:00
2024-09-09 01:19:47 +02:00
return mqtt