diff --git a/ingester/gateway/mbus_mqtt_gebos.lua b/ingester/gateway/mbus_mqtt_gebos.lua new file mode 100644 index 0000000..854cac2 --- /dev/null +++ b/ingester/gateway/mbus_mqtt_gebos.lua @@ -0,0 +1,1019 @@ +ver="1.8" +----- CONFIGURATION ----- +-------- NB-IoT --------- +APN = "auto" -- APN for the SIM card +PLMNID = 0 -- PLMNID for the SIM card +reinitOnWakeGather = 1 -- 1 for reinitialization of the NB Iot module before gathering +--------- MQTT --------- +mqtt_server = "ingest.gebos.online" -- broker +mqtt_user_base = "acrcv-" -- client name template +mqtt_client_name = nil -- client name - set to some value, otherwise will be mqtt_user_base .. imsi +mqtt_topic_base = "acrios/" -- topic template, will add IMSI/ .. topic name +mqtt_user = "bender" -- user name (for login) +mqtt_password = "hancock" -- password (for login) +mqtt_port = 8883 -- port (8883 using TLS/SSL) +mqtt_mode = "auto" -- "TCP" or "SSL" or autodetection based on port (8883 for SSL, else TCP) +mqtt_timeout = 20000 -- timeout used for connecting and waiting for subscription +reportScheduled = true -- true for reporting settings to the MQTT. List of days on which gathering occurs +willMessage = true -- true to enable MQTT disconnect message +atDebug = true -- true for debugging AT commands +------- Filtering ------- +allowedDevTypes = {} -- empty means no filter +allowedManIDs = {} +allowedIDs = {} +sontexIDs = {} -- example : {"27632659", "28725033", "27718226"} +sontexIDsFreqOffs = {} +------- Start-up -------- +nextWR = 0 -- 1 for starting with gathering, 0 for starting with beacon +------- Gathering timming ------- +-- scheduled -- +workdayOnly = 0 -- true for workdays only, false for all days +numberOfDays = 31 -- number of days/workdays since end of month +startHour = 0 -- hour of start of the readout +randomizeSeconds = 7200 -- up to 7200s~2hrs of delayed start since startHour +-- periodic -- +pDays = 0 -- period of days for gathering data +pHrs = 0 -- period of hours for gathering data +pMins = 15 -- period of minutes for gathering data +---- Mode switching ----- +mtbl = { + S1 = 120, -- S1 mode + T1 = 120, -- T1/C1 mode + M = 120, -- Metra mode + --SENSUS434 = 0, -- Sensus T1 433 MHz + --BUP433 = 0, -- Bubble-up 433 MHzW + --BUP868 = 0 -- Bubble-up 868MHz +} +------- Timeouts -------- +spe = 1 -- *10minutes time per device timeout +ift = 10 -- interframe timeout in minutes +beaconT = 12*4 -- sending beacon interval (12*4*15min = 12h) +--------- Other --------- +noComWdg = 20*24*3600*1000 -- no communication watchdog - resets the device if no communication is received in the specified time (20 days in milliseconds) +SCmV = 3150 -- minimum mV on SC before sending/publishing the message +--- CONFIGURATION END --- + +lGTs = 0 +lBTs = 0 +startTs = 0 +tauTs = 0 +schDays = "" +insIDdataT = -1 +tb = api.table +mtC = 5 --max count of gathering batch before sending to mqtt +daysList = {} +daysListMonth = -1 + +-- MQTT functions +function MQTT_SSL_Start() + local function wSc(mV) --waiting for the SC to be charged + local vchkTs = gt(1) + while true do + local volt = api.getBatteryVoltage() + p("V =", volt, "mV") + if volt > mV or ((gt(1) - vchkTs) > 120000) then + break + end + ex("_sleep", 10) + end + end + + if mqtt_mode ~= "SSL" and mqtt_mode ~= "TCP" then + if mqtt_port == 8883 then + mqtt_mode = "SSL" + else + mqtt_mode = "TCP" + end + end + + wSc(SCmV) + at("AT+QCPMUCFG=1,2") + at('AT+CEDRXS=2,5,"1111"') + at('AT+QCPTWEDRXS=2,5,"0000","1111"') + + if mqtt_mode == "SSL" then + at("AT+CSSLCFG=\"sslversion\",0,4") + end + if mqtt_mode == "SSL" and (mqtt_user == nil or mqtt_user == "") then + at("AT+CSSLCFG=\"authmode\",0,0") + else + at("AT+CSSLCFG=\"authmode\",0,1") + end + + if mqtt_client_name == nil then + mqtt_client_name = mqtt_user_base .. getIMSI() + end + at("AT+CMQTTSTART", nil, nil, "+CMQTTSTART: 0") + if mqtt_mode ~= "SSL" then + at('AT+CMQTTACCQ=0,"'..mqtt_client_name.. '",0') -- ,0 for plain + else + at('AT+CMQTTACCQ=0,"'..mqtt_client_name.. '",1') -- ,1 for SSL/TLS + at('AT+CMQTTSSLCFG=0,0') + end + + if willMessage then + local will_topic = mqtt_topic_base .. "status/disconnect" + local will_message = "Device ".. getIMSI() .." has disconnected" + local qos = 1 + MQTT_publish_single(will_topic, will_message, qos, 0, true) + end + + if mqtt_user ~= nil and mqtt_user ~= "" then + at('AT+CMQTTCONNECT=0,"tcp://'.. mqtt_server..":".. mqtt_port ..'",150,1,"'..mqtt_user..'","'..mqtt_password..'"', mqtt_timeout, 0, "+CMQTTCONNECT: 0,0|+CMQTTCONNECT: 0,23", "ERROR|+CMQTTCONNECT: 0,") + else + at('AT+CMQTTCONNECT=0,"tcp://'.. mqtt_server..":".. mqtt_port ..'",150,1', mqtt_timeout, 0, "+CMQTTCONNECT: 0,0|+CMQTTCONNECT: 0,23", "ERROR|+CMQTTCONNECT: 0,") + end +end + +function MQTT_abortIfNotConnected() + local function MQTT_checkConnected() + if at("AT+CMQTTDISC?", nil, nil, "+CMQTTDISC: 0,0", "+CMQTTDISC: 0,") >= 0 then + return true + end + return false + end + + if not MQTT_checkConnected() then + MQTT_stop() + MQTT_SSL_Start() + if not MQTT_checkConnected() then + return true + end + end + return false +end + +function MQTT_subscribe(topic) + local agn = 3 + while agn > 0 do + local res = at("AT+CMQTTSUB=0,"..(#topic)..",2", 10000, 0, "$>") + if res >= 0 then + res = at(topic, nil, nil, "+CMQTTSUB: 0,0", nil, nil, nil, 0, 1) + if res >= 0 then + return true + end + end + if MQTT_abortIfNotConnected() then + return false + end + agn = agn - 1 + end + + return res +end + +function MQTT_stop() + at("AT+CMQTTDISC=0,60") + at("AT+CMQTTREL=0") + at("AT+CMQTTSTOP") + at("AT+CPSMS=1;+QCPMUCFG=1,4", nil, nil, nil, nil, nil, nil, nil, 1) +end + +js = nil +function json(key, value, clear) + if js == nil or clear then + js = "{" + else + js = js .. "," + end + js = js .. '"' .. tostring(key) .. '":"' .. tostring(value) .. '"' +end + + +function MQTT_publishJson(topic) + js = js .. "}" + MQTT_publish_single(topic, js, 1, 1) + js = nil +end + +imsi = nil +function getIMSI() return imsi or (string.gmatch(api.nbAT("IMSI?", 5000), "([0-9]+)")() or "000000000000000") end +function gt(state) return api.getTick(state) end +function ns() return api.getTick(2) end + +p=print +rxf=api.wmbusReceiveFrame +wf=api.wmbusFilter +sv=api.setVar +g=api.getVar +pp=pack.pack +pu=pack.unpack +sb=string.byte +ex=api.exec + +function sc(...) api.wmbusSetup("SONTEX_CONFIG", ...) end +function at(...) api.delayms(100) local res _,res = api.nbAT(...) return res end +function err(...) return ex("_get_last_error", ...) end +function isInArray(el, arr) for _, v in ipairs(arr) do if el == v then return true end end return false end +function wsm(m) p("mode = ", m) api.wmbusSetup("MODE", m) end +function b(d) return pp("b", d) end +bb=bit.band +function s(p, v) sv(p, v, true) end + +function switchMode() + if n - modeSwTs >= modeSwMs then + modeSwTs = n + local oldMode, mks = curMode, {} + + for k in pairs(mtbl) do mks[#mks + 1] = k end + + for i = curMode, #mks + curMode do + local idx = (i % #mks) + 1 + if mtbl[mks[idx]] > 0 then curMode = idx break end + end + modeSwMs = 1000*mtbl[mks[curMode]] + + if oldMode ~= curMode then + local oldName = oldMode == -1 and "" or mks[oldMode] + p(oldName," -> ",mks[curMode] .. " mode") + wsm(mks[curMode]) + end + end + + return math.min(60000,modeSwMs) +end + +function onNbTAU() + p("TAU timer reporting to network...") + tauTs = ns() +end + +function onWake () + + local function bl(rc,rm,ri,rv,rd,rci,raw,rssi,rft,rtt) + p(rc,rm,ri,rv,rd,rci, "", rssi, rft, rtt) + if rm == 0x5068 and (rd == 0x08 or rd == 0x37 or rft == "C") then + return false + end + if rm == 0x4493 then + if rci ~= 0x7A then + return false + elseif rd == 0x08 and #raw < 45 then + return false + end + end + if rm == 0x2C2D and rci == 0x8d and rd == 0x04 then + if #raw < 100 or #raw > 200 then + return false + end + end + + if #allowedDevTypes ~= 0 and not isInArray(rd, allowedDevTypes) then + return false + end + + if #allowedManIDs ~= 0 and not isInArray(rm, allowedManIDs) then + return false + end + + if #allowedIDs ~= 0 and not isInArray(ri, allowedIDs) then + return false + end + + return true + end + + api.getTick(0,1) + + local reas,ser,nb=ex("_get_wake_info") + local ye,mo,day,ho,mi,se = api.getTimeDate() + p("onWake(), reason =", reas) + p("Uptime =",ns()-startTs, "sec") + p("serial =", ser) + p("NB-IoT = ", nb) + p("time =", ye,mo,day,ho,mi,se) + if reas ~= "timer" then + nextWR = 0 + end + if nextWR == 0 then + p("BEACON", lBTs, ns()) + lBTs = ns() + else + p("GATHER", lGTs, ns()) + lGTs = ns() + if reinitOnWakeGather == 1 then + api.nbAT("REINITIALIZE") + end + end + + if lGTs <= 0 then + lGTs = ns() + end + + if lBTs <= 0 then + lBTs = ns() + end + + local function init() + local function sumModes() + local ts = 0 + for _, v in pairs(mtbl) do ts = ts + v end + return ts + end + start = gt(0) + + maxTimeout = spe*10*60000 + + T = 1000*sumModes() + if maxTimeout < T then + maxTimeout = T + end + curMode = -1 + modeSwMs = 0 + modeSwTs = gt(0) + lastTs = gt(0) + n = gt(0) + if T > 0 then + switchMode() + rxedAll = false + else + rxedAll = true + end + tb("clear", 1) + end + + local function sendBatch(mtC) + local function modeToStr(inp) + return ({"F", "C", "T", "S", "T2S", "ERR", "SONTEX433", "M", "BUP"})[inp] + end + + local function sendSingleMeter(ri, rft, rssi, rd, raw) + + local function binToHex(str) + local ret = api.dumpArray(str, "return") + return ret == nil and "" or ret + end + + if rft ~= "M" then + rihex = string.format("%08X",ri) + else -- for metra, send as shown on the device + if not ((rd ~= 9) and (#raw == 22 or #raw == 20)) then + rihex = string.format("0101-%08d", ri) + else + rihex = string.format("%d", ri) + end + end + local tn = mqtt_topic_base .. "meter/" .. rihex .. "/raw" + p("Publishing " .. rihex .." data...") + json("ID", rihex, 1) + json("mode", rft) + json("RSSI", rssi) + json("raw-data", binToHex(raw)) + MQTT_publishJson(tn) + end + + for idx = 1, mtC do + p("Sending Idx :" .. tostring(idx)) + local rft = modeToStr(tonumber(tb("get-index", 5, idx))) -- mode to string + sendSingleMeter(tb("get-index", 6, idx),rft , tb("get-index", 4, idx), + tb("get-index", 3, idx), tb("get-index", 2, idx)) + end + + for i = 2, 6 do + tb("clear", i) + end + insIDdataT = 0 + end + + init() + + local function insertToDataTable(ri, rft, rssi, rd, raw) + + local function modeToNum(input) + for i, v in ipairs({"F", "C", "T", "S", "T2S", "ERR", "SONTEX433", "M", "BUP"}) do if v == input then return i end end + end + + p("Adding ID to Table :" .. ri) + tb("insert", 6, ri) + local rn = modeToNum(rft) -- mode to number + tb("insert", 5, rn) + tb("insert", 4, tonumber(rssi)) + tb("insert", 3, tonumber(rd)) + tb("insert", 2, raw) + insIDdataT = insIDdataT + 1 + p("Current Idx " .. tostring(insIDdataT)) + end + + while true do + while true do + + n = gt(0) + + -- handle overflow + if n < start or n < modeSwTs or n < lastTs then + modeSwTs = n + start = n + lastTs = n + end + local iftEnd = (n - lastTs) > (60000*ift) + local gtout = (n - start) > maxTimeout + if gtout or (iftEnd and ift ~= 0) or nextWR == 0 or rxedAll then + if iftEnd and ift ~= 0 then + p("Inter-frame timeout!") + elseif gtout then + p("Timeout!") + end + + sendBatch(insIDdataT) -- if zero, will just return + rcntr = bb(g(2)+1) + sv(2, rcntr) + + noSl = 0 + if nextWR == 1 then + local function sendGatherReport(count, rcntr) + local tn = mqtt_topic_base .. "gathering-report" + local y,M,d,h,m,sec = api.getTimeDate() + json("meter-count", count, 1) + json("relative-message-counter", rcntr) + json("time", string.format("%04d-%02d-%02dT%02d:%02d:%02d", y,M,d,h,m,sec)) + + if ex("_hw_features", "CC:1") or false then + local res1,res2,res3,res4,res5,res6,res7,res8 = api.exec("_cc") + json("consumed-energy", res2) + json("input-voltage", res4) + json("system-voltage", res6) + json("temperature", res8) + end + MQTT_publishJson(tn) + end + + local sontexSuccess = 0 + if #sontexIDs ~= 0 then + wsm("SONTEX433") + local r, offs + for k,v in pairs(sontexIDs) do + r = 0 + if sontexIDsFreqOffs[v] == nil then + r, offs = api.wmbusSetup("SONTEX_TUNE", v) + if offs == nil then + r = 1 + end + p(v, offs, "SAVED") + sontexIDsFreqOffs[v] = offs + else + r = 1 + offs = sontexIDsFreqOffs[v] + p(v, offs, "LOADED") + end + api.wmbusSetup("FREQ_OFFSET", offs) + if r ~= 0 then + sc(10,8,650,100) + r, _, rm, ri, _, _, _, _, raw, _, rssi,rft = rxf(60000, v) + if raw ~= nil then + sontexSuccess = sontexSuccess + 1 + insertToDataTable(tonumber("0x"..tostring(ri)), rft, rssi, 0, raw) + if insIDdataT >= mtC or ex("_free_mem") < 8500 then + sendBatch(insIDdataT) + end + end + end + end + api.wmbusSetup("FREQ_OFFSET", 0) + sendBatch(insIDdataT) + end + + sendGatherReport(tb("used", 1)+sontexSuccess, rcntr) + else -- beacon + + local function sendBeacon(rcntr, reason, uptime, sinceTau, lastGatherTs, lastBeaconTs, version) + local ans, res = api.nbAT("AT+CSQ", 6000, nil, "+CSQ:", "99", 3) + local tn = mqtt_topic_base .. "beacon" + local y,M,d,h,m,sec = api.getTimeDate() + json("signal", tonumber(string.match(ans, "+CSQ: ([0-9]+),%d.*")) or 0, 1) + json("relative-message-counter", rcntr) + json("time", string.format("%04d-%02d-%02dT%02d:%02d:%02d", y,M,d,h,m,sec)) + json("wake-up-reason", reason) + json("uptime-seconds", uptime) + json("power-mV", api.getBatteryVoltage()) + json("lua-version", version) + MQTT_publishJson(tn) + + + local function MQTT_receive() + MQTT_subscribe(mqtt_topic_base.."up/#") + + local function MQTT_waitForRetainedMessage(timeout) + local payLen + local topLen + local topic + local ansbuf,res = api.nbAT(" ", timeout, nil, "+CMQTTRXTOPIC: 0,", nil, 0, nil, 0, 1) + if res >= 0 then + for topLenStr in string.gmatch(ansbuf, "+CMQTTRXTOPIC: 0,([0-9]+)") do + topLen = tonumber(topLenStr) + end + if topLen ~= nil then + topic,res = api.nbAT(" ", 1000, nil, nil, nil, -2, nil, 0, nil) + topic = topic:gsub("%s+","") + if #topic == topLen then + ansbuf,res = api.nbAT(" ", 1000, nil, "+CMQTTRXPAYLOAD: 0,", nil, -3, nil, 0, nil) + if res >= 0 then + for payLenStr in string.gmatch(ansbuf, "+CMQTTRXPAYLOAD: 0,([0-9]+)") do + payLen = tonumber(payLenStr) + end + if payLen ~= nil then + payload,res = api.nbAT(" ", 2000, nil, nil, nil, -2, nil, 0, 1, payLen) -- fixed amount + if #payload == payLen then + return topic, payload + end + + end + end + return topic, nil + end + end + end + return nil, nil + end + + local top, pay = MQTT_waitForRetainedMessage(mqtt_timeout) + print(top,pay) + if top == nil then + p("Nothing received") + elseif top:find("config") then + if compare_json_store(pay) then + MQTT_publish_single(top, "", 1, 1) -- clear + MQTT_publish_single(mqtt_topic_base.."config", pay, 1, 1) -- send back + end + elseif top:find("action") then + if pay ~= nil and #pay > 1 then + function clearAction() + MQTT_publish_single(extop, "", 1, 1) + end + + local func, err = loadstring(pay) + if func then + extop = top + func() + extop = nil + else + p("Error executing: ", err) + end + func = nil + end + else + p("Unknown topic ", top, " dropping...") + end + + if top ~= nil then + ex("clearSwWdogNoCom") + end + end + + + MQTT_receive() + end + sendBeacon(rcntr, reas, ns()-startTs, ns()-tauTs, lGTs, lBTs, ver) + end + + insec = pDays*86400+pHrs*3600+pMins*60 - (ns() - lGTs) + insecb = beaconT*15*60 - (ns()-lBTs) + + local function checkScheduledGather(nextbeaconinsec, manualgather) + if manualgather == 1 then + return + end + if numberOfDays == 0 then + return + end + local y,M,d,h,m,s = api.getTimeDate() + + local function get_days_in_month(month, year) + local function is_leap_year(year) + return year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0) + end + return month == 2 and is_leap_year(year) and 29 or ("\31\28\31\30\31\30\31\31\30\31\30\31"):byte(month) + end + + local function get_day_of_week(dd, mm, yy) + local mmx = mm + + if (mm == 1) then mmx = 13; yy = yy-1 end + if (mm == 2) then mmx = 14; yy = yy-1 end + + local val8 = dd + (mmx*2) + math.floor(((mmx+1)*3)/5) + yy + math.floor(yy/4) - math.floor(yy/100) + math.floor(yy/400) + 2 + local val9 = math.floor(val8/7) + local dw = val8-(val9*7) + + if (dw == 0) then + dw = 7 + end + + return dw + end + + local function month2ts(d, h, m, s) + return (s + 60*(m + 60*(h + 24*d))) + end + + if #daysList == 0 then + -- this will sync the time + api.nbAT("TIME_SYNC",1) + + y,M,d,h,m,s = api.getTimeDate() + + local gM = M + local gy = y + local tdc = get_days_in_month(gM, gy) + if (h > startHour or (h == startHour and m > 0)) and d == tdc then + -- last day of month and AFTER sampling hour -> generate for next month + gM = gM + 1 + if gM >= 13 then + gM = 1 + gy = gy + 1 + end + tdc = get_days_in_month(gM, gy) + end + local dl = {} + for di = 1, tdc do + if workdayOnly then + local work_days = {2, 3, 4, 5, 6} + if isInArray(get_day_of_week(di, gM, gy), work_days) then + dl[#dl + 1] = di + p("candidate-scheduled-workdays", #dl, daysListYear, daysListMonth, di) + end + else + dl[#dl + 1] = di + p("candidate-scheduled", #dl, daysListYear, daysListMonth, di) + end + end + daysListMonth = gM + daysListYear = gy + schDays = "" + for i = 0, numberOfDays-1 do + if #dl - i > 0 then + daysList[#daysList + 1] = dl[#dl - i] + p("scheduled", #daysList, daysListYear, daysListMonth, dl[#dl - i], tostring(startHour) .. ":00") + if reportScheduled then + if schDays == "" then + schDays = dl[#dl - i] + else + schDays = schDays .. "|" .. dl[#dl - i] + end + end + end + end + if reportScheduled then + local tn = mqtt_topic_base .. "scheduled" + + p("Publishing scheduled data plan...") + json("Year", daysListYear,1) + json("Month", daysListMonth) + json("Day-list", schDays) + json("Hour", tostring(startHour) .. ":00") + MQTT_publishJson(tn) + end + end + + local diff = -1 + if y == daysListYear and M == daysListMonth then + while true do + local nowts = month2ts(d, h, m, s) + local nextts = month2ts(daysList[#daysList], startHour, 0, 0) + diff = nextts - nowts + if diff < 0 then + daysList[#daysList] = nil + if #daysList == 0 then + break + end + else + break + end + end + else + -- last day of month + if d == get_days_in_month(M, y) then + local nowts = month2ts(0, h, m, s) + local nextts = month2ts(daysList[#daysList], startHour, 0, 0) + diff = nextts - nowts + end + end + + if diff >= 0 and diff <= nextbeaconinsec then + p("consume-scheduled", #daysList, daysListYear, daysListMonth, daysList[#daysList], tostring(startHour) .. ":00") + -- remove last member of daysList + daysList[#daysList] = nil + + -- schedule + noSl = 1 + nextWR = 1 + p("Sleep before scheduled gathering...") + diff = diff + api.randInt(0, randomizeSeconds) + ex("_sleep", diff) + end + end + + checkScheduledGather(insecb, noSl) + + if noSl == 0 then + + if insec <= 10 then + insec = 10 + end + + if insecb <= 10 or insecb > 3600*48 then + insecb = 10 + end + + if insec >= insecb or (pDays == 0 and pHrs == 0 and pMins == 0) then + p("beacon next", insec, insecb) + nextWR = 0 -- next is beacon + api.wakeUpIn(0,0,0,insecb) + else + p("gather next",insec, insecb) + nextWR = 1 -- next is gather + api.wakeUpIn(0,0,0,insec) + end + MQTT_stop() + return + else -- restart gathering + init() + end + end + + _,rc,rm,ri,rv,rd,rci,_,raw,FId,rssi,rft,rtt = rxf(switchMode(), 0) + if raw ~= nil then + p("Rxed!") + break + end + + end + + if tb("index-of", 1, ri) ~= nil then + p(string.format("%08X",ri), " already rcvd.") + else + if bl(rc,rm,ri,rv,rd,rci,raw,rssi,rft,rtt) then + if tb("insert", 1, ri) == 0 then + p("Up node #", tb("used", 1),": ", string.format("%08X",ri), ", mode ", rft, ", rssi ", rssi) + else + p("just sending, memory full: ", string.format("%08X",ri), ", mode ", rft, ", rssi ", rssi) + end + lastTs = gt(0) + + insertToDataTable(ri, rft, rssi, rd, raw) + + if #allowedIDs ~= 0 and tb("used",1) >= #allowedIDs then + rxedAll = true + end + + else + p("bl drop ", string.format("%08X",ri), "devType ", string.format("%02X",rd)) + end + + if insIDdataT >= mtC or ex("_free_mem") < 8500 then + sendBatch(insIDdataT) + end + end + end +end + +function normalize_json(json) + return json:gsub('%s*:%s*',':'):gsub('%s*,%s*',','):gsub('%[%s*','['):gsub('%s*%]',']'):gsub('{%s*','{'):gsub('%s*}','}'):gsub('\n',''):gsub('\r',''):gsub('%s*$','') +end + +function parse_object(s) + local obj = {} + for k, v in s:gmatch('"(.-)"%s*:%s*(%b{})') do obj[k] = parse_object(v) end + for k, v in s:gmatch('"(.-)"%s*:%s*(%b[])') do obj[k] = parse_array(v) end + for k, v in s:gmatch('"(.-)"%s*:%s*(0x%x+)') do obj[k] = tonumber(v) end + for k, v in s:gmatch('"(.-)"%s*:%s*(%d+)') do obj[k] = tonumber(v) end + for k, v in s:gmatch('"(.-)"%s*:%s*"(.-)"') do obj[k] = v end + return obj +end + +function parse_array(s) + local arr, i, index = {}, 1, 2 + while index < #s do + local char = s:sub(index,index) + if char == '{' then + local obj_start, obj_end = s:find('%b{}', index) + arr[i], index = parse_object(s:sub(obj_start, obj_end)), obj_end + 1 + elseif char == '[' then + local arr_start, arr_end = s:find('%b[]', index) + arr[i], index = parse_array(s:sub(arr_start, arr_end)), arr_end + 1 + elseif char == '"' then + local val_start, val_end, value = s:find('"(.-)"', index) + arr[i], index = value, val_end + 1 + elseif s:sub(index, index + 1) == '0x' then + local val_start, val_end, value = s:find('(0x%x+)', index) + arr[i], index = tonumber(value), val_end + 1 + elseif char:match('[0-9]') or char == '-' then + local val_start, val_end, value = s:find('(-?%d+%.?%d*)', index) + arr[i], index = tonumber(value), val_end + 1 + else index = index + 1 end + if s:sub(index,index) == ',' then index = index + 1 end + i = i + 1 + end + return arr +end + +function parse_json(json, dry_run) + json = normalize_json(json) + local index, json_len = 1, #json + while index <= json_len do + local key_start, key_end, key = json:find('"(.-)"%s*:', index) + if not key_start then break end + index = key_end + 1 + local char = json:sub(index, index) + local result + p("parse: ", key) + p("freemem: ", ex("_free_mem")) + if char == '{' then + local obj_start, obj_end = json:find('%b{}', index) + result, index = parse_object(json:sub(obj_start, obj_end)), obj_end + 1 + elseif char == '[' then + local arr_start, arr_end = json:find('%b[]', index) + result, index = parse_array(json:sub(arr_start, arr_end)), arr_end + 1 + elseif char == '"' then + result, index = json:match('"(.-)"()', index) + elseif json:sub(index, index + 1) == '0x' then + local v = json:match('(0x%x+)()', index) + result, index = tonumber(v), index + else + local v = json:match('(-?%d+%.?%d*)()', index) + result, index = tonumber(v), index + end + + if not dry_run then _G[key] = result p(key, " <= ", result) end + end +end + + +function compare_json_store(newjson, justLoad) + local jsonCfg = "" + local cfgLen = g(16, 0) + if cfgLen ~= 0 then + jsonCfg = g(18, "bytes", cfgLen) + end + + if justLoad then pcall(parse_json, jsonCfg) return false end + + if newjson == nil then return false end + + newjson = normalize_json(newjson) + + p("old: ", jsonCfg) + p("new: ", newjson) + + if newjson ~= jsonCfg then + p("Load New!") + local ok = pcall(parse_json, newjson, true) + if ok then + s(16, #newjson) + sv(18, "bytes", newjson) + pcall(parse_json, newjson) + return true + else + p("new config not valid!") + end + end + + return false +end + +function MQTT_publish_single(path, value, qos, retained, is_lwt) + p("Publishing to: ", path) + p(string.format("Value (%d bytes): ", #value), value) + local agn = 3 + local topic_cmd = is_lwt and "AT+CMQTTWILLTOPIC=0," or "AT+CMQTTTOPIC=0," + local payload_cmd = is_lwt and "AT+CMQTTWILLMSG=0," or "AT+CMQTTPAYLOAD=0," + + if #value == 0 then + MQTT_stop() -- allows to send empty payload + if MQTT_abortIfNotConnected() then + return false + end + end + + while agn > 0 do + local res = at(topic_cmd..(#path), 5000, 0, "$>") + if res >= 0 then + res = at(path, nil, nil, nil, nil, nil, nil, 0) + if res >= 0 then + if #value > 0 then + local payload_qos = is_lwt and ","..qos or "" + res = at(payload_cmd..(#value)..payload_qos, 5000, 0, "$>") + if res >= 0 then + res = at(value, nil, nil, nil, nil, nil, nil, 0) + end + end + if not is_lwt and res >= 0 then + res = at("AT+CMQTTPUB=0,"..qos..",60,"..retained, mqtt_timeout, 0, "+CMQTTPUB: 0,0", nil, nil, nil, nil, 1) + if res >= 0 then + return true + end + elseif is_lwt and res >= 0 then + return true + end + end + end + + if MQTT_abortIfNotConnected() then + return false + end + agn = agn - 1 + end + + return false +end + + +function onStartup() + + if not ex("_hw_features", "SIM7022:1") then + p("Your hardware is not supported!") + api.ledControl(1) + while true do end + end + + local function createDataTable() + tb("new", 6, "U32", mtC) + tb("new", 5, "I8", mtC) + tb("new", 4, "U32", mtC) + tb("new", 3, "U32", mtC) + tb("new", 2, "STR", mtC) + insIDdataT = 0 + end + + local function sendHardwareInfo(version, fwma, fwmi, fwbf, modelName, source, SN) + local tn = mqtt_topic_base .. "hardware-info" + json("IMEI", api.nbAT("IMEI?"), 1) + json("IMSI", getIMSI()) + json("SN", SN) + json("model-name", modelName) + json("source", source) + json("firmware-version", fwma.."."..fwmi.."."..fwbf) + json("lua-version", version) + --json("hw-features", ex("_hw_features")) + MQTT_publishJson(tn) + end + + local function sendErrorReport() + local _, reas = ex("_reset_reason") + local sh = err("SHORT", 0, -1) + local tn = mqtt_topic_base .. "error-report" + json("reset-reason", reas, 1) + if #sh > 0 then + json("short", sh) + end + local e = ex("_get_last_error", "STDOUT_RAW",0,-1) + if #e > 0 then + json("has-serial-log", 1) + else + json("has-serial-log", 0) + end + MQTT_publishJson(tn) + + if #e > 0 then + tn = tn .. "/serial-log" + MQTT_publish_single(tn, e, 1, 1) + end + end + + + p("WMBus-NBIoT - MQTT, V"..ver) + + local sysinfo = ex("_sysinfo") + fwma = sysinfo["ver"]["major"] + fwmi = sysinfo["ver"]["minor"] + fwbf = sysinfo["ver"]["bugfix"] + p(sysinfo["ver"]["major"] ..".".. sysinfo["ver"]["minor"] .. "." .. sysinfo["ver"]["bugfix"]) + local modelName = sysinfo["model"]["name"] or "unknown" + local source = sysinfo["model"]["source"] or "unknown" + local SN = sysinfo["SN"] + lowMemory = ex("_free_mem") < 80000 + + at("APN=" .. APN) + at("PLMNID=" .. PLMNID) + + ex("setSwWdogNoComReloadValue", noComWdg) + + mqtt_topic_base = mqtt_topic_base .. getIMSI() .. '/' + MQTT_SSL_Start() + sendErrorReport() + + wf("purge") + sv(2, 0) + startTs = ns() + tauTs = ns() + tb("set-max-id", 6) + if lowMemory then + tb("new", 1, "U32", 150) + mtC = 15 + else + tb("new", 1, "U32", 800) + mtC = 50 + end + insIDdataT = -1 + + compare_json_store(nil, true) -- just load old config + + if atDebug then + at("AT_DEBUG_ON") + end + + sendHardwareInfo(ver, fwma, fwmi, fwbf, modelName, source, SN) + + MQTT_stop() + createDataTable() + onStartup = nil +end \ No newline at end of file