#!/usr/bin/lua -- Created by jiaqiang.ye@lnxall.com -- Implementation of 创业惠康 protocol -- 2022/01/11 -- Load external modules local uci = require 'uci' local bit = require 'bit' local posix = require 'posix' local mosq = require 'mosq' local cjson = require 'cjson' local ubus = require 'ubus' local struct = require 'struct' local invoker = require 'invoker' local cyhkp = require 'cyhkp' local cyhkthread = require 'cyhkthread' -- global protocol running information local cyhk_proto = {} -- global loop flag local cyhk_run = true -- MQTT input handler table local cyhk_cmdtab = {} -- system network configure function table local cyhk_nettab = {} -- gateway sn local g_devsn = nil local function nonzero_string(wstr) local typn = type(wstr) if typn ~= "string" then return false end if string.len(wstr) > 0 then return true end return false end -- signal handler local function handle_signal(signo) -- set loop flag to false cyhk_run = false end -- function to initialize CYHK protocol running information local function cyhk_proto_init(product_key) -- setup PATH, procd does not give us `/lib/dyiot/bin posix.setenv('PATH', '/lib/dyiot/bin:/usr/sbin:/usr/bin:/sbin:/bin', true) -- get MAC address of the gateway local devinfo, macaddr = cyhkp.factory_get(), nil if devinfo then macaddr = devinfo.MAC end if not macaddr or #macaddr == 0 then io.stderr:write("Error, cannot get the MAC address\n") io.stderr:flush() return false end -- get serial-no for gateway g_devsn = devinfo.SN if type(g_devsn) ~= "string" or string.len(g_devsn) == 0 then io.stderr:write("Error, serialno not found for gateway.\n") io.stderr:flush() return false end -- set productKey and macaddress to working thread if not cyhkthread.setpkey(product_key, macaddr) then io.stderr:write("Error, cannot set set productKey for working thread\n") return false end -- Use the upper-case version of MAC address macaddr = string.upper(macaddr) cyhk_proto.macaddr = macaddr -- construct subscription topic cyhk_proto.topic = string.format("%s/%s/in/+", product_key, macaddr) -- create ubus connection cyhk_proto.ucon = ubus.connect() if not cyhk_proto.ucon then io.stderr:write("Error, failed to connect to ubus\n") return false end -- create MQTT client cyhk_proto.mqtt = mosq.new('LNXALL_CYHK', true) -- number of times connected cyhk_proto.connected = false -- successfully registered to cloud cyhk_proto.registered = false return true end local function cyhk_proto_connected(why) cyhk_proto.connected = true io.stdout:write('MQTT broker connected: ' .. tostring(why) .. "\n") io.stdout:flush() end local function cyhk_proto_disconnect(why) cyhk_proto.connected = false cyhkthread.stop() -- stop the working thread io.stderr:write("Disconnected from MQTT broker: " .. tostring(why) .. "\n") end -- 平台回复心跳 cyhk_cmdtab[cyhkp.CMD_HEARTBEAT_RSP] = function (pmsg) io.stdout:write("Received heart-beat reply from cloud.\n") io.stdout:flush() return false end -- 平台回复入网请求 cyhk_cmdtab[cyhkp.CMD_CONNECT_RSP] = function (pmsg) if pmsg.result == 0 then cyhk_proto.registered = true print("Access to cloud has been granted!") else cyhk_proto.registered = false print("Access to cloud has been denied!") end return false end -- 平台发送NTP服务器配置 cyhk_cmdtab[cyhkp.CMD_NTPCFG] = function (pmsg) local msgs = pmsg.body if type(msgs) == "table" then msgs = msgs[1] end if type(msgs) == "table" then msgs = msgs.msg end if type(msgs) ~= "table" then return nil, "invalid config for NTP setting" end local host, port = msgs.host, msgs.port if type(host) ~= "string" or #host == 0 then return nil, "invalid NTP host ipaddr" end if type(port) ~= "number" then return nil, "invalid NTP port number" end print(string.format("New NTP Server: %s, port: %d", host, port)) local ntpcfg = uci.cursor('/etc/config') -- update NTP system configuration ntpcfg:set('system', 'ntp', 'timeserver') ntpcfg:set('system', 'ntp', 'enabled', '1') ntpcfg:set('system', 'ntp', 'enable_server', '1') ntpcfg:set('system', 'ntp', 'port', tostring(port)) ntpcfg:set('system', 'ntp', 'server', { [1] = host, } ) ntpcfg:commit('system') -- save configuration ntpcfg = nil -- reload sysntpd service. os.execute('exec /etc/init.d/sysntpd reload') return true end -- command: 131 -- function to update WAN configuration cyhk_nettab[cyhkp.MSGTYPE_NETCFG_WAN] = function (wancfg) local iptype = wancfg.ip_type local network, wan = 'network', 'wan' local netdev = cyhkp.WAN_NETDEV local waninfo = cyhk_proto.ucon:call('network.interface.wan', 'status', {}) if type(waninfo) == "table" and type(waninfo.l3_device) == "string" and string.len(waninfo.l3_device) > 0 then netdev = waninfo.l3_device end if iptype == 0 then -- dhcp -- load configuration from `/etc/config local netcfg = uci.cursor('/etc/config') netcfg:delete(network, wan) -- delete WAN configuration first netcfg:set(network, wan, 'interface') netcfg:set(network, wan, 'proto', 'dhcp') netcfg:set(network, wan, 'ifname', netdev) netcfg:set(network, wan, 'metric', '0') netcfg:set(network, wan, 'defaultroute', '1') netcfg:commit(network) -- write to /etc/config/network netcfg = nil return true, cyhkp.POST_RELOAD_NETWORK -- do reload netifd service end if iptype == 1 then -- static -- validate various static IP address settings if type(wancfg.gateway) ~= "string" or string.len(wancfg.gateway) == 0 then return nil, "invalid gateway for WAN" end if type(wancfg.ipaddr) ~= "string" or string.len(wancfg.ipaddr) == 0 then return nil, "invalid ipaddr for WAN" end if type(wancfg.netmask) ~= "string" or string.len(wancfg.netmask) == 0 then return nil, "invalid netmask for WAN" end if type(wancfg.dnss) ~= "string" or string.len(wancfg.netmask) == 0 then return nil, "invalid dnss for WAN" end local netcfg = uci.cursor('/etc/config') netcfg:delete(network, wan) -- delete previous configuration netcfg:set(network, wan, 'interface') netcfg:set(network, wan, 'proto', 'static') netcfg:set(network, wan, 'ifname', netdev) netcfg:set(network, wan, 'gateway', wancfg.gateway) netcfg:set(network, wan, 'ipaddr', wancfg.ipaddr) netcfg:set(network, wan, 'netmask', wancfg.netmask) local dnslst = {} for dns in string.gmatch(wancfg.dnss, '%S+') do dnslst[#dnslst + 1] = dns end netcfg:set(network, wan, 'dns', dnslst) netcfg:commit(network) netcfg = nil return true, cyhkp.POST_RELOAD_NETWORK -- do reload netifd service end -- PPPoE connection support if iptype == 2 then if type(wancfg.pppoeuser) ~= "string" or string.len(wancfg.pppoeuser) == 0 then return nil, "no valid PPPoE username given" end if type(wancfg.pppoepass) ~= "string" or string.len(wancfg.pppoepass) == 0 then return nil, "no valid PPPoE password given" end local netcfg = uci.cursor('/etc/config') netcfg:delete(network, wan) -- remove old configuration netcfg:set(network, wan, 'interface') netcfg:set(network, wan, 'proto', 'pppoe') netcfg:set(network, wan, 'ifname', netdev) netcfg:set(network, wan, 'username', wancfg.pppoeuser) netcfg:set(network, wan, 'password', wancfg.pppoepass) netcfg:commit(network); netcfg = nil return true, cyhkp.POST_RELOAD_NETWORK end return nil, "unknown network configuration for WAN" end -- command 131 -- function to update WIFI configuration cyhk_nettab[cyhkp.MSGTYPE_NETCFG_WIFI] = function (wcfg) local vaplist = wcfg.vap_list if type(vaplist) ~= "table" then return nil, "no WiFi configuration found" end local idx, haswwan = 0, false local cleared, noerror, errmsg = false, true, nil local wificfg = uci.cursor('/etc/config') while true do idx = idx + 1 -- only support two configurations if idx > 2 then break end local vap = vaplist[idx] if type(vap) ~= "table" then if idx == 1 then noerror, errmsg = false, "no WiFi configuration found" end break end if vap.mode ~= "ap" and vap.mode ~= 'sta' then noerror, errmsg = false, "invalid WiFi mode" break end local vapn = vap.vap_name if type(vapn) ~= "string" or string.len(vapn) == 0 then noerror, errmsg = false, "invalid WiFi vap_name" break end if type(vap.ssid) ~= "string" or string.len(vap.ssid) == 0 or string.len(vap.ssid) >= 31 then noerror, errmsg = false, "invalid WiFi ssid, too long or too short" break end if not cleared then local ifaces = {} wificfg:foreach('wireless', 'wifi-iface', function (ifa) ifaces[#ifaces + 1] = ifa[".name"] end) for _, iface in ipairs(ifaces) do wificfg:delete('wireless', iface) end ifaces = nil cleared = true -- wifi-iface has been cleared end wificfg:set('wireless', vapn, 'wifi-iface') -- radio0: the name of default `wifi-device local wdev = type(vap.device) == "string" and vap.device or 'radio0' wificfg:set('wireless', wdev, 'disabled', '0') wificfg:set('wireless', wdev, 'channel', type(vap.channel) == "string" and vap.channel or 'auto') wificfg:set('wireless', vapn, 'device', wdev) wificfg:set('wireless', vapn, 'mode', vap.mode) wificfg:set('wireless', vapn, 'ssid', vap.ssid) local haspw = type(vap.key) == "string" and string.len(vap.key) > 0 if haspw then wificfg:set('wireless', vapn, 'key', vap.key) end if vap.mode == "ap" then wificfg:set('wireless', vapn, 'network', 'lan') else -- station mode haswwan = true wificfg:set('wireless', vapn, 'network', 'wifiwan') end wificfg:set('wireless', vapn, 'encryption', haspw and "psk2" or "none") wificfg:set('wireless', vapn, 'disabled', vap.disabled and '1' or '0') end if not noerror then -- something wrong happened wificfg = nil return nil, errmsg end if haswwan then -- Add wireless wan, delete them first wificfg:delete('network', 'wifiwan') wificfg:delete('network', 'wifiwan') wificfg:set('network', 'wifiwan', 'interface') wificfg:set('network', 'wifiwan', 'proto', 'dhcp') wificfg:set('network', 'wifiwan', 'metric', '20') wificfg:commit('network') end -- write the configuration files wificfg:commit('wireless') wificfg = nil return true, cyhkp.POST_RELOAD_WIFI end -- command 131 -- function to update LAN configuration, static IP configuration only cyhk_nettab[cyhkp.MSGTYPE_NETCFG_LAN] = function (lancfg) -- check various configuration items if lancfg.ip_type ~= 1 then return nil, "invalid ip_type for LAN" end if type(lancfg.ipaddr) ~= "string" or string.len(lancfg.ipaddr) == 0 then return nil, "invalid ipaddr for LAN" end if type(lancfg.netmask) ~= "string" or string.len(lancfg.ipaddr) == 0 then return nil, "invalid netmask for LAN" end if lancfg.dhcp_server_enable ~= 0 and lancfg.dhcp_server_enable ~= 1 then return nil, "invalid dhcp_server enable option for LAN" end if type(lancfg.dhcp_start) ~= "number" or type(lancfg.dhcp_limit) ~= "number" or type(lancfg.dhcp_leasetime) ~= "number" then return nil, "invalid DHCP server options for LAN" end local netdev = nil local laninfo = cyhk_proto.ucon:call('network.interface.lan', 'status', {}) if type(laninfo) == "table" and type(laninfo.l3_device) == "string" and string.len(laninfo.l3_device) > 0 then netdev = laninfo.l3_device else return nil, "cannot get network device for LAN" end -- begin to write new configurations local netcfg = uci.cursor('/etc/config') netcfg:delete('network', 'lan') -- delete old LAN configuration first netcfg:set('network', 'lan', 'interface') netcfg:set('network', 'lan', 'type', 'bridge') netcfg:set('network', 'lan', 'ifname', netdev) netcfg:set('network', 'lan', 'proto', 'static') netcfg:set('network', 'lan', 'ipaddr', lancfg.ipaddr) netcfg:set('network', 'lan', 'netmask', lancfg.netmask) netcfg:commit('network') netcfg:delete('dhcp', 'lan') -- remove old DHCP configuration for LAN netcfg:set('dhcp', 'lan', 'dhcp') netcfg:set('dhcp', 'lan', 'interface', 'lan') netcfg:set('dhcp', 'lan', 'dhcpv6', 'server') netcfg:set('dhcp', 'lan', 'ra', 'server') netcfg:set('dhcp', 'lan', 'networkid', netdev) netcfg:set('dhcp', 'lan', 'start', tostring(lancfg.dhcp_start)) netcfg:set('dhcp', 'lan', 'limit', tostring(lancfg.dhcp_limit)) netcfg:set('dhcp', 'lan', 'leasetime', tostring(lancfg.dhcp_leasetime)) netcfg:set('dhcp', 'lan', 'force', '1') netcfg:set('dhcp', 'lan', 'ignore', '0') if lancfg.dhcp_server_enable == 0 then netcfg:set('dhcp', 'lan', 'dhcpv4', 'disabled') netcfg:set('dhcp', 'lan', 'dhcpv6', 'disabled') end netcfg:commit('dhcp') netcfg = nil if lancfg.dhcp_server_enable == 0 then invoker.invoke(invoker.CLOSEFD, '/etc/init.d/odhcpd', 'stop', 'disable') else invoker.invoke(invoker.CLOSEFD, '/etc/init.d/odhcpd', 'enable', 'start') end return true, cyhkp.POST_RELOAD_NETWORK end -- command 131 -- function to update 4G network setting cyhk_nettab[cyhkp.MSGTYPE_NETCFG_4GLTE] = function (cfg4g) if type(cfg4g.APN) ~= "string" or string.len(cfg4g.APN) == 0 then return nil, "invalid APN for 4G LTE network" end -- User name not used if type(cfg4g.password) ~= "string" or string.len(cfg4g.password) == 0 then return nil, "invalid password for 4G LTE network" end -- refer to config_manager/src/cm_handle_msg.c, function `msg_set_sim_card, -- which invokes shell script: /lib/dyiot/bin/clear_sim_pin_set_apn.sh os.execute(string.format('exec /lib/dyiot/bin/clear_sim_pin_set_apn.sh -p %s -a %s', cfg4g.password, cfg4g.APN)) return true, cyhkp.POST_RELOAD_NETWORK end -- command 131 -- function to update MQTT server setting cyhk_nettab[cyhkp.MSGTYPE_NETCFG_MQTT] = function (mcfg) if type(mcfg.host) ~= "string" or string.len(mcfg.host) == 0 then return nil, "invalid MQTT Server hostname" end if type(mcfg.port) ~= "number" then return nil, "invalid MQTT Server port" end if type(mcfg.user) ~= "string" then return nil, "invalid MQTT Server username" end if type(mcfg.pass) ~= "string" then return nil, "invalid MQTT Server password" end if not invoker.tcpcheck(mcfg.host, mcfg.port, 2500) then return nil, "failed to connect to new MQTT server" end local cafile, certfile, keyfile = mcfg["cafile"], mcfg["certfile"], mcfg["keyfile"] if type(cafile) ~= "string" then cafile = "" end if type(certfile) ~= "string" then certfile = "" end if type(keyfile) ~= "string" then keyfile = "" end -- download TLS related files if string.len(cafile) > 0 and not cyhkp.download(cafile, cyhkp.MQTTCA, 0x3, 0x5) then return nil, string.format("Error, failed to download '%s'", cafile) end if string.len(certfile) > 0 and not cyhkp.download(certfile, cyhkp.MQTTCERT, 0x3, 0x5) then return nil, string.format("Error, failed to download '%s'", certfile) end if string.len(keyfile) > 0 and not cyhkp.download(keyfile, cyhkp.MQTTKEY, 0x3, 0x5) then return nil, string.format("Error, failed to download '%s'", keyfile) end -- open /app/config/mqtt_server.json local filh = io.open(cyhkp.MQTTSVRCFG, "w") if not filh then return false end filh:write(string.format('{\n\t"host": "%s",\n\t"port": %d,\n\t"user": "%s",\n\t"pass": "%s",\n\t', mcfg.host, mcfg.port, mcfg.user, mcfg.pass)) filh:write(string.format('"cafile": "%s",\n\t"certfile": "%s",\n\t"keyfile": "%s"\n}\n', string.len(cafile) > 0 and cyhkp.MQTTCA or cafile, string.len(certfile) > 0 and cyhkp.MQTTCERT or certfile, string.len(keyfile) > 0 and cyhkp.MQTTKEY or keyfile)) filh:close(); filh = nil return true, cyhkp.POST_RECONNECT end -- 平台下发网络配置 cyhk_cmdtab[cyhkp.CMD_NETCFG] = function (pmsg) local msgs = pmsg.body if type(msgs) ~= "table" then return nil, "Error, invalid config for Network settings" end local idx, postact = 1, 0 -- iterate all network settings while msgs[idx] do local netcfg = msgs[idx] local msgnet = netcfg.msg local handler = cyhk_nettab[netcfg.msg_type] if type(msgnet) ~= "table" or not handler then io.stderr:write("Warning, unknown network type, ignored\n") else local okay, errmsg = handler(msgnet) if not okay then return nil, errmsg end if type(errmsg) == "number" and errmsg > 0 and bit.band(postact, errmsg) == 0 then postact = postact + errmsg end end idx = idx + 1 -- next network config end return true, postact end -- function to get WAN/LAN information local function wanlan_info(wanlan) -- Get WAN/LAN working status local wlinfo = cyhk_proto.ucon:call('network.interface.' .. wanlan, 'status', {}) if type(wlinfo) ~= "table" then io.stderr:write("Error, cannot get status for WAN!\n") return nil end -- construct reply message for WAN/LAN local wl_info = { proto = wlinfo.proto, status = wlinfo.up and '1' or '0' } -- store IPv4 address information local ipaddr = wlinfo["ipv4-address"] if type(ipaddr) == "table" and ipaddr[1] then ipaddr = ipaddr[1] wl_info.subnet = string.format("%s/%d", ipaddr.address, ipaddr.mask) end -- store DNS settings for WAN/LAN local dnss = wlinfo["dns-server"] if type(dnss) == "table" and type(dnss[1]) == "string" and string.len(dnss[1]) > 0 then wl_info.dnss = table.concat(dnss, " ") end -- read from /sys/class/net/{NETDEV}/... local netdev = wlinfo.l3_device if type(netdev) == "string" and posix.access(string.format("/sys/class/net/%s/address", netdev)) then local dirpath = '/sys/class/net/' .. netdev wl_info.macaddr = invoker.readfile(dirpath .. '/address', invoker.TRIMEND) wl_info.speed = invoker.readfile(dirpath .. '/speed', invoker.TRIMEND) wl_info.txbytes = invoker.readfile(dirpath .. '/statistics/tx_bytes', invoker.TRIMEND) wl_info.rxbytes = invoker.readfile(dirpath .. '/statistics/rx_bytes', invoker.TRIMEND) wl_info.txpackets = invoker.readfile(dirpath .. '/statistics/tx_packets', invoker.TRIMEND) wl_info.rxpackets = invoker.readfile(dirpath .. '/statistics/rx_packets', invoker.TRIMEND) end return wl_info end -- 平台请求网络信息 cyhk_cmdtab[cyhkp.CMD_NETSTATUS] = function (pmsg) local wan_info = wanlan_info('wan') local lan_info = wanlan_info('lan') local wcount, wifi_info = 0, {} local wifi = cyhk_proto.ucon:call('network.wireless', 'status', {}) if type(wifi) == "table" then -- iterate the radio devices for wdev, wst in pairs(wifi) do local wcfg = type(wst) == "table" and wst["config"] or nil if type(wcfg) ~= "table" then io.stderr:write("Error, cannot get config for " .. wdev .. "\n") break end -- get the radio device path local devpath = string.format('/sys/devices/%s', type(wcfg.path) == "string" and wcfg.path or "NONEXIST") if posix.access(devpath) then local wifiup = false local radinfo, first_wifi = {}, nil -- get only the first configuration if type(wst["interfaces"]) == "table" and type(wst["interfaces"][1]) == "table" and type(wst["interfaces"][1]["config"]) == "table" then wifiup = true first_wifi = wst.interfaces[1].config else local ifaces, wificfg = {}, uci.cursor('/etc/config') wificfg:foreach('wireless', 'wifi-iface', function (ifa) ifaces[#ifaces + 1] = ifa[".name"] end) first_wifi = ifaces[1] and ifaces[1] or {} wificfg = nil end radinfo["radio"] = wdev radinfo["type"] = first_wifi.mode radinfo["up"] = wifiup radinfo["channel"] = wcfg.channel radinfo["ssid"] = first_wifi.ssid radinfo["encryption"] = first_wifi.encryption wcount = wcount + 1 wifi_info[wcount] = radinfo first_wifi = nil end end end -- load 4G LTE information local lteinfo = nil local tty4g = invoker.readfile('/tmp/4g_modem_tty', invoker.TRIMEND) if tty4g and #tty4g > 0 and posix.access(tty4g) then -- 4G LTE device found lteinfo = {} lteinfo.IMEI = invoker.readfile('/tmp/simcard_imei', invoker.TRIMEND) lteinfo.IMSI = invoker.readfile('/tmp/simcard_imsi', invoker.TRIMEND) lteinfo.REG = invoker.readfile('/tmp/simcard_reg', invoker.TRIMEND) -- Get signal strength local _, sig4g = invoker.invoke(invoker.OUTPUT + invoker.CLOSEFD, 'gcom', '-s', '/etc/gcom/getstrength.gcom', '-d', tty4g) if sig4g and #sig4g > 0 then lteinfo.SIG = string.match(sig4g, 'CSQ:%s+([%d,]+)') end end -- load MQTT configuration local mqttcfg = cyhkp.load_mqtt_svrcfg() if type(mqttcfg) == "table" then -- remove sensitive fields mqttcfg.user = nil; mqttcfg.pass = nil mqttcfg.cafile = nil; mqttcfg.certfile = nil mqttcfg.keyfile = nil end local netcnt = 0 local reply = {} if wan_info then netcnt = netcnt + 1 reply[netcnt] = { msg_type = 31, msg = wan_info } -- WAN end if wcount > 0 then netcnt = netcnt + 1 reply[netcnt] = { msg_type = 32, msg = wifi_info } -- WiFi end if lan_info then netcnt = netcnt + 1 reply[netcnt] = { msg_type = 33, msg = lan_info } -- LAN end if lteinfo then netcnt = netcnt + 1 reply[netcnt] = { msg_type = 34, msg = lteinfo } -- 4G Network end if mqttcfg then netcnt = netcnt + 1 reply[netcnt] = { msg_type = 35, msg = { host = mqttcfg.host, port = mqttcfg.port, state = cyhk_proto.connected and 1 or 0, } } end return true, reply end -- 平台下发升级请求 cyhk_cmdtab[cyhkp.CMD_FWUPGRADE] = function (pmsg) local upmsg, url = nil, nil -- Get firmware URL for download if type(pmsg.body) == "table" then upmsg = pmsg.body[1] end if type(upmsg) == "table" then upmsg = upmsg.msg end if type(upmsg) == "table" then url = upmsg.url end if type(url) ~= "string" or #url == 0 then return nil, "no valid firmware download url found" end -- check firmware size, mininum: 16 bytes, maximum: 512MB local fsize = upmsg.size if type(fsize) ~= "number" or fsize <= 0x10 or fsize > 0x20000000 then return nil, "no invalid firmware size found" end -- check firmware md5sum local md5sum = upmsg.md5 if type(md5sum) ~= "string" or #md5sum ~= 32 then return nil, "no valid md5sum found for firmware" end local dlsize, errmsg = cyhkp.download(url, cyhkp.FWPATH, true, nil, fsize, md5sum) if dlsize ~= fsize then posix.unlink(cyhkp.FWPATH) if type(errmsg) ~= "string" then errmsg = "unknown download error" end return nil, errmsg end io.stdout:write("Firmware downloaded successfully!\n") io.stdout:flush() return true, cyhkp.POST_FWUPGRADE end -- 平台下发重启 cyhk_cmdtab[cyhkp.CMD_REBOOT] = function (pmsg) return true, cyhkp.POST_REBOOT end -- function to process templates_cfg.json and nodes_cfg.json local function handle_template_node(tptcfg, nodcfg) -- load templates_cfg.json configuration local odtempcfg = cyhkp.load_json_config(cyhkp.TEMPLATECFG, nil) if not odtempcfg or type(odtempcfg.template_cfg) ~= "table" then io.stderr:write("Warning, using empty templates_cfg.json!\n") odtempcfg = {}; odtempcfg.template_cfg = {} end -- load nodes_cfg.json configuration local odnodecfg = cyhkp.load_json_config(cyhkp.NODECFG, nil) if not odnodecfg or type(odnodecfg.nodes_cfg) ~= "table" then io.stderr:write("Warning, using empty nodes_cfg.json!\n") odnodecfg = {}; odnodecfg.nodes_cfg = {} end local idx = 0 -- nodes_cfg.json and templates_cfg.json are not modified: local mod_nodcfg, mod_temcfg = false, false -- add or remove items from templates_cfg.json while true do idx = idx + 1 local tcfg = tptcfg and tptcfg[idx] or nil if tcfg == nil then break end if type(tcfg) ~= "table" then return nil, "invalid type of template item" end local tempid = tcfg.template_id -- make sure that template_id is a string if type(tempid) ~= "string" or #tempid == 0 then return nil, "invalid template_id" end -- find the action for the template item local action = tcfg.action if action ~= "add" and action ~= "remove" then return nil, "invalid action for template" end -- try to find corresponding device/node-config local jdx = cyhkp.find_index(nodcfg, 'template_id', tempid) if not jdx then return nil, "no corresponding device setting found" end local ncfg = nodcfg[jdx] -- the actions must be the same, add/remove if action ~= ncfg.action then -- action cannot be change here return nil, "actions do not match in template and device settings" end local odncfg, odtcfg = odnodecfg.nodes_cfg, odtempcfg.template_cfg -- find the index for `template_id in nodes_cfg.json and templates_cfg.json local nodeIdx = cyhkp.find_index(odncfg, "template_id", tempid) local tempIdx = cyhkp.find_index(odtcfg, "template_id", tempid) if (nodeIdx and type(nodeIdx) ~= "number") or (tempIdx and type(tempIdx) ~= "number") then return nil, "Error, invalid index type for template or node items" end if action == "add" then -- remove `action field ncfg.action = nil; tcfg.action = nil -- As a workaround for BLE nodes_cfg.json configuration, -- if the node_cfg item does not have `ext_data field, -- encode itself as JSON string and insert it as `ext_data if ncfg.ext_data == nil then local extd = cjson.encode(ncfg) ncfg["ext_data"] = extd; extd = nil end mod_nodcfg, mod_temcfg = true, true if nodeIdx then odncfg[nodeIdx] = ncfg else odncfg[#odncfg + 1] = ncfg end if tempIdx then odtcfg[tempIdx] = tcfg else odtcfg[#odtcfg + 1] = tcfg end else -- remove the configuration if nodeIdx then while true do local nextcfg = odncfg[nodeIdx + 1] odncfg[nodeIdx] = nextcfg if nextcfg == nil then break end nodeIdx = nodeIdx + 1; nextcfg = nil end mod_nodcfg = true end if tempIdx then while true do local nextcfg = odtcfg[tempIdx + 1] odtcfg[tempIdx] = nextcfg if nextcfg == nil then break end tempIdx = tempIdx + 1; nextcfg = nil end mod_temcfg = true end end -- action == remove end -- while true do idx = 0 -- modify nodes_cfg.json while true do idx = idx + 1 local ncfg = nodcfg[idx] if ncfg == nil then break end if type(ncfg) ~= "table" then return nil, "invalid type of device-node item" end local action = ncfg.action if type(action) == "string" then local tempid = ncfg.template_id if type(tempid) ~= "string" then return nil, "invalid template-id for node item" end if action == "change" then -- here we only process items that has changed local jdx = cyhkp.find_index(odnodecfg.nodes_cfg, 'template_id', tempid) if not jdx then io.stderr:write("Warning, template_id not found: " .. tempid .. "\n") -- return nil, "device-node cannot be changed, template_id: " .. tempid else mod_nodcfg = true; ncfg.action = nil if ncfg.ext_data == nil then local extd = cjson.encode(ncfg) ncfg["ext_data"] = extd; extd = nil end odnodecfg.nodes_cfg[jdx] = ncfg end else -- action ~= "change" io.stdout:write(string.format("Action '%s' ignored for %s\n", action, tempid)) io.stdout:flush() end end -- type(action) == "string"; note that action might be nil, see above end -- OK, we're now done: if mod_nodcfg then cyhkp.write_json_config(cyhkp.NODECFG, odnodecfg) end if mod_temcfg then cyhkp.write_json_config(cyhkp.TEMPLATECFG, odtempcfg) end odnodecfg = nil; odtempcfg = nil -- explicitly set the tables to nil return true end -- function to process card information local function handle_cardinfo(cinfo) local scfgm = false -- load rs485 port cfg file local serialcfg = cyhkp.load_json_config(cyhkp.RS485PORTCFG) if not serialcfg or type(serialcfg["rs485_cfg"]) ~= "table" then scfgm = true serialcfg = {} -- empty RS485 serial configuration local rs485_cfg = {} rs485_cfg[1] = {}; rs485_cfg[2] = {} rs485_cfg[3] = {}; rs485_cfg[4] = {} serialcfg.rs485_cfg = rs485_cfg end for _, cardi in ipairs(cinfo) do -- check the type of elements if type(cardi) ~= "table" then return nil, "invalid card information settings" end -- check the type of `card_id local cardid, proto = cardi.card_id, cardi.protocol print("type(cardid == %s)" .. type(cardid)) print("type(cardid) == %s" .. type(proto)) if type(cardid) ~= "number" or type(proto) ~= "number" then return nil, "invalid type of card_id or protocol" end -- check again, valid values for `card_id: 0, 1, 2, 3 if cardid < 0 or cardid >= 4 or cardid ~= math.floor(cardid) then return nil, "invalid card_id, not in range" end -- check card interface local iface = cardi.interface if iface == "SERIAL" and type(cardi.serial_cfg) == "table" then scfgm = true -- need to write to cyhkp.RS485PORTCFG serialcfg.rs485_cfg[cardid + 1] = cardi.serial_cfg end -- check cfg fild local ccfg = cardi.cfg if type(ccfg) ~= "table" then return nil, string.format("cfg not given for card_id: %d", cardid) end local cfgfile, errmsg = cyhkp.cardinfo_path(cardid, proto) if not cfgfile then return nil, errmsg end cyhkp.write_json_config(cfgfile, ccfg) end if scfgm then cyhkp.write_json_config(cyhkp.RS485PORTCFG, serialcfg) end return true end -- function to process application installation local function handle_appinst(appinfo) local idx, acnt = 0, 0 while true do -- loop: install applications one-by-one idx = idx + 1 local ipkg = appinfo[idx] if ipkg == nil then break end if type(ipkg) ~= "table" then return nil, "invalid application install information" end local action = ipkg.action if action ~= "add" and action ~= "change" and action ~= "remove" then return nil, "invalid action for application" end local appn = ipkg.name -- application name if type(appn) ~= "string" or string.len(appn) == 0 then return nil, "invalid application name" end if action == "remove" then local exst, obuf = invoker.invoke(invoker.OUTPUT + invoker.TRIMEND + invoker.CLOSEFD, cyhkp.opkg, "remove", appn) if exst ~= 0 then return nil, "failed to remove package: " .. appn end if obuf and #obuf > 0 then print(obuf) end else local pkgurl = ipkg.URL if type(pkgurl) ~= "string" or #pkgurl == 0 then return nil, "URL not given for package: " .. appn end -- extract package full name: local pkgname = string.match(pkgurl, '([^/]+)$') if not pkgname or #pkgname == 0 then return nil, "invalid URL for application install" end -- get md5sum local pkgmd5 = ipkg.md5sum if type(pkgmd5) ~= "string" or #pkgmd5 ~= 32 then return nil, "invalid package md5sum given" end -- download installation package first local dlpath = "/tmp/" .. pkgname local okay, errmsg = cyhkp.download(pkgurl, dlpath, true, nil, false, pkgmd5) if not okay then return nil, errmsg end -- reinstall the package, remove first if action == "change" then local exst = invoker.invoke(invoker.NOSTDIO + invoker.CLOSEFD, cyhkp.opkg, "remove", appn) if exst ~= 0 then return nil, "failed to remove package: " .. appn end end -- invoke /bin/opkg with reinstall forced okay = invoker.invoke(invoker.CLOSEFD, cyhkp.opkg, "--force-reinstall", "install", dlpath) posix.unlink(dlpath) if okay ~= 0 then return nil, "failed to install package: " .. appn end io.stdout:write("Installed package successfully: " .. pkgname .. "\n") end acnt = acnt + 1 end if acnt == 0 then return nil, "no package removed or installed" end return true end -- 平台下发运行参数配置 cyhk_cmdtab[cyhkp.CMD_PARAMS] = function (pmsg) local msgs = pmsg.body if type(msgs) ~= "table" then return nil, "no parameters found" end local ncount = 0 local cardcfg = nil -- 插卡配置 local tempcfg = nil -- 模版配置 local devcfg = nil -- 设备修改 local appcfg = nil -- 应用安装 for _, cfgval in pairs(msgs) do if type(cfgval) ~= "table" then return nil, "invalid parameter settings" else local mtype = cfgval.msg_type if mtype == cyhkp.MSGTYPE_PARAM_CARD then if cardcfg then return nil, "Card settings given more than once" end cardcfg = cfgval.msg if type(cardcfg) ~= "table" then return nil, "invalid card settings" end elseif mtype == cyhkp.MSGTYPE_PARAM_TEMPLATE then if tempcfg then return nil, "Template settings given more than once" end tempcfg = cfgval.msg if type(tempcfg) ~= "table" then return nil, "invalid template settings" end elseif mtype == cyhkp.MSGTYPE_PARAM_DEVICE then if devcfg then return nil, "Device settings given more than once" end devcfg = cfgval.msg if type(devcfg) ~= "table" then return nil, "invalid device settings" end elseif mtype == cyhkp.MSGTYPE_PARAM_APPINST then if appcfg then return nil, "Application settings given more than once" end appcfg = cfgval.msg if type(appcfg) ~= "table" then return nil, "invalid application settings" end else return nil, "Unknown msg_type for parameter settings" end ncount = ncount + 0x1 end end if ncount == 0 then return nil, "No parameter settings found" end print("Number of configurations found: " .. tostring(ncount)) local okay, errmsg = nil, nil -- handle card configuration if cardcfg then okay, errmsg = handle_cardinfo(cardcfg) if not okay then return nil, errmsg end end -- template settings and if devcfg then okay, errmsg = handle_template_node(tempcfg, devcfg) if not okay then return nil, errmsg end end -- handle application installation if appcfg then okay, errmsg = handle_appinst(appcfg) if not okay then return nil, errmsg end end return true, cyhkp.POST_RELOAD_SERVICES end -- 数据通信 - 平台回复或平台下发数据 cyhk_cmdtab[cyhkp.CMD_DATA_REPLY] = function (pmsg) local msgs = pmsg["body"] local typn = type(msgs) if typn ~= "table" then return nil, "Invalid data body" end local idx, errs = 0, 0 while true do idx = idx + 1 local msg = msgs[idx] typn = type(msg) if typn == "table" then msg = msg["msg"] typn = type(msg) end if typn ~= "table" then break end local devsn = msg["sn"] typn = type(devsn) if typn == "string" and string.len(devsn) > 0 then local devport = "BLE" -- default BLE device local topic = string.format("ipc/%s/%s/device/%s/data/Set_Rglt", g_devsn, devport, devsn) if not cyhkthread.pubint(topic, cjson.encode(msg)) then errs = errs + 1 end end end if errs == 0 then return true end return nil, string.format("Failed to forward message, erros: %d\n", errs) end -- 平台请求 - 网关运行状态 cyhk_cmdtab[cyhkp.CMD_STATUS_REQ] = function (pmsg) local mem, cpu, temp, disk = nil, nil, nil, nil -- get memory usage and cpu load local exitst, topinfo = invoker.invoke(invoker.OUTPUT + invoker.CLOSEFD, 'top', '-n', '1', '-d', '1') if exitst == 0 and topinfo and string.len(topinfo) > 0 then local memused, memfree = string.match(topinfo, 'Mem:%s+(%d+)K used,%s+(%d+)K free') if memused and memfree then memused = tonumber(memused) memfree = tonumber(memfree) mem = math.floor(memused * 100 / (memused + memfree)) end local cpuusr, cpusys = string.match(topinfo, 'CPU:%s+(%d+)%%%s+usr%s+(%d+)%%') if cpuusr and cpusys then cpu = tonumber(cpuusr) + tonumber(cpusys) end end -- compute the average cpu temperature local temps, cpuidx = 0, 0 while true do local tfile = string.format("/sys/class/thermal/thermal_zone%d/temp", cpuidx) local tmp = invoker.readfile(tfile, invoker.TRIMEND) if not tmp or #tmp == 0 then break end tmp = tonumber(tmp) if tmp then temps = temps + tmp end cpuidx = cpuidx + 1 end if cpuidx > 0 then temp = math.floor(temps / cpuidx) end -- call `df -h` is much simpler local dfh = io.popen("exec busybox df -h") if not dfh then return nil, "failed to get disk usage" end for line in dfh:lines() do local pinfo = {} for part in string.gmatch(line, '(%S+)') do pinfo[#pinfo + 1] = part end if pinfo[6] == "/" then disk = pinfo[5] break end end -- close pipe file handle dfh:close(); dfh = nil local replyMsg = {mem = mem, cpu = cpu, temp = temp, disk = disk} return true, replyMsg end -- function to parse the CYHK MQTT message local function cyhk_proto_parse(topin, pmsg) local cmdval = string.match(topin, '/(%d+)$') if cmdval then cmdval = tonumber(cmdval) end if not cmdval then io.stderr:write("invalid topic, no command found: " .. topin .. "\n") return nil end -- check whether the command is available if not cyhk_cmdtab[cmdval] then io.stderr:write("unknown CYHK command from cloud: " .. tostring(cmdval) .. "\n") return nil end local plen = string.len(pmsg) -- check against length of CYHK magic if plen < 0x4 then return nil end -- sliently drop the message -- unpack the magics local serial_id, st_class, st_id = struct.unpack('BB>H', pmsg:sub(1, 0x4)) if serial_id ~= 100 then -- 100 -> JSON data io.stderr:write(string.format("invalid serialization ID: 0x%x\n", serial_id)) return nil end -- parse the json data, which must be present local okay, jdat = false, nil if plen > 0x4 then okay, jdat = pcall(cjson.decode, pmsg:sub(0x5)) end if not okay then io.stderr:write(tostring(jdat)) io.stderr:write("failed to decode JSON data:\n") io.stderr:write(pmsg:sub(0x5)) io.stderr:write("\n") return nil end local header = jdat.header local typn = type(header) if typn ~= "table" then io.stderr:write(string.format(string.format("invalid header type: %s\n", typn))) return nil end -- check the msgid typn = type(header.id) if typn ~= "number" and typn ~= "string" then io.stderr:write(string.format("invalid message-id, type: %s\n", typn)) return nil end local rval = {} rval.command = cmdval rval.serialid = serial_id rval.structclass = st_class rval.structid = st_id rval.id = header.id rval.result = header.result typn = type(jdat.body) if typn == "string" and string.len(jdat.body) > 0 then okay, rval.body = pcall(cjson.decode, jdat.body) if not okay then io.stderr:write("Error, invalid body string!\n") io.stderr:flush() return nil end else rval.body = {} -- empty body end -- set jdat to nil, for the good of garbage collector header.id = nil header.result = nil header = nil jdat.header = nil jdat.result = nil jdat.body = nil jdat = nil return rval end -- function to publish MQTT messages local function cyhk_publish_message(topic, tabmsg) local serial_id = 100 local struct_class = 50 local struct_id = 11582 local payload = struct.pack('BB>H', serial_id, struct_class, struct_id) if type(tabmsg) == "table" then payload = payload .. cjson.encode(tabmsg) else -- tabmsg must be a string payload = payload .. tabmsg end cyhkthread.publish(topic, payload) end -- function to handle MQTT incoming messages local function cyhk_proto_recvmsg(mid, topicin, cyhkmsg) local msg = cyhk_proto_parse(topicin, cyhkmsg) if not msg then return false end local cmdv = msg.command print("Topic: " .. topicin) print("command: " .. tostring(cmdv)) print("struct class: " .. tostring(msg.structclass)) print("struct id: " .. tostring(msg.structid)) -- We've checked that the command handler exists local rval, postact = cyhk_cmdtab[cmdv](msg) if rval == nil then if type(postact) == "string" then io.stderr:write(postact .. "\n") end -- reply with an error message, postact must be a string local tval = cyhkp.CMDRSP[cmdv] or 0 local topic = string.format('%s/%s/out/%d', cyhkp.PRODUCTKEY, cyhk_proto.macaddr, tval) tval = cyhkp.STRUCTIDS[tval] or 0 local errmsg = { ["header"] = { id = msg.id, result = 1, reason = postact, structId = tval }, ["body"] = "", } cyhk_publish_message(topic, errmsg) elseif rval then -- reply with a success reply local tval = cyhkp.CMDRSP[cmdv] or 0 local topic = string.format('%s/%s/out/%d', cyhkp.PRODUCTKEY, cyhk_proto.macaddr, tval) local body, typn = nil, type(postact) if typn == "table" then body = cjson.encode(postact) elseif typn == "string" then body = postact end tval = cyhkp.STRUCTIDS[tval] or 0 local response = { ["header"] = { id = msg.id, result = 0, structId = tval }, ["body"] = body or "", } cyhk_publish_message(topic, response) else io.stdout:write("No need to reply command: " .. tostring(cmdv) .. "\n") end if type(postact) == "number" and postact ~= 0 then if bit.band(postact, cyhkp.POST_RELOAD_WIFI) ~= 0 then invoker.invoke(invoker.CLOSEFD, 'wifi', 'reload') end if bit.band(postact, cyhkp.POST_RELOAD_NETWORK) ~= 0 then posix.nanosleep{tv_sec = 1, tv_nsec = 500000000} -- delay 1.5 second cyhk_proto.ucon:call('network', 'reload', {}) end if bit.band(postact, cyhkp.POST_RELOAD_FIREWALL) ~= 0 then invoker.invoke(invoker.CLOSEFD, '/etc/init.d/firewall', 'reload') end if bit.band(postact, cyhkp.POST_RELOAD_DNSMASQ) ~= 0 then invoker.invoke(invoker.CLOSEFD, '/etc/init.d/dnsmasq', 'reload') end if bit.band(postact, cyhkp.POST_FWUPGRADE) ~= 0 then posix.nanosleep{tv_sec = 1, tv_nsec = 500000000} -- delay 1.5 second invoker.invoke(invoker.NOSTDIO + invoker.CLOSEFD, '/sbin/sysupgrade', '-F', cyhkp.FWPATH) end if bit.band(postact, cyhkp.POST_REBOOT) ~= 0 then posix.nanosleep{tv_sec = 1, tv_nsec = 500000000} -- delay 1.5 second posix.sync() -- flush filesystem -- invoker.invoke(0, 'reboot') cyhk_proto.ucon:call('system', 'reboot', {}) end if bit.band(postact, cyhkp.POST_RELOAD_SERVICES) ~= 0 then -- need to reload a list of services cyhkp.reloadservices() end if bit.band(postact, cyhkp.POST_RECONNECT) ~= 0 then posix.nanosleep{tv_sec = 1, tv_nsec = 500000000} -- delay 1.5 second cyhk_proto.mqtt:disconnect() io.stdout:write("Old MQTT broker disconnected!\n") io.stdout:flush() end end -- destroy msg, for the good of garbage collector msg.command = nil msg.serialid = nil msg.structclass = nil msg.structid = nil msg.id = nil msg.result = nil msg.body = nil return true end -- function to connect and subscribe MQTT topic local function cyhk_proto_connect() -- load MQTT broker configuration local brkcfg = cyhkp.load_mqtt_svrcfg(nil) cyhkthread.setaddr(brkcfg.host, brkcfg.port, brkcfg.user, brkcfg.pass) -- update the configuration for working thread local mqtt = cyhk_proto.mqtt -- determine TLS related files local cafile, certfile, keyfile = brkcfg["cafile"], brkcfg["certfile"], brkcfg["keyfile"] if not nonzero_string(cafile) then cafile = nil end if not nonzero_string(certfile) then certfile = nil end if not nonzero_string(keyfile) then keyfile = nil end if cafile or certfile or keyfile then local certs = true local okay = mqtt:tls_set(cafile, nil, certfile, keyfile) if okay and not certfile and not keyfile then certs = false end if okay then okay = mqtt:tls_opts_set(certs, "tlsv1.2") end if not okay then io.stderr:write("Error, failed to set MQTT TLS options!\n") io.stderr:flush() return false end end if type(brkcfg.user) == "string" and string.len(brkcfg.user) > 0 then if not mqtt:login_set(brkcfg.user, brkcfg.pass) then io.stderr:write("Failed to set MQTT broker username/password\n") io.stderr:flush() return false end end -- register message callback function mqtt.ON_CONNECT = cyhk_proto_connected mqtt.ON_MESSAGE = cyhk_proto_recvmsg -- register disconnect callback function mqtt.ON_DISCONNECT = cyhk_proto_disconnect local retv, errmsg = mqtt:connect(brkcfg.host, brkcfg.port) if not retv then io.stderr:write("Failed to connect MQTT broker: " .. tostring(errmsg) .. "\n") io.stderr:write(string.format("MQTT broker host: %s, port: %d\n", brkcfg.host, brkcfg.port)) io.stderr:flush() return false end -- underlying TCP connection might be established later cyhk_proto.connected = true -- subscribe main topic retv = mqtt:subscribe(cyhk_proto.topic) if not retv then io.stderr:write(string.format("Error, failed to subscribe topic: %s\n", cyhk_proto.topic)) return false end local mosq_ptr = mqtt:get_handle() if type(mosq_ptr) ~= "string" then io.stderr:write("Error, unknown type of handle!\n"); io.stderr:flush() return false end -- start working thread first if not cyhkthread.start(mosq_ptr) then io.stderr:write("Error, cannot start working thread!\n") return false end return true end -- LNXALL-CYHK protocol main function. -- MQTT connection has been established, and topic subscribed. local function cyhk_proto_main() local devinfo = cyhkp.factory_get() if not devinfo then devinfo = {} end -- empty table -- construct registration message local body = { [1] = { ["msg_type"] = 1, ["msg"] = { ["hard_ver"] = devinfo.HWVER, ["firm_ver"] = devinfo.FIRMVER, ["spec_ver"] = cyhkp.VERSION, ["module_ver"] = devinfo.PRODUCT, }, } } local regmsg = { ["header"] = { id = cyhkthread.msgid(), result = 0, structId = cyhkp.STRUCTIDS[cyhkp.CMD_CONNECT] }, ["body"] = cjson.encode(body), } local regtop = string.format('%s/%s/out/%d', cyhkp.PRODUCTKEY, cyhk_proto.macaddr, cyhkp.CMD_CONNECT) local tnow, thet = invoker.uptime(), nil local count, mqtt = 0, cyhk_proto.mqtt io.stdout:write("Trying to register to CYHK cloud...\n"); io.stdout:flush() -- wait until registered to cloud while not cyhk_proto["registered"] do if count < 2 then count = count + 1 cyhk_publish_message(regtop, regmsg) end thet = invoker.uptime() -- registration timeout in 30 seconds: if (thet - tnow) >= 30 then io.stderr:write("Failed to register to cloud!\n") io.stderr:flush() return false end mqtt:loop(3000) if not cyhk_run then break end end -- free memory by settings unused variables to nil regmsg = nil; devinfo = nil; regtop = nil -- OK, now we have registered to cloud tnow = invoker.uptime() while cyhk_proto["connected"] do if not cyhk_run then break end thet = invoker.uptime() -- send hearbeat every 55 seconds if (thet - tnow) >= 55 then tnow = tnow + 55 -- update heartbeat timestamp local topic = string.format("%s/%s/out/%d", cyhkp.PRODUCTKEY, cyhk_proto.macaddr, cyhkp.CMD_HEARTBEAT) local hmsg = { ["header"] = { id = cyhkthread.msgid(), structId = cyhkp.STRUCTIDS[cyhkp.CMD_HEARTBEAT] }, ["body"] = "", } cyhk_publish_message(topic, hmsg) end -- loop for 5 seconds if not mqtt:loop(5000) then io.stderr:write("MQTT loop has failed, and don't known why\n") -- cannot break here end end end posix.chdir("/") mosq.init() -- initialize MQTT library invoker.setname('cyhkp') -- set process name posix.signal(posix.SIGINT, handle_signal) if not cyhk_proto_init(cyhkp.PRODUCTKEY) then os.exit(1) end -- set SIGCHLD handler to default, -- so that child processes have to be waited: posix.signal(posix.SIGCHLD, posix.SIG_DFL) -- connect and receive messages if cyhk_proto_connect() then cyhk_proto_main() end io.stdout:write("CYHK protocol application exits!\n") cyhkthread.stop() -- destroy MQTT connection cyhk_proto.mqtt:destroy() cyhk_proto.mqtt = nil -- destroy ubus connection cyhk_proto.ucon:close() cyhk_proto.ucon = nil os.exit(2) -- terminate script gracefully