Files
Gebos/ingester/gateway/mbus_mqtt_gebos.lua
T
2026-07-02 17:44:24 +02:00

1019 lines
27 KiB
Lua

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