ver="1.11" ----- 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 --------- -- DIAGNOSTIC: connect by IP to bypass carrier DNS — the modem fails with -- +CMQTTCONNECT: 0,25 (DNS error) when given the hostname. Revert to -- "ingest.gebos.online" once DNS works on this SIM/APN (fix the APN or set -- modem DNS servers); a hardcoded IP breaks silently if the host ever moves. mqtt_server = "178.104.178.108" -- broker (= ingest.gebos.online) 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 = 60000 -- timeout used for connecting and waiting for subscription; generous because the TLS handshake (~6 kB cert chain) is slow over NB-IoT 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) -- DIAGNOSTIC: dump PDP context / assigned IP / DNS before every connect -- attempt. No IP in AT+CGPADDR = no data session (wrong APN) — explains -- +CMQTTCONNECT: 0,25 (DNS) and 0,3 (socket connect fail). at("AT+CGDCONT?", 5000) at("AT+CGPADDR", 5000) at("AT+CGCONTRDP", 5000) -- DIAGNOSTIC: eUICC probe (run once, then remove). ICCID identifies the -- SIM issuer; the AT+CSIM APDUs ask the card whether it has an eUICC -- ISD-R applet and, if so, its EID. -- select ISD-R: response ending 9000 or 61xx = eUICC; 6A82 = plain SIM. -- GetEID: response contains BF3E..5A10<32 hex digits> = the EID. at("AT+CCID", 5000) at('AT+CSIM=42,"00A4040410A0000005591010FFFFFFFF8900000100"', 10000) at('AT+CSIM=22,"81E2910006BF3E035C015A"', 10000) 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" then -- authmode 0 = TLS without server-certificate verification. The modem -- has no CA certificate configured (no AT+CSSLCFG="cacert",...), so -- authmode 1 could never validate the broker's Let's Encrypt chain and -- the handshake stalled with no CONNACK. To restore verification: -- provision ISRG Root X1 on the module (AT+CCERTDOWN), point -- AT+CSSLCFG="cacert",0,"" at it, then set authmode back to 1. at("AT+CSSLCFG=\"authmode\",0,0") 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) -- DIAGNOSTIC: dump the PDP context, assigned IP and DNS servers to the -- serial log. If AT+CGPADDR shows no address (or 0.0.0.0) the SIM has no -- data path — wrong APN — and no socket/DNS can ever work. at("AT+CGDCONT?", 5000) at("AT+CGPADDR", 5000) at("AT+CGCONTRDP", 5000) 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