M-Bus using NB-IoT - MQTT
This is a documentation for devices ACR-CV-101N-M-D and ACR-CV-101N-M-EAC using MQTT to publish the readouts.
MQTT requires firmware for ACR-CV-101N-M-D and ACR-CV-101N-M-EAC using version 2.15.0 and newer.
Application is using MQTT 3.1 .
List of Items
The following is covered in the article:
- Requirements
- Base Principles
- Installation
- Initial Meter Scan
- MQTT Protocol
- Script Parameters
- Remote Configuration and Actions
Requirements
For the use of this solution, following is required:
- Converter ACR-CV-101N-M-D or ACR-CV-101N-M-EAC
- Firmware 2.15.0 or newer, please, Download it HERE
- MQTT version 3.1
- MQTT broker and server
- The following Lua script, please, Download it HERE or read through it/copy it below (note that the version in the article may not be the latest)
MQTT M-Bus Lua script
ver="1.0"
----- CONFIGURATION -----
-------- NB-IoT ---------
APN="auto" -- APN for the SIM card ("auto"=autodetection)
PLMNID=0 -- PLMNID for the SIM card (0=autodetection) Vodafone CZ=23003
reinitOnWakeGather=0 -- 1 for reinitialization of the NB Iot module before gathering (prevent strugling)
--------- MQTT ---------
mqtt_server= "test.mosquitto.org" -- broker address IP or domain name
mqtt_port=1883 -- port
mqtt_mode="TCP" -- "TCP" or "SSL" or autodetection based on port (8883 for SSL, else TCP)
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= "" -- user name (for login)
mqtt_password= "" -- password (for login)
mqtt_timeout=25000 -- timeout used for connecting and waiting for subscription
reportScheduled=true -- true for reporting settings to the MQTT. List of days on which gathering occurs
atDebug=false -- true for debugging AT commands
memDebug=false
------- Start-up --------
nextWR=0 -- 1 for starting with gathering, 0 for starting with beacon
sendErrorLog=1 -- send error log to broker after restart device
------- Gathering timming -------
-- scheduled --
workdayOnly=false -- true for workdays only, false for all days
numberOfDays=31 -- number of days/workdays since end of month, 0 for OFF , 31 all days
startHour=10 -- hour of start of the readout
startMin=0 -- min of start of the readout
randomizeSeconds=3*60 -- up to 7200s~2hrs of delayed start since startHour
-- periodic -- (not used)
pDays=0 -- period of days for gathering data, 7 days
pHrs=0 -- period of hours for gathering data
pMins=0 -- period of minutes for gathering data
------- Timeouts --------
beaconT=48 -- sending beacon (48*15min=12h) interval (n*15 minutes multiple, if zero, then ~1 minute)
--------- 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=3100 -- minimum mV on SC before sending/publishing the message
--- CONFIGURATION END ---
lGTs=0
lBTs=0
startTs=0
tauTs=0
schDays=""
daysList={}
daysListMonth=-1
pow_source="battery"
-- M-Bus specific configuration
mbus_addresses={"0"} -- Default, will be overwritten by JSON config {"mbus_addresses": ["0", "8", "3"]}
common_filters={} -- List of common VIF/DIF filters, e.g., {"0102AABB", ...} https://wiki.acrios.com/tools/M-Bus%20Filter/
mbus_primary=""
mbus_secondary=""
mbus_default_preheat=6500 -- Default preheat time
mbus_baud_rate=2400
-- M-Bus constants
SSCAN=0
SUNICAST=1
insIDdataT=-1 -- Helper Table counter
mbC=16 -- Max count of M-Bus devices (HW specific)
-- Shorts
p=print
sv=api.setVar
g=api.getVar
mSt=api.mbusState
mVDF=api.mbusVifDifFilter
dms=api.delayms
led=api.ledControl
pp=pack.pack
pu=pack.unpack
sb=string.byte
sf=string.format
ss=string.sub
sr=string.reverse
tb=api.table
ex=api.exec
bb=bit.band
tn=""
-- Table Helper Functions
function get_mem()
if memDebug then
p("freemem: ", ex("_free_mem"))
end
end
function createDataTable() -- Create Data Tables
get_mem()
tb("set-max-id",3)
lowMemory=ex("_free_mem") < 80000
if lowMemory then
p("Low memory: creating smaller tables")
mbC=5
tb("new", 2, "U32", mbC) -- ID
tb("new", 1, "STR", mbC) -- payload
else
p("High memory: creating bigger tables")
mbC=16
tb("new", 2, "U32", mbC) -- ID
tb("new", 1, "STR", mbC) -- payload
end
get_mem()
insIDdataT=0
end
function clearDataTable(save)
if save then
for idx=1, insIDdataT do
local id=tb("get-index", 2, idx)
addToMbusAddresses(tostring(id))
end
end
p("Clearing Data Tables")
tb("clear", 1)
tb("clear", 2)
insIDdataT=0
get_mem()
end
function addToTable(id,payload)
p("Adding to Data Tables",id,payload)
tb("insert", 2, id)
tb("insert", 1, payload)
insIDdataT=insIDdataT + 1
get_mem()
end
function prepareDataTable(tn,save) -- Prepare JSON data for Publishing (topic)
for idx=1, insIDdataT do
local payload=tb("get-index", 1, idx)
local id=tb("get-index", 2, idx)
local utime=ex("_get_unix_time")
json("ID", id, 1)
json("send-time",utime)
json("raw-data", payload)
MQTT_publishJson(tn .. id .. "/raw")
end
clearDataTable(save)
end
-- M-Bus Helper Functions
function binToHex(str)
local ret=api.dumpArray(str, "return")
return ret==nil and "" or ret
end
function hexToBin(h)
local r=""
for i=1, #h, 2 do
r=r .. string.char(tonumber(h:sub(i, i+1), 16))
end
return r
end
-- LED Signaling Helper
function ledBlink(count,ts,te)
count=count or 1
ts=ts or 100
te=te or 100
led(0) led(0)
for i=1,count do
led(1)
dms(ts)
led(0)
dms(te)
end
led(0) led(0)
end
function applyMbusFilter(filter)
filter=filter or ""
mVDF("purge")
mVDF("populate", hexToBin(filter))
mVDF("activate", 0)
mVDF("show")
end
function getCC() if ex("_hw_features", "CC:1") then local _, res2=ex("_cc") p("CC",res2)return res2 else return 0 end end
function mBusControl(state, baudR, parity, stopB, dataBits, preheat)
state=state or 0
baudR=baudR or mbus_baud_rate
parity=parity or 2
stopB=stopB or 1
dataBits=dataBits or 8
preheat=preheat or mbus_default_preheat
local mbh="M-Bus setup: "
local mbp="M-Bus power: "
if state==1 then
api.mbusSetup(baudR, parity, stopB, dataBits)
p(mbh,"Baud rate: "..baudR,"Parity: ".. parity,"Stop byte: ".. stopB,"Data byte: ".. dataBits)
p(mbh,"Preheat:"..tostring(preheat))
p(mbp,"ON")
p(mSt(state))
dms(preheat)
else
p(mSt(state))
p(mbp,"OFF")
end
end
function parse_mbus_frame(payload)
local p_addr=sb(payload, 6)
local id_bytes_le=ss(payload, 8, 11)
local id_bytes_be=sr(id_bytes_le)
local function bytes_to_hex(str)
local hex=""
for i=1, #str do
hex=hex .. sf("%02X", sb(str, i))
end
return hex
end
local s_addr= bytes_to_hex(id_bytes_be)
return p_addr, s_addr
end
function addToMbusAddresses(id)
id=id:gsub("%s+", "")
local addr, filter_or_index=id:match("([^:]+):(.+)")
if not addr then
addr=id
end
for i=1, #mbus_addresses do
local existing_addr=mbus_addresses[i]
local existing_base_addr=existing_addr:match("([^:]+)")
if existing_base_addr==addr or existing_addr==id then
return
end
end
mbus_addresses[#mbus_addresses + 1]=id
end
function mbusPrimary(sID,eID, filter)
tn=""
sID=sID or 0
eID=eID or sID
for id=sID,eID do
cs=id + 0x5B
cs=cs % 256
local b=pack.pack('<b5', 0x10, 0x5B, id, cs, 0x16)
if sID==eID and filter then
applyMbusFilter(filter)
end
tn=mqtt_topic_base .. "meter/"
status, c, a, ci, _, raw=api.mbusTransaction(b, 500, 1)
p("Primary=" .. tostring(id))
if status > 0 then
api.dumpArray(raw, "raw")
addToTable(id,binToHex(raw))
end
end
if sID ~= eID then led(0) led(0,0,0,0,0) mBusControl(0) p("Primary scan DONE") dms(1500) ledBlink(insIDdataT,350,850) prepareDataTable(tn,true) else p("mbusPrimary sID: ",sID) p("Primary unicast DONE") return tn end
end
function wSc(mV) --waiting for the SC to be charged
if pow_source ~= "ac" then -- only if device has battery power
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
end
function mbusSecondary(Qtype, meterID, filter)
tn=""
local function mbusSecondaryAddressing(meterID)
local idByte1=tonumber(string.sub(meterID, 1, 2), 16)
local idByte2=tonumber(string.sub(meterID, 3, 4), 16)
local idByte3=tonumber(string.sub(meterID, 5, 6), 16)
local idByte4=tonumber(string.sub(meterID, 7, 8), 16)
local selectMessageNoChecksum={0x68, 0x0B, 0x0B, 0x68, 0x53, 0xFD, 0x52, idByte4, idByte3, idByte2, idByte1, 0xFF, 0xFF, 0xFF, 0xFF}
local checksum=0
for i=5, #selectMessageNoChecksum do
checksum=checksum + selectMessageNoChecksum[i]
end
checksum=checksum % 256
local selectMessage=pack.pack("b17", selectMessageNoChecksum[1], selectMessageNoChecksum[2], selectMessageNoChecksum[3], selectMessageNoChecksum[4],
selectMessageNoChecksum[5], selectMessageNoChecksum[6], selectMessageNoChecksum[7], selectMessageNoChecksum[8],
selectMessageNoChecksum[9], selectMessageNoChecksum[10], selectMessageNoChecksum[11], selectMessageNoChecksum[12],
selectMessageNoChecksum[13], selectMessageNoChecksum[14], selectMessageNoChecksum[15], checksum, 0x16)
local res, _, _, _, ans=api.mbusTransaction(selectMessage, 1000, 1)
local statusSelect=(ans and ans:byte(1)==0xE5) and "Select successful" or "Select failed"
dms(150)
local requestMessage=pack.pack("b5", 0x10, 0x7B, 0xFD, 0x78, 0x16)
local res, c, a, ci, ans, raw=api.mbusTransaction(requestMessage, 2000, 3)
local statusRequest=(res > 0) and "Request successful" or "Request failed"
local userData=statusRequest and raw or ""
return statusSelect, statusRequest, userData
end
local function hex(n)
return string.format("%08X", n)
end
local function toHex(val)
val=val or 0
local hex=string.format("%X", val)
local len=#hex
if len < 2 then
len=2
elseif len % 2==1 then
len=len + 1
end
return "0x" .. string.format("%0" .. len .. "X", val)
end
wSc(SCmV)
if Qtype==SSCAN then
e, cnt=api.mbusScan("")
p("Found " .. tostring(cnt) .. " devices")
for j=1, cnt do
p("found id=" .. hex(e[j].identification) .. ", manid=" .. e[j].manufacturer .. ", ver=" .. e[j].version .. ", medium=" .. e[j].medium)
statusSelect, statusRequest, userData=mbusSecondaryAddressing(hex(e[j].identification))
api.dumpArray(userData, "raw")
tn=mqtt_topic_base .. "scan/secondary/"
p("Publishing meter " .. tostring(j) .. " of " .. tostring(cnt))
addToTable(toHex(e[j].manufacturer or ""),binToHex(userData or ""))
end
mBusControl(0)
p("Secondary scan DONE")
prepareDataTable(tn,nil)
elseif Qtype==SUNICAST then
applyMbusFilter(filter)
statusSelect, statusRequest, userData=mbusSecondaryAddressing(meterID)
api.dumpArray(userData, "raw")
tn=mqtt_topic_base .. "meter/"
p("Publishing data for ID: " .. meterID)
addToTable((meterID or ""),binToHex(userData or ""))
p("DONE S UNICAST")
mBusControl(0)
prepareDataTable(tn,nil)
end
end
function MQTT_checkConnected()
if at("AT+CMQTTDISC?", nil, nil, "+CMQTTDISC: 0,0", "+CMQTTDISC: 0,") >= 0 then
return true
end
return false
end
function processAddresses(addresses)
for _, addr_entry in ipairs(addresses) do
local addr, filter_or_index=addr_entry:match("([^:]+):(.+)")
if addr then
local num_addr=tonumber(addr)
local filter
if #addr < 3 and num_addr < 256 then
if filter_or_index:match("^%d+$") then
local index=tonumber(filter_or_index) + 1
filter=common_filters[index]
if not filter then
p("Error: Invalid common_filters index " .. filter_or_index .. " for address " .. addr)
end
else
filter=filter_or_index
end
mbusPrimary(num_addr,num_addr, filter)
else
if filter_or_index:match("^%d+$") then
local index=tonumber(filter_or_index) + 1
filter=common_filters[index]
if not filter then
p("Error: Invalid common_filters index " .. filter_or_index .. " for address " .. addr)
end
else
filter=filter_or_index
end
mbusSecondary(SUNICAST, addr, filter)
end
else
if #addr_entry <= 3 and tonumber(addr_entry) < 254 then
mbusPrimary(addr_entry,nil, nil)
elseif #addr_entry >= 6 then
mbusSecondary(SUNICAST, addr_entry)
end
end
end
if insIDdataT > -1 then
if not MQTT_checkConnected() then
MQTT_stop()
MQTT_SSL_Start()
end
p("sending unicast table") prepareDataTable(tn,nil) end
end
function MQTT_SSL_Start()
if mqtt_mode ~= "SSL" and mqtt_mode ~= "TCP" then
if mqtt_port==8883 then
mqtt_mode="SSL"
else
mqtt_mode="TCP"
end
end
at("AT+QCPMUCFG=1,2")
-- EDRX Activation
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
local connect_result=-1
if mqtt_user ~= nil and mqtt_user ~= "" then
connect_result=at('AT+CMQTTCONNECT=0,"tcp://'.. mqtt_server..":".. mqtt_port ..'",300,1,"'..mqtt_user..'","'..mqtt_password..'"', mqtt_timeout+10000, 0, "+CMQTTCONNECT: 0,0|+CMQTTCONNECT: 0,23", "ERROR|+CMQTTCONNECT: 0,")
else
connect_result=at('AT+CMQTTCONNECT=0,"tcp://'.. mqtt_server..":".. mqtt_port ..'",300,1', mqtt_timeout+10000, 0, "+CMQTTCONNECT: 0,0|+CMQTTCONNECT: 0,23", "ERROR|+CMQTTCONNECT: 0,")
end
if connect_result >= 0 then
p("Successfully connected to MQTT")
else
p("Failed to connected to MQTT")
end
end
function MQTT_abortIfNotConnected()
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")
p("Disconnecting from MQTT")
at('AT+CEDRXS=0') -- Disable eDRX
at('AT+QCPTWEDRXS=0') -- Disable PTW/eDRX
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
function at(...) dms(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 b(d) return pp("b", d) end
function s(p, v) sv(p, v, true) end
function onNbTAU()
p("TAU timer reporting to network...")
tauTs=ns()
end
function MQTT_receive()
MQTT_subscribe(mqtt_topic_base.."up/#")
p("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)
p(top,pay)
if top==nil then
p("Nothing received")
elseif top:find("config") then
p("New received")
p("topic:",top,"payload:",pay)
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
function onWake()
api.getTick(0,1)
ex("_sysclk", "48000000")
ex("clearSwWdogNoSleep")
ex("_print_mode","CONNECTED")
MQTT_SSL_Start()
if not MQTT_checkConnected() then
MQTT_stop()
MQTT_SSL_Start()
end
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("Consumed Energy=",getCC())
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 addSecondsToDate(i)
local y, M, d, h, m, s=api.getTimeDate()
local function l(y) return y%4==0 and(y%100~=0 or y%400==0)end
local function dm(m,y) local d={31,l(y)and 29 or 28,31,30,31,30,31,31,30,31,30,31} return d[m]end
s=s + i
while s >= 60 do s=s - 60; m=m + 1 end while s < 0 do s=s + 60; m=m - 1 end
while m >= 60 do m=m - 60; h=h + 1 end while m < 0 do m=m + 60; h=h - 1 end
while h >= 24 do h=h - 24; d=d + 1 end while h < 0 do h=h + 24; d=d - 1 end
while d > dm(M,y) do d=d - dm(M,y); M=M + 1 if M > 12 then M=M - 12; y=y + 1 end end
while d <= 0 do M=M - 1 if M <= 0 then M=M + 12; y=y - 1 end d=d + dm(M,y) end
while M > 12 do M=M - 12; y=y + 1 end while M <= 0 do M=M + 12; y=y - 1 end
return y, M, d, h, m, s
end
local function formatdt(y, M, d, h, m, sec)
return string.format("%04d-%02d-%02dT%02d:%02d:%02d", y, M, d, h, m, sec)
end
while true do
rcntr=bb(g(2)+1)
sv(2, rcntr)
noSl=0 -- flag used by scheduled gathering
insec=pDays*86400+pHrs*3600+pMins*60 - (ns() - lGTs)
insecb=beaconT*15*60 - (ns()-lBTs)
if nextWR==1 then
mBusControl(1)
processAddresses(mbus_addresses)
mBusControl(0)
else -- beacon
local function sendBeacon(rcntr, reason, uptime, sinceTau, lastGatherTs, lastBeaconTs, version)
local ans, res=api.nbAT("AT+CSQ", 6000, nil, "+CSQ:", "99", 3)
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", formatdt( y, M, d, h, m, sec))
json("next-beacon", formatdt( addSecondsToDate(lastBeaconTs)))
if #daysList > 0 then
json("next-gather", formatdt(daysListYear,daysListMonth,daysList[#daysList],startHour,(startMin),0))
end
json("wake-up-reason", reason)
json("uptime-seconds", uptime)
if pow_source ~= "ac" then
json("power-mV", api.getBatteryVoltage())
json("batt-disch-mA",getCC())
end
json("lua-version", version)
MQTT_publishJson(tn)
end
sendBeacon(rcntr, reas, ns()-startTs, ns()-tauTs, insec, insecb, ver)
MQTT_receive()
end
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
api.nbAT("TIME_SYNC",1) -- this will sync the time
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) .. ":" .. tostring(startMin))
if reportScheduled then
if schDays=="" then
schDays=dl[#dl - i]
else
schDays=schDays .. "|" .. dl[#dl - i]
end
end
end
end
if reportScheduled then
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) .. ":" .. tostring(startMin))
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, startMin, 0)
diff=nextts - nowts
if diff < 0 then
daysList[#daysList]=nil
if #daysList==0 then
break
end
else
break
end
end
else
if d==get_days_in_month(M, y) then
local nowts=month2ts(0, h, m, s)
local nextts=month2ts(daysList[#daysList], startHour, startMin, 0)
diff=nextts - nowts
end
end
if diff >= 0 and diff <= nextbeaconinsec then
p("consume-scheduled", #daysList, daysListYear, daysListMonth, daysList[#daysList], tostring(startHour) .. ":" .. tostring(startMin))
daysList[#daysList]=nil
-- schedule
noSl=1
nextWR=1
p("Sleep before scheduled gathering...")
diff=diff + api.randInt(0, randomizeSeconds)
MQTT_stop()
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 :", formatdt(addSecondsToDate(insecb)))
p("beacon next", insec, insecb)
nextWR=0 -- next is beacon
api.wakeUpIn(0,0,0,insecb)
else
p("gather next",formatdt(addSecondsToDate(insec)))
p("gather next",insec, insecb)
nextWR=1 -- next is gather
api.wakeUpIn(0,0,0,insec)
end
MQTT_stop()
return -- go to sleep...
else
mBusControl(0)
MQTT_SSL_Start()
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)
get_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
if newjson==jsonCfg then
p("Config it is a same !")
pcall(parse_json, newjson)
return true
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()
p("NBIoT - MQTT MBus , V"..ver)
if not ex("_hw_features", "SIM7022:1") then
p("Your hardware is not supported!")
led(1)
while true do end
end
if atDebug then
at("AT_DEBUG_ON")
end
ex("_sysclk", "48000000") -- speed up CPU
at("APN=" .. APN)
at("PLMNID=" .. PLMNID)
ex("setSwWdogNoComReloadValue", noComWdg)
api.nbAT("TIME_SYNC",1)
createDataTable()
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"
pow_source=sysinfo["model"]["source"] or "unknown"
local SN=sysinfo["SN"]
local function sendHardwareInfo(version, fwma, fwmi, fwbf, modelName, pow_source, SN)
local y, M, d, h, m, sec=api.getTimeDate()
tn=mqtt_topic_base .. "hardware-info"
json("time", string.format("%04d-%02d-%02dT%02d:%02d:%02d", y, M, d, h, m, sec),1)
json("IMEI", api.nbAT("IMEI?") or "")
json("IMSI", getIMSI()or "")
json("SN", SN)
json("hw-model", ex("_hw_features") or "")
json("model-name", modelName)
json("firmware-version", fwma.."."..fwmi.."."..fwbf)
json("lua-version", version)
json("source", pow_source)
if pow_source ~= "ac" then
json("consumed-mA", getCC())
end
MQTT_publishJson(tn)
end
local function sendErrorReport()
local _, reas=ex("_reset_reason")
local sh=err("SHORT", 0, -1)
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
mqtt_topic_base=mqtt_topic_base .. getIMSI() .. '/'
MQTT_SSL_Start()
if sendErrorLog==1 then sendErrorReport() end
sv(2, 0)
startTs=ns()
tauTs=ns()
compare_json_store(nil, true) -- just load old config
sendHardwareInfo(ver, fwma, fwmi, fwbf, modelName, pow_source, SN)
wSc(SCmV)
mBusControl(1)
ex("_sleep_mode",99)
led(1,0,0,0,"L123456789H987654321LLLLL", 50) -- LED Signalization M-Bus scan process
--mbusSecondary(SSCAN, nil) -- Uncomment for initial secondary scan
mbusPrimary(0,252,nil) -- Uncomment for initial primary scan
ex("_sleep_mode",0)
if atDebug then
p(mbus_primary, "mbus_primary")
end
MQTT_receive() -- subscribe configuration from broker
--MQTT_stop()
if not memDebug then
get_mem=nil
end
onStartup=nil
end
Base Principles
Communication diagram
Key principles of the converter when using MQTT
- After initialization, the converter scans for the presence of meters using primary address scanning and publishes the obtained data to the MQTT server in a topic named according to the SIM card’s IMSI.
- The converter sends the current status of the meters via messages at a configurable interval through the MQTT broker.
- The converter schedules automatic meter reading based on the configuration.
- The converter allows data collection using a random distribution of the sending interval, e.g., several minutes, to prevent broker overload.
- Additionally, the converter regularly publishes a “beacon” message, which is a short message indicating that the device is operational and connected to the network. After sending the beacon message, the device checks for any incoming configuration messages or actions scheduled on MQTT.
- This means that if you send a remote configuration, it will only be applied after the publication of a beacon message.
- It should also be noted that only one configuration or action message is applied per beacon message publication. For example, if you send two configuration messages, the first will be applied after one beacon, and the second after the next.
Boot-up and scheduled actions diagram
Pressing the button within the device wakes up the device and sends a beacon message, this does not change any scheduled readings or beacons.
The device periodically publishes a beacon message to the MQTT broker to indicate that it is operational and connected to the network. The beacon message is a short message that contains the device's status and information.
After the beacon is published, the device subscribes to the acrios/<IMSI>/up/#
topic to receive configuration and actions messages.
The device clears acrios/<IMSI>/up/config
message after applying the changes and publishes the changed parameters to the acrios/<IMSI>/config
topic.
The device executes the actions from the acrios/<IMSI>/up/action
topic. Actions can be scheduled to execute every time the device publishes the beacon message.
Device Installation
The device supports multiple devices to up to 16 UL (unit load).
Make sure the meters, if using multiple, have a different primary address or that the script is detecting them based on the secondary address.
To install the converter, follow these steps:
- Unscrew the 4 screws holding the lid and put it aside.
- If the battery is inserted inside, it should be disconnected. Put it aside.
- Insert the SIM card into the appropriate slot – see the indication on the PCB desk.
- Pass the data cable through the left cable gland and connect the communication interface wires to the MBUS+ and MBUS- terminals. The polarity of the wires does NOT matter.
- Screw the antenna onto the appropriate external connector.
- Connect the battery to the appropriate connector – see the indication on the PCB desk.
After being connected, the device performs an M-Bus scan, which starts once the bootloader has completed. The LED then blinks to indicate the number of detected M-Bus meters.
You can also verify successful device connection if you have access to an MQTT client.
Finally, a scan can be performed manually using ACRIOS GUI.
- Place the lid back on the box and screw it on – it should hold tightly and should be screwed on evenly to preserve water proof properties.
- Secure the device with screws and Mounting holes or via other means such as cable ties.
You may also see N-M-D quick start guide for further details.
Initial Meter Scan
The device executes primary or secondary scan as part of the start-up procedure - based on your preference (primary addressing is the default). This scan finds and saves all the meters. If you wish to re-initiate the device and therefore the scan, see the procedure below.
Primary Addressing
Primary address is a single number between 0-252, each representing a meter. The numbering is explained in the following table.
Address | Function |
---|---|
0 | Factory default address. |
1-250 | Addresses that can be assigned to slaves. (Primary addresses) |
251-252 | Reserved for future use. |
253 | Indicates that addressing is performed at the network layer instead. (Secondary addressing procedure) |
254 | Broadcast, meters reply with their addresses. (Causes collision, test purposes only) |
255 | Broadcast, meters do not reply. |
Note, that if there are two meters using the same address connected to a single converter, this will result in a conflict. Ensure that you connect meters with a different primary addresses to a converter (max capacity is 16 UL) if you want to use primary scan.
The secondary address
It consists of 4 parts:
- 4 bytes being the device ID (serial #)
- 2 bytes being the manufacturer’s identifier
- 1 byte being the device version
- 1 byte being the device media
The benefit of secondary addressing is that there is no need to reconfigure primary addresses, but the meters will appear in the MQTT logs under their secondary address (usually a long string of numbers and letters).
Lua Code Configuration Handling the Initial Scan
The device executes primary or secondary scan as part of the start-up procedure. This scan finds and saves all the meters. If you wish to re-initiate the device and therefore the scan as well see the procedure below.
The following code is handling the initial scan. It can be found at the end of the code in the onStartup()
function - line 972 or nearby (if code gets changed in future):
The code handling the scan can be found on line 1053 or nearby (if code gets changed in future).
Either pick primary, or secondary scan by commenting and uncommenting individual lines.
led(1,0,0,0,"L123456789H987654321LLLLL", 50) -- LED Signalization M-Bus scan process
--mbusSecondary(SSCAN, nil) -- Uncomment for initial secondary scan
mbusPrimary(0,252,nil) -- Default scan - primary addresses
Using Primary Scan
Scanning through the all primary addresses
The following configuration will scan for all the possible primary addresses.
Note that this scan will take quite a while, as scanning this many addresses is time consuming (several minutes).
mbusPrimary(0,252,nil) -- 0 is the starting address, 252 is the last address (can not be higher), the third argument must be nil
Scanning through addresses starting at 0
The following configuration will scan for the first 20 primary addresses. This is especially useful if you know the addresses beforehand and do not wish to scan for too long (this would reduce the scanning time to around 15-20 seconds).
Your meters are at primary addresses between 0 and 20 in this case. The scan now takes considerably shorter time to execute, but if there is any meter with primary address of 21 and higher, it will not be registered.
mbusPrimary(0,20,nil) -- 0 is the starting address, 20 is the last address, the third argument must be nil
Scanning through addresses starting at some number
You can also scan for any range of addresses,e.g. from primary address 50 to 80:
mbusPrimary(50,80,nil) -- 50 is the starting address, 80 is the last address, the third argument must be nil
Using Secondary Scan
If you want to use only secondary address scan upon the device start, the code should look as following.
mbusSecondary(SSCAN, nil) -- This line needs to be uncommented or added
--mbusPrimary(0,252,nil) -- Comment or delete this line
Visual LED Scan
The device first needs to go through bootloader – either after powering it up or after restart. This is indicated by LED dimly getting dimly lit up.
Bootloader finishes after couple minutes after the device is powered up (or restarted by repeated button press). You may skip the bootloader by holding the button and letting go once LED lights up.
Once the bootloader is finished or skipped, the device will do the following:
Tap the button 7x quickly in order to restart the device – this re-initializes bootloader.


- It scans for primary or secondary addresses of meters - see above.
- Sends the initial message, see MQTT Client Initial Connection
- Performs a visual M-Bus device device scan.
Here is how the visual indication looks:
- It takes a while before the scan starts after skipping/running through the bootloader
- After the bootloader finishes or is skipped, an initial scan is performed
- During the initial scan, the LED pulses for the duration of the scan, which takes several minutes
- After the scan, the LED blinks once per each meter detected
- If nothing was detected, the LED does not blink at all after the scan
MQTT Client Initial Connection
After the bootloader finishes, an initial message is sent to your MQTT client. The message contains hardware information about the converter, as well as a topic containing detected meters and their initial state.
This approach is useful because it allows you to fully inspect the state of things and whether the device has been successfully installed.
Here is an example:
MQTT Protocol
MQTT is a M2M/IoT connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging transport. It is useful for connections with remote locations where a small code footprint is required or network bandwidth is limited. The protocol allows devices to communicate with minimal overhead and low bandwidth.
MQTT Broker
An MQTT broker is a server that receives all messages from the clients and then routes the messages to the appropriate destination clients. The broker is responsible for receiving all messages, filtering the messages, deciding who is interested in them, and then publishing the message to all subscribed clients.
MQTT Client
An MQTT client is any device that runs an MQTT library and connects to an MQTT broker over a network. The client can be a publisher, a subscriber, or both. We are using MQTT Explorer as an MQTT client for demonstration purposes.
References
Here you can find useful resources regarding to use of MQTT. Please refer to them in case of need.
Script Parameters
The Lua script contains a set of parameters that define the behavior of the device. The parameters can be adjusted to adapt the device’s behavior to your needs.
Script configuration is usually necessary only if you wish to modify it afterwards. In most cases, we supply the device with a preconfigured script.
--
denotes a comment in the script.
ver="1.0"
----- CONFIGURATION -----
-------- NB-IoT ---------
APN="auto" -- APN for the SIM card ("auto"=autodetection)
PLMNID=0 -- PLMNID for the SIM card (0=autodetection) Vodafone CZ=23003
reinitOnWakeGather=0 -- 1 for reinitialization of the NB Iot module before gathering (prevent strugling)
--------- MQTT ---------
mqtt_server= "myserver.net" -- broker address IP or domain name
mqtt_port=1883 -- port
mqtt_mode="TCP" -- "TCP" or "SSL" or autodetection based on port (8883 for SSL, else TCP)
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= "userOne" -- user name (for login)
mqtt_password= "passwordOne" -- password (for login)
mqtt_timeout=25000 -- timeout used for connecting and waiting for subscription
reportScheduled=true -- true for reporting settings to the MQTT. List of days on which gathering occurs
atDebug=false -- true for debugging AT commands
memDebug=false
------- Start-up --------
nextWR=0 -- 1 for starting with gathering, 0 for starting with beacon
sendErrorLog=1 -- send error log to broker after restart device
------- Gathering timming -------
-- scheduled --
workdayOnly=false -- true for workdays only, false for all days
numberOfDays=31 -- number of days/workdays since end of month, 0 for OFF , 31 all days
startHour=10 -- hour of start of the readout
startMin=0 -- min of start of the readout
randomizeSeconds=3*60 -- up to 7200s~2hrs of delayed start since startHour
-- periodic -- (not used)
pDays=0 -- period of days for gathering data, 7 days
pHrs=0 -- period of hours for gathering data
pMins=0 -- period of minutes for gathering data
------- Timeouts --------
beaconT=48 -- sending beacon (48*15min=12h) interval (n*15 minutes multiple, if zero, then ~1 minute)
--------- 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=3100 -- minimum mV on SC before sending/publishing the message
--- CONFIGURATION END ---
lGTs=0
lBTs=0
startTs=0
tauTs=0
schDays=""
daysList={}
daysListMonth=-1
pow_source="battery"
-- M-Bus specific configuration
mbus_addresses={"0"} -- Default, will be overwritten by JSON config {"mbus_addresses": ["0", "8", "3"]}
common_filters={} -- List of common VIF/DIF filters, e.g., {"0102AABB", ...} https://wiki.acrios.com/tools/M-Bus%20Filter/
mbus_primary=""
mbus_secondary=""
mbus_default_preheat=6500 -- Default preheat time
mbus_baud_rate=2400
-- M-Bus constants
SSCAN=0
SUNICAST=1
insIDdataT=-1 -- Helper Table counter
mbC=16 -- Max count of M-Bus devices (HW specific)
Private Broker Attributes
Change the following parameters to connect the device to a broker:
mqtt_server
- URL or IP of MQTT server.mqtt_user
- Username.mqtt_password
- Password.
Example of how it could look like:
--------- MQTT ---------
mqtt_server = "myserver.net" -- 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 = "myusername" -- user name (for login)
mqtt_password = "mypassword" -- password (for login)
In order to upload the script into the device, you may consult the ACRIOS GUI guide.
Once the script is uploaded, the device subscribes to the broker.
Modifying the Script for Different SIM Card Providers
If you wish to use a SIM card coming from a different provider than Miotiq, modify the script accordingly:
A. Set these parameters to the values provided by your SIM card provider.
...
APN = "cdp.iot.t-mobile.nl" -- change this to the APN of your SIM card provider (this is an example for T-Mobile NL)
PLMNID = 20416 -- change this to the PLMNID of your SIM card provider (this is an example for T-Mobile NL)
...
B. Or set the values as following for the automatic selection.
...
APN = "auto"
PLMNID = 0
...
The automatic selection has to be supported by the SIM card provider.
Gathering
Gathering is the process of collecting data from the meters. The script allows you to configure the gathering process in this ways:
- Scheduled Gathering - gathering data at a specified time.
- Periodic Gathering - gathering data at regular intervals.
- Scheduled and Periodic Gathering - combining both scheduled and periodic gathering.
Scheduled Gathering
The scheduled gathering process is scheduled to start at a specified time - startHour
and startMin
, which can be delayed by a random time spread parameter - randomizeSeconds
. The gathering process can be set to work only on workdays or all days - workdayOnly
and on a specified number of days from the end of the month - numberOfDays
.
-- Scheduled gathering --
workdayOnly = false -- true only for workdays, false for all days
numberOfDays = 31 -- number of days/workdays from the end of the month
startHour = 23 -- hour of reading start
startMin = 55 -- minute of reading start
randomizeSeconds = 140 -- up to 7200s ~2h delay of start from startHour + startMin
The device also sends beacon at the interval of beaconT
. This interval begind at the moment the device is booted up.
------- Timeouts --------
beaconT = 48 -- beacon sending interval, where the value 1 equals 15 minutes (48*15min = 12h)
workdayOnly
This parameter changes, whether the scheduled gathering happens at the weekends.
- read out every day
workdayOnly = false -- readout every day including weekends
- read out on the work days
workdayOnly = true -- readout every work day only
numberOfDays
This parameter changes, which days of the month will the meters be read out.
Changing the numberOfDays
to a value of 31
:
- read out every day of the month (excluding weekends if
workdayOnly
istrue
)
numberOfDays = 31 -- reads out every day of the month
- read out last 3 days of the month (excluding weekends if
workdayOnly
istrue
)
numberOfDays = 3 -- reads out last 3 days of the month
- turn off scheduled gathering mode (likely when you want to use periodic mode instead)
numberOfDays = 0 -- turns off the scheduled gathering
startHour, startMin a randomizeSeconds
Sets the time when the meter is read and then adds a random value in seconds, so that data transmission from all converters does not occur at the same time – this prevents overloading the communication between the MQTT broker and the clients.
------readout will occur at 23:55 + 0-180 seconds------
startHour = 23 -- hour of the readout
startMin = 55 -- minute of the readout
randomizeSeconds = 180 -- randomly adds between 0 - 180 seconds before the redout begins
Periodic Gathering
The periodic gathering process sends data at regular intervals defined by the days - pDays
, hours - pHrs
, and minutes - pMins
parameters. Also beacon beaconT
is sent once every 12 hours in this example.
-- Periodic gathering ---
pDays = 0 -- period of days for sending data
pHrs = 24 -- period of hours for sending data
pMins = 0 -- period of minutes for sending data
------- Timeouts --------
beaconT = 48 -- sending beacon interval (4*15min = 1h)
If you want to only use the periodic gathering process, set the numberOfDays
parameter to 0
. See above.
Disable Periodic Gathering
To disable periodic gathering, set all the parameters to 0
.
-- Periodic gathering --- if all three parameters are 0, it is disabled
pDays = 0
pHrs = 0
pMins = 0
Scheduled and Periodic Gathering
You can combine both scheduled and periodic gathering processes to send data at regular intervals and specific times. After the scheduled gathering process is completed, the device will start sending data at regular intervals until the next scheduled gathering process.
Script logic
Diagram explanation:
- After powering on the device and running the bootloader – it is possible to configure the device (using GUI, locally, or remote configuration) and check its connection
- the device can also be reconfigured at any time using remote configuration
- The device publishes the first message, which includes the result of the initial scan of M-Bus meters
- This way you can easily verify that the meters are properly connected
- The device wakes up at scheduled intervals – performing the action that is planned as the next one
- this may be either reading and sending meter data or a beacon message
- The device then checks whether another action should immediately follow, and if not, it goes to sleep
- this includes setting the next wake-up time
- Next, the device wakes up and continues again from step 3.
Remote Configuration and Actions
The script supports remote device configurations via JSON files or direct action execution. In order to successfully do so, a retain message must be sent in the correct format and using appropriate topic. Also note that JSON is used to configure the device, whereas action is in a form of plain text.
Once the configuration or action is published, it appears in the up
topic, up until it is applied/executed. Once done, it is removed from this topic.
In order for the configuration or action to be applied, a beacon must be triggered. This happens either in an interval based on the script, see the Gathering section, or if triggered manually.
In order to trigger the beacon manually, unbox the device – it has to be powered up and running – and press the button.
Remote Configuration Examples
To configure the script parameters using MQTT retain messages, you can send JSON messages to the topic mqtt_topic_base .. "config"
, which the script will process and store.
The retain message ensures that the configuration remains stored on the broker and the device receives it upon connection. Below are the JSON messages for various configurations that correspond to the variables in the script.
Topic: acrios/<IMSI>/up/config
Messages are in JSON format and can contain any of the global parameters from the script. The device will apply changes after sending the beacon message. After the changes are successfully applied, the device will:
- Clear message from
acrios/<IMSI>/up/config
topic. - Publish the changed parameters to the
acrios/<IMSI>/config
topic.
Overview
- The Topic is:
acrios/<IMSI>/up/config
(replace<IMSI>
with the IMSI of the device – related to the SIM card a device is using). - The message must use a retain flag (
retained = 1
). - JSON must be valid and in format to be eligible for processing
parse_json
function. See below for the examples. - After sending the configuration message (with a retain flag), the device processes the message at the next connection and confirms its receipt by publishing to the topic
acrios/<IMSI>/config
, and removes the configuration message after its application.
Ensure that the JSON is in an appropriate format and uses a correct capitalization, e.g.: "startHour"
Example of JSON format:
{
"mqtt_server": "broker.example.com",
"mqtt_port": 1883
}
Individual JSON Configuration Examples
- Setting MQTT Parameters
{
"mqtt_server": "broker.example.com",
"mqtt_port": 1883,
"mqtt_user": "newuser",
"mqtt_password": "newpass",
"mqtt_client_name": "acrcv-custom123",
"mqtt_mode": "TCP",
"mqtt_timeout": 30000
}
- Sets new MQTT broker, port, username, password, client name, connection mode, and timeout.
- Send to:
acrios/<IMSI>/up/config
with retain flag.
M-Bus Configuration
-
Setup of M-Bus Addresses and Filters
{
"mbus_addresses": ["27:0502AABB01CC02EEFF01EE03EEFFEE", "6578953:0102AABB", "8:0"],
"common_filters": ["0102AABB", "03020C7802046D0304937F"]
}- Configures the list of M-Bus addresses (primary and secondary) and common VIF/DIF filters.
- Address format
<address>:<filter>
or<address>:<filter index>
fromcommon_filters
.
MBus VIF/DIF Filter - Description
Below is a table describing the structure of the MBus VIF/DIF filter based on the example
0502AABB01CC02EEFF01EE03EEFFEE
.Order Value Description 1 05
Number of filters (in this case, 5 filters) 2 02
Length of the first filter (2 bytes) 3 AABB
Data of the first filter 4 01
Length of the second filter (1 byte) 5 CC
Data of the second filter 6 02
Length of the third filter (2 bytes) 7 EEFF
Data of the third filter 8 01
Length of the fourth filter (1 byte) 9 EE
Data of the fourth filter 10 03
Length of the fifth filter (3 bytes) 11 EEFFEE
Data of the fifth filter Explanations:
- Number of filters: The first byte (
05
) indicates the total number of filters in the sequence. - Filter length: Specifies the number of bytes that make up the following filter (e.g.,
02
means the filter is 2 bytes). - Filter data: Hexadecimal value representing the filter itself (e.g.,
AABB
,CC
, etc.).
Setup of M-Bus Addresses without Filters
{
"mbus_addresses": ["0", "65789532", "8"]
}- Configures the list of M-Bus addresses (primary and secondary) without using VIF/DIF filtering.
- Device address
<primary address>
or<secondary address>
. - The script logic processes a number less than 255 as a primary address.
-
Setup of Scheduled Data Collection
{
"workdayOnly": 1,
"numberOfDays": 5,
"startHour": 22,
"randomizeSeconds": 3600
}- Activates data collection only on workdays, sets 5 days from the end of the month, starts at 22:00, and includes a random delay of up to 1 hour.
-
Setup of Beacon Interval and Watchdog
{
"beaconT": 96,
"noComWdg": 2592000000
}- Sets the beacon interval to 96 * 15 minutes (24 hours) and the no-communication watchdog to 30 days (in milliseconds).
-
Complex Configuration (Combination of Multiple Parameters)
{
"mqtt_server": "myserver.net",
"mqtt_port": 60883,
"mqtt_mode": "TCP",
"mbus_addresses": ["0:0", "22003287:0", "8:0"],
"common_filters": ["03020C7802046D0304937F"],
"startHour": 23,
"randomizeSeconds": 7200,
"beaconT": 48,
"SCmV": 3150
}- Combines MQTT settings (TCP connection), M-Bus addresses, scheduled data collection (23:00 with a 2-hour random delay), beacon interval (12 hours), and minimum voltage on the Super Capacitor (3150 mV).
How to Send a Retain Message
Using an MQTT client (e.g., Mosquitto, MQTT Explorer), send a message to the topic acrios/<IMSI>/up/config
with the retain flag. Example command for Mosquitto:
mosquitto_pub -h <mosquitto.org> -t "acrios/<IMSI>/up/config" -m '{"mqtt_server":"broker.example.com","mqtt_port":1883,"mqtt_user":"user","mqtt_password":"password"}' -r
-r
: Activates the retain flag.- Replace
<IMSI>
with the actual IMSI of the device. - Replace
<mosquitto.org>
with the MQTT broker address. - Ensure the broker and credentials (
mosquitto.org
,user
,password
) are correctly replaced and correspond.
Verification of Receipt
- The device checks the validity of the JSON upon receiving the message and saves the configuration.
- It sends a confirmation to the topic
acrios/<IMSI>/config
with the message content and clears the retain message. - Monitor this topic for confirmation.
Warning
- Ensure the JSON is correctly formatted, otherwise, the device will reject the configuration.
- Retain messages remain on the broker until overwritten or cleared, ensuring the device receives the configuration upon connection.
Actions
Topic: acrios/<IMSI>/up/action
Messages contain Lua script commands that the device will execute. The device executes the commands after sending a beacon message:
If the message includes the command clearAction()
at the end, the device will clear the message after execution.
If the clearAction()
command is not included, the device will execute the commands each time it sends a beacon message, effectively scheduling repeated commands.
Example:
Topic:
acrios/901405103467181/up/action
Message:
api.ledControl(1)
api.delayms(1000)
api.ledControl(0)
clearAction()
In this case, the device turns on the LED for 1 second and then turns it off. This is a one-time action due to clearAction()
.
For a complete list of actions, consult the Lua API section.