1049 lines
29 KiB
Lua
1049 lines
29 KiB
Lua
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,"<file>" 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 |