Skip to main content

L-M-T Long Cable Measurement

Documentation for the script purposed for long range cable measurement with use of LMT-CV-N-PEC2-D.

Introduction


The Converter is a battery-powered NB-IoT device that performs autonomous measurements at configurable intervals and reports status via NB-IoT connectivity. It features intelligent alarm burst reporting to extend battery life while ensuring timely fault notification.

The device is designed for monitoring pipe insulation and continuity of underground infrastructure. It operates completely autonomously with intelligent state-change detection and adaptive reporting modes to maximize battery longevity during both normal operation and fault conditions.

This article contains:


Requirements

The following is required:

note

You may use Acrios GUI to upload or the Lua script or firmware, but the devices come with all the things pre-installed.

For the script configuration, you may use downlink commands or edit the script via use of Acrios GUI.

Script Specification

The device uses a Lua script for its functionality and configuration.

Full Lua Script
ver = "1.0"
---- CONFIGURATION ----
APN = "auto"
PLMNID = "0"
protocol = "UDP"
ip = "192.168.0.20"
port = 4242
withImsi = true
receiveTimeout = 15000
-- Payload structure description
-- 1 - MessageType [IMSI - 16B][Framecounter unsigned int - 4B][0000 - 4x1B] ( 0 = OK 1 = Error state first 2 int for P1 second for P2 )
-- 2 - MessageType [IMSI - 16B][Framecounter unsigned int - 4B][0000 - 4x1B] [[unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ]]
-- 3 - MessageType [IMSI - 16B][Framecounter unsigned int - 4B][0000 - 4x1B] [[unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ]] [VCC in mV unsigned short- 2B]

MessageType = 3 -- IMSI (withImsi) 1 = 4B 0 as ok 1 as error [eq 0000] | 2 = for 1 type and (4B + 4B)for one channel which contains Resistance of LOOP and LEAK for both channels
------- DEVICE WAKEUP PERIOD --------
SendingPeriod = 60 * 60 -- send message in minutes
CheckPeriod = 55 -- check period in seconds

-- EEPROM IDX --
SendingPeriod_eeprom_idx = 40
CheckPeriod_eeprom_idx = 42


-- SHORTS
at = api.nbAT
b = string.char
pp = pack.pack
pu = pack.unpack
p = print
ex = api.exec
Insulation = "Insulation"
Continuity = "Continuity"

state = -2
ohm = 0
comment = ""
ErrStateFlag = 0
IMSI = ""
message = {
group = {
frameCounter = 0,
errorFlag = 1,
okFlag = 0,
maxErrMessInRow = 10,
ts = 0,
ts_a = 0,
errMessCounter = 0,
prevAlarm = false,
currentMode = "normal",
burstCount = 0,
lastSendTs = 0,
prevPattern = {},
prevAlarmPattern = {},
[1] = {
Insulation = { Insulation = Insulation, state = state, comment = comment, ohm = ohm },
Continuity = { Continuity = Continuity, state = state, comment = comment, ohm = ohm }
},
[2] = {
Insulation = { Insulation = Insulation, state = state, comment = comment, ohm = ohm },
Continuity = { Continuity = Continuity, state = state, comment = comment, ohm = ohm }
}
}
}
function comparePatterns(old, new)
local changes = 0
for i = 1, #old do
if old[i] ~= new[i] then
changes = changes + 1
p("Pattern changed at pos:" .. i .. ": " .. old[i] .. " -> " .. new[i])
end
end
return changes
end

function gt(state) return api.getTick(state) 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 createStatePattern()
local pattern = {}
local tests = { Insulation, Continuity }
local index = 1
for i = 1, 2 do
for _, test_name in ipairs(tests) do
pattern[index] = message.group[i][test_name].state
index = index + 1
end
end
return pattern
end

function copyPattern(src)
local dst = {}
for i, v in ipairs(src) do
dst[i] = v
end
return dst
end

function getIMSI() return (string.gmatch(api.nbAT("IMSI?", 5000), "([0-9]+)")() or "000000000000000") end

function getIMEI() return (string.gmatch(api.nbAT("IMEI?", 5000), "([0-9]+)")() or "000000000000000") end

function getICCID() return (string.gmatch(api.nbAT("ICCID?", 5000), "([0-9]+)")() or "00000000000000000000") end

function compensateVoltage(measured_mv, batt_mv)
local V_ref = 3600
if batt_mv <= 0 then
return measured_mv
end
local num = measured_mv * V_ref
local half = math.floor(batt_mv / 2)
local compensated_mv = math.floor((num + half) / batt_mv)
return compensated_mv
end

function calc_pipeR(Vin_mV, VoutCh1_mV, VoutCh2_mV)
local Ch1_R = 18000
local Ch2_R = 27
local ch2R1 = -1
local ch1R1 = -1
if VoutCh1_mV == 0 or VoutCh1_mV < 0 then
ch1R1 = 1000000
else
local num1 = Ch1_R * Vin_mV
local half1 = math.floor(VoutCh1_mV / 2)
local rounded1 = math.floor((num1 + half1) / VoutCh1_mV)
ch1R1 = rounded1 - Ch1_R
end
if VoutCh2_mV == 0 or VoutCh2_mV < 0 then
ch2R1 = 1000000
else
local num2 = Ch2_R * Vin_mV
local half2 = math.floor(VoutCh2_mV / 2)
local rounded2 = math.floor((num2 + half2) / VoutCh2_mV)
ch2R1 = rounded2 - Ch2_R
end
return ch1R1, ch2R1, nil
end

-- Checks for insulation and continuity alarms based on measured resistances and cable length, using integer thresholds and tolerances.
function checkAlarms(insulation_ohm, continuity_ohm, length_m, group_id)
local INSULATION_THRESHOLD = 10000
local INSULATION_TOL_PERCENT = 20
local CONTINUITY_PER_100M = 30
local CONTINUITY_OPEN_THRESHOLD = 1000000
local CONTINUITY_TOL_PERCENT = 20
if insulation_ohm < 0 or continuity_ohm < 0 or length_m <= 0 then
message.group[group_id].Insulation.state = 1
message.group[group_id].Continuity.state = 1
return nil, nil, "ERROR: Invalid inputs (negative ohm or zero length)!"
end
local ins_temp = INSULATION_THRESHOLD * (100 - INSULATION_TOL_PERCENT)
local insulation_alarm_threshold = math.floor((ins_temp + 50) / 100)
local cont_temp_max = CONTINUITY_PER_100M * length_m
local max_continuity_ohm = math.floor((cont_temp_max + 50) / 100)
local cont_temp_threshold = max_continuity_ohm * (100 + CONTINUITY_TOL_PERCENT)
local continuity_alarm_threshold = math.floor((cont_temp_threshold + 50) / 100)
local insulation_alarm = (insulation_ohm < insulation_alarm_threshold)
local continuity_alarm = (continuity_ohm > continuity_alarm_threshold) or
(continuity_ohm >= CONTINUITY_OPEN_THRESHOLD)
local mess_t = nil
if insulation_alarm then
mess_t = "ALARM: Insulation (P" ..
tostring(group_id) ..
"-LOOP+ & P" ..
tostring(group_id) .. "-LEAK pins) under limit (< " .. insulation_alarm_threshold .. " ohm, tolerance -20%)"
message.group[group_id].Insulation.state = 1
elseif continuity_alarm then
mess_t = "ALARM: Continuity (P" ..
tostring(group_id) .. "-LOOP+ & P" .. tostring(group_id) .. "-LOOP-) over limit (> " ..
continuity_alarm_threshold .. " R, tolerance +20%) or open (>= 1 M)"
message.group[group_id].Continuity.state = 1
else
mess_t = "OK: No alarms (insulation=" ..
insulation_ohm .. "R, continuity=" .. continuity_ohm .. "R, length=" .. length_m .. "m)"
message.group[group_id].Insulation.state = 0
message.group[group_id].Continuity.state = 0
end
return insulation_alarm, continuity_alarm, mess_t
end

-- Reads measurements from a specified group, compensates voltages, calculates resistances, and checks for alarms.
function readGroup(group_id, averageCount)
local res, ch1, ch2, mv = api.loopMeasurement(group_id, 30, averageCount)
local item = message.group[group_id]
item.Insulation.state = message.group.okFlag
item.Continuity.state = message.group.okFlag
item.Insulation.comment = "ok"
item.Continuity.comment = "ok"
if res == 0 then
p("res:" ..
res ..
"\t ch1(mV):" .. ch1 .. "\t ch2(mV):" .. ch2 .. "\t batt(mV):" .. mv .. "\t avg:" .. averageCount)
p("Compensated Values")
local ch1_m = compensateVoltage(ch1, mv)
local ch2_m = compensateVoltage(ch2, mv)

p("res:" ..
res ..
"\t ch1(mV):" ..
ch1_m .. "\t ch2(mV):" .. ch2_m .. "\t batt(mV):" .. mv .. "\t avg:" .. averageCount)
local ch1_ohm, ch2_ohm, err = calc_pipeR(3600, ch1_m, ch2_m)
item.Insulation.ohm = ch1_ohm
item.Continuity.ohm = ch2_ohm
if err == nil then
p("Calculated Resistance of pipe")
p("ch1_ohm:" .. ch1_ohm .. "\t ch2_ohm:" .. ch2_ohm)
local insulation_alarm, continuity_alarm, alarm_msg = checkAlarms(ch1_ohm, ch2_ohm, 100, group_id)
p("Alarm check: " .. (alarm_msg or "No message"))
if insulation_alarm then
p("INSULATION ALARM!")
item.Insulation.comment = "INSULATION ALARM!"
item.Insulation.state = message.group.errorFlag
end
if continuity_alarm then
p("CONTINUITY ALARM!")
item.Continuity.comment = "CONTINUITY ALARM!"
item.Continuity.state = message.group.errorFlag
end
else
p(err)
end
else
p("res:" .. res .. "\t error")
end
end



function processResponse(rcvd)
p("Processing response...")
if rcvd == nil or #rcvd <= 0 then
p("No response...")
return
end
p("Received Data: ")
api.dumpArray(rcvd, "raw")
local cmd = rcvd:byte(1)
rcvd = rcvd:sub(2) -- drop command byte
local tmp
if cmd == 20 then -- period in minutes report
if #rcvd == 2 then
p("Sending Period in minutes")
_, tmp = pu(rcvd, "<H") -- 2 bytes, lil endian (unsigned short)
api.setVar(SendingPeriod_eeprom_idx, tmp * 60, true) -- config periodic message in minutes
SendingPeriod = api.getVar(SendingPeriod_eeprom_idx, SendingPeriod)
end
elseif cmd == 21 then -- period in seconds to check
if #rcvd == 2 then
p("Check Period in seconds")
_, tmp = pu(rcvd, "<H") -- 2 bytes, lil endian (unsigned short)
api.setVar(CheckPeriod_eeprom_idx, tmp, true) -- config check interval in seconds
CheckPeriod = api.getVar(CheckPeriod_eeprom_idx, CheckPeriod)
end
elseif cmd == 7 then -- compatibility command for reset
p("Resetting...")
ex("_reset")
elseif cmd == 25 then -- execute custom lua request command 19
local ok, func, err = pcall(loadstring, rcvd)
if ok then
pcall(func)
elseif err then
p("Error executing: ", err)
end
func = nil
end
end

-- UPLINK MESSAGES ------------------------------------------
function sendStandardMessage(with_rec)
-- cmdByte 10
p("SENDING MESSAGE:")
local sendImsi = withImsi and IMSI or getIMSI()
local messBuf = ""
local messOhm = 0
local batVolt = 0
for i = 1, #message.group do
local item = message.group[i]
if MessageType == 1 then
messBuf = messBuf ..item.Insulation.state .. item.Continuity.state
elseif MessageType >= 2 then
messBuf = messBuf .. item.Insulation.state .. item.Continuity.state
messOhm = messOhm .. pp("I2", item.Insulation.ohm, item.Continuity.ohm)
end
if MessageType == 3 then
batVolt = pp("<H", api.getBatteryVoltage())
end
end
message.group.frameCounter = message.group.frameCounter + 1
if MessageType == 1 then
msg = sendImsi ..pp("I",message.group.frameCounter).. messBuf
elseif MessageType >= 2 then
msg = sendImsi ..pp("I",message.group.frameCounter).. messBuf .. messOhm
end
if MessageType == 3 then
msg = sendImsi ..pp("I",message.group.frameCounter).. messBuf .. messOhm .. batVolt
end

p("message FCNT :"..tostring(message.group.frameCounter) )
api.dumpArray(msg, "raw")
wSc(2600) -- wait for SC charged
_, rcvd = api.nbSend(ip, port, msg, receiveTimeout, protocol)
if rcvd ~= nil and #rcvd > 0 and with_rec == 1 then
processResponse(rcvd)
end
end

function onWake()
ex("_sysclk", "48000000") -- speed up CPU
ex("_print_mode", "CONNECTED")
local errState = 0
local sent = false
p("OnWake")
p("Reading P1 ports")
readGroup(1, 1)
p("Reading P2 ports")
readGroup(2, 1)
local currentTs = gt(2)
local currentPattern = createStatePattern()
for i = 1, #message.group do
local item = message.group[i]
errState = errState + item.Insulation.state + item.Continuity.state
p("Error state value Insulation & Continuity:" .. tostring(item.Insulation.state),
tostring(item.Continuity.state))
end
p("Error state value:" .. tostring(errState))
local isAlarmNow = (errState > 0)
p("isAlarmNow: " .. tostring(isAlarmNow) .. " errState: " .. tostring(errState))
if isAlarmNow then
if not message.group.prevAlarm then
-- First alarm detection
sendStandardMessage(0)
sent = true
message.group.burstCount = 1
message.group.currentMode = "alarm_burst"
message.group.prevAlarmPattern = copyPattern(currentPattern)
p("First alarm burst send #1")
else
-- Continuing alarm
local patternChanged = false
if #message.group.prevAlarmPattern > 0 then
local diffs = comparePatterns(message.group.prevAlarmPattern, currentPattern)
if diffs > 0 then
patternChanged = true
p("Alarm pattern changed: " .. diffs)
end
end
if patternChanged then
message.group.burstCount = 0
message.group.currentMode = "alarm_burst"
end
if message.group.currentMode == "alarm_burst" then
if message.group.burstCount < message.group.maxErrMessInRow then
sendStandardMessage(0)
sent = true
message.group.burstCount = message.group.burstCount + 1
message.group.prevAlarmPattern = copyPattern(currentPattern)
p("Burst send #" .. message.group.burstCount)
if message.group.burstCount == message.group.maxErrMessInRow then
message.group.currentMode = "alarm_periodic"
p("Switching to alarm periodic")
end
end
elseif message.group.currentMode == "alarm_periodic" then
local timeElapsed = currentTs - message.group.lastSendTs
if timeElapsed >= SendingPeriod then
sendStandardMessage(1)
sent = true
message.group.prevAlarmPattern = copyPattern(currentPattern)
p("Periodic alarm send")
end
end
end
else
-- No alarm now
if message.group.prevAlarm then
-- Return to normal state
sendStandardMessage(0)
sent = true
message.group.currentMode = "normal"
message.group.burstCount = 0
message.group.prevAlarm = false
message.group.prevPattern = copyPattern(currentPattern)
p("Return to normal send")
else
-- Normal mode: check for pattern change
local changes = 0
if #message.group.prevPattern > 0 then
changes = comparePatterns(message.group.prevPattern, currentPattern)
end
if changes > 0 then
sendStandardMessage(1)
sent = true
p("Normal change send, changes: " .. changes)
end
message.group.prevPattern = copyPattern(currentPattern)
end
end
-- Always update state
message.group.prevAlarm = isAlarmNow
if sent then
message.group.lastSendTs = currentTs
end
api.wakeUpIn(0, 0, 0, CheckPeriod)
end

function onStartup()

ex("_sysclk", "48000000") -- speed up CPU
ex("_print_mode", "CONNECTED")

p("OnStart")

at("APN=" .. APN)
at("PLMNID=" .. PLMNID)

IMSI = getIMSI()

CheckPeriod = api.getVar(CheckPeriod_eeprom_idx, CheckPeriod)
message.group.ts = gt(2)
message.group.lastSendTs = message.group.ts
onStartup = nil
end

Connection Configuration

The following variables configure the connection.

warning

Beware, that incorrectly setting up connection detail via downlink will disconnect the device.

APN = "auto"                    -- Automatic APN selection
PLMNID = "0" -- Auto-detect mobile operator
protocol = "UDP" -- Fixed protocol
ip = "192.168.0.20" -- Server IP address
port = 4242 -- Server UDP port
withImsi = true -- Include IMSI in payload
receiveTimeout = 15000 -- 15-second response timeout

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.

1a. Set these parameters to the values provided by your SIM card provider.

...
APN = "auto" -- <- change this (but keep the commas, e.g.: "cdp.iot.t-mobile.nl")
PLMNID = 0 -- <- change this (e.g.: 20416)
...

1b. Or set the values as following for the automatic selection.

...
APN = "auto"
PLMNID = 0
...
warning

The automatic selection has to be supported by the SIM card provider.

2. If you are using our backend, set IMSI to 1.

...
withIMSI = 1
...

Message Configuration

The following variables configure the messages.

You may configure the following aspects:

Type 1 – Status Only

Sends only alarm states (OK/Alarm) for both P1 and P2 channels. No resistance or battery data. Smallest message size (24 bytes). Best for minimal bandwidth requirements where you only need to know if an alarm occurred.

Type 2 – Status + Resistance

Includes alarm states plus measured resistance values in ohms for insulation and continuity on both channels. Useful for diagnostics and understanding fault severity. Message size: 40 bytes.

Complete monitoring payload with alarm states, resistance values, and battery voltage in millivolts. Enables predictive battery maintenance and full device diagnostics. Recommended for production deployments. Message size: 42 bytes.


TypeSizeAlarm StateResistanceBattery
124B
240B
342B
-- Payload structure description
-- 1 - MessageType [IMSI - 16B][Framecounter unsigned int - 4B][0000 - 4x1B] ( 0 = OK 1 = Error state first 2 int for P1 second for P2 )
-- 2 - MessageType [IMSI - 16B][Framecounter unsigned int - 4B][0000 - 4x1B] [[unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ]]
-- 3 - MessageType [IMSI - 16B][Framecounter unsigned int - 4B][0000 - 4x1B] [[unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ][unsigned int value in ohm - 4B ]] [VCC in mV unsigned short- 2B]

MessageType = 3 -- IMSI (withImsi) 1 = 4B 0 as ok 1 as error [eq 0000] | 2 = for 1 type and (4B + 4B)for one channel which contains Resistance of LOOP and LEAK for both channels

Device Wake-up Period and EEProm IDX

You may further configure the sendig period and check period.

note

Note that the value you input is in seconds

------- DEVICE WAKEUP PERIOD --------
SendingPeriod = 60 * 60 -- send message in seconds (this would be 60 minutes)
CheckPeriod = 55 -- check period in seconds

-- EEPROM IDX --
SendingPeriod_eeprom_idx = 40
CheckPeriod_eeprom_idx = 42

The device supports over-the-air configuration via NB-IoT downlink messages. Remote commands are processed in the processResponse() function.

HEX Conversion

The downlink payloads have to be sent in HEX downlink payload. Check the following mini guide to see how to do the HEX conversion.

HEX Conversion Mini Guide

Number conversion

If the downlink is in form of a number, following needs to be done.

For number conversion, use HEX converter such as this one

  1. Enter a number you want to convert, e.g. 4000

HEX number conversion

  1. Now use the little endian version, which would be A0 0F in this case.

  2. This would be the downlink message, if you were to change e.g. initial delay. Make sure that there is a space behind the last byte for the downlink to be valid.

If you were to convert a number, that has one byte only, but the requirement for payload is 2 bytes, simply replace the second byte (or other amount of unused bytes) with 00

For example, 0A is 10, therefore downlink would look like this: 0A 00

Text conversion

For text conversion, use HEX converter such as this one

This would be used mostly for custom Lua commands in case of this script.

  1. Enter a text you would like to convert, e.g. print("Hi")

HEX string conversion

  1. And simply copy the converted HEX payload, which would be 70 72 69 6E 74 28 22 48 69 22 29 in this case.

  2. Add a relevant downlink command in front of your message, in this case preface the HEX string with 19 as that is a command for a custom Lua command.

  3. The resulting downlink message would be 19 70 72 69 6E 74 28 22 48 69 22 29

The message structure is as follows:

The first part of the message is the command number:

14 - (0x14)

Then it follows either with a numerical value, or the command is all that is needed, or the hex can be a Lua code snippet.

In this case, we are using command 0x14, which changes the sending period, so we follow it up with and integer value:

1E

And since it is a smaller number, we will follow it with an empty byte:

00

The message would look as follows:

14 1E 00 - changes sending period to every 30 minutes

Command 20 – Change Measurement Sending Period

What it does: Controls how often the device sends status messages when no faults are detected.

Use case:

  • Default is 1 hour (3600 seconds) – good for most installations
  • Change to 30 minutes (1800 seconds) if you want more frequent updates
  • Change to 6 hours if you want to save battery in non-critical areas

How to send:

The downlink should have a following form.

Command Code: 0x14
Parameter: [Period in minutes]

Example 1: Change to 30 minutes

Hex message: 14 1E 00
Breakdown: 14 = Command code (0x14)
1E = 30 minutes (little-endian: 0x001E)
00 = (continuation of 2-byte value)

Example 2: Change to 1 hour (default)

Hex message: 14 3C 00
Breakdown: 14 = Command code
3C = 60 minutes (0x003C)
00 = (continuation)

Example 3: Change to 6 hours

Hex message: 14 68 01
Breakdown: 14 = Command code
68 01 = 360 minutes (0x0168)

Practical impact:

Sending PeriodMessage FrequencyBattery Impact
30 minutes2× hourly~10% higher consumption
1 hour (default)Every hourBaseline
6 hours4× daily~60% lower consumption

Command 21 – Change Measurement Check Period

What it does: Controls how often the device wakes up to perform measurements.

Use case:

  • Default is 55 seconds – detects faults within ≤60 seconds
  • Change to 120 seconds if you only need fault detection every 2 minutes (saves battery)
  • Change to 30 seconds if you need very fast fault detection (uses more battery)

How to send:

The downlink should have a following form.

Command Code: 0x15
Parameter: [Period in seconds]

Example 1: Change to 55 seconds (default)

Hex message: 15 37 00
Breakdown: 15 = Command code (0x15)
37 = 55 seconds (0x0037)
00 = (continuation)

Example 2: Change to 120 seconds (2 minutes)

Hex message: 15 78 00
Breakdown: 15 = Command code
78 = 120 seconds (0x0078)
00 = (continuation)

Example 3: Change to 30 seconds

Hex message: 15 1E 00
Breakdown: 15 = Command code
1E = 30 seconds (0x001E)
00 = (continuation)

** Important:** CheckPeriod affects fault detection latency. If you set it to 120 seconds, faults may take up to 2 minutes to be detected.

Practical impact:

Check PeriodFault Detection TimeBattery Impact
30 seconds≤ 30 seconds~30% higher consumption
55 seconds (default)≤ 60 secondsBaseline
120 seconds≤ 120 seconds~25% lower consumption

Command 7 – Reset Device

What it does: Performs a complete device restart. Configuration (from EEPROM) is preserved.

Use case:

  • Device is stuck or unresponsive
  • After updating firmware
  • Troubleshooting connectivity issues

How to send:

The downlink should have a following form.

Command Code: 0x07
(No parameters required)

Example:

Hex message: 07

Effect:

  • Device reboots immediately
  • All EEPROM settings retained
  • Device reconnects to NB-IoT network
  • Measurement cycle restarts fresh

Time to recovery: ~30–60 seconds after reset command is sent


Command 25 – Execute Custom Lua Code

What it does: Runs custom Lua scripts directly on the device.

Use case:

  • Advanced debugging
  • Querying current device state
  • Custom automation
  • Technical support requests

How to send:

The downlink should have a following form.

Command Code: 0x19
Parameter: [Lua script code]

Example 1: Reset frame counter

Hex message: 19 6D 65 73 73 61 67 65 2E 67 72 6F 75 70 2E 66 72 61 6D 65 43 6F 75 6E 74 65 72 20 3D 20 30
Lua code: message.group.frameCounter = 0

Example 2: Query battery voltage

Hex message: 19 70 28 22 42 61 74 74 65 72 79 3A 20 22 20 2E 2E 20 61 70 69 2E 67 65 74 42 61 74 74 65 72 79 56 6F 6C 74 61 67 65 28 29 20 2E 2E 20 22 20 6D 56 22 29
Lua code: p("Battery: " .. api.getBatteryVoltage() .. " mV")

Installation Specifications

Electrical Specifications

ParameterValue
Excitation Voltage3.6 VDC
Measurement Interval (default)55 seconds
Reporting Interval (normal)3600 seconds (1 hour)
ProtocolNB-IoT (UDP)
Default Server Port4242

Cable and Measurement Specifications

ParameterValueNotes
Maximum Cable Length300 mSupported measurement range
Wire Resistivity0.012–0.015 Ω/mTest wire specification
Pipe Length Configuration0–300 mUser-configurable

Measurement Principle

The device uses a voltage divider principle for pipe measurement:

  • Insulation Test (R1-2 / LOOP+ to LEAK): Measures resistance between positive excitation and insulation monitoring line
  • Continuity Test (R1-3 / LOOP+ to LOOP-): Measures resistance between positive and negative conductors of the pipe

Both measurements are performed simultaneously at regular CheckPeriod intervals.

Alarm Thresholds

Insulation Alarm (R1-2):

  • Threshold: < 10 kΩ
  • Tolerance: ±20%
  • Effective alarm level: < 8 kΩ
  • Action: Triggers insulation fault report

Continuity Alarm (R1-3):

  • Base threshold: 30 Ω per 100 m of configured pipe length
  • Open/over-range threshold: ≥ 1 MΩ
  • Tolerance: ±20% (applied to calculated threshold)
  • Action: Triggers continuity fault report

Example Continuity Calculation (100 m cable):

Base threshold: 30 Ω/100m × 100m = 30 Ω
With tolerance: 30 Ω × (1 + 0.20) = 36 Ω alarm threshold

Example Continuity Calculation (300 m cable):

Base threshold: 30 Ω/100m × 300m = 90 Ω
With tolerance: 90 Ω × (1 + 0.20) = 108 Ω alarm threshold

Physical Connector and Wiring

Terminal Mapping:

TerminalGroupFunction
P1-LOOP+P1 (Pair 1)High potential excitation (positive)
P1-LEAKP1 (Pair 1)Insulation measurement point
P1-LOOP-P1 (Pair 1)Continuity measurement return
P2-LOOP+P2 (Pair 2)High potential excitation (positive)
P2-LEAKP2 (Pair 2)Insulation measurement point
P2-LOOP-P2 (Pair 2)Continuity measurement return

Cable Arrangement:

Device Terminal          Cable Under Test
─────────────────────────────────────────
P1-LOOP+ ─────────────────── [Conductor A]

[Device]

P1-LOOP- ─────────────────── [Conductor B]

P1-LEAK ────────────────────── [Separate insulation line]

Constraints:

  • Maximum cable length: 300 meters
  • Wire resistivity specification: 0.012–0.015 Ω/m
  • Both P1 and P2 groups can measure independent cable pairs simultaneously

Battery Lifetime Estimates

Normal Operation (No Faults)

Measurement profile: Check every 55 seconds; send status every 1 hour

Battery CapacityBattery VoltageEnergy per MeasurementExpected Battery Life
1D 19Ah (no margin)3.4V1.13 mWh6.6 years
1D 19Ah (30% safety margin)3.4V1.13 mWh4.6 years

The 30% safety margin is recommended for production deployments to account for:

  • Temperature fluctuations
  • Measurement variability
  • Network latency spikes
  • Unexpected alarm conditions

Alarm State Impact

Battery consumption increases significantly during fault conditions due to burst messaging:

Alarm Burst Phase:

  • Up to 10 messages at ~55-second intervals
  • Total duration: ~550 seconds (~9 minutes)
  • Increased power draw due to frequent NB-IoT transmissions

Pattern Change Reset:

  • If alarm pattern changes, burst count resets
  • Additional 10-message cycle begins
  • Worst-case scenario: Alternating faults can accelerate battery drain

Periodic Alarm Phase:

  • After 10 burst messages, falls back to 1-hour intervals
  • Consistent with normal operation reporting rate

Recommendation: For installations with frequent or intermittent faults, plan battery replacement more conservatively (3–4 year intervals).

Voltage Compensation

The firmware automatically compensates for battery voltage variations to maintain measurement accuracy across the discharge curve:

V_ref = 3.6V
compensated_mv = (measured_mv × V_ref) / batt_mv

This ensures reliable alarm detection even as battery voltage decays from 3.6V to 2.8V.


Reporting Logic and Messaging

Measurement and State Detection

Measurement Cycle:

  1. Device wakes at CheckPeriod interval (default: 55 seconds)
  2. Performs voltage measurements on both P1 and P2 groups
  3. Compensates voltages for current battery level
  4. Calculates resistance values via voltage divider formula
  5. Evaluates alarms against configured thresholds
  6. Creates current state pattern: [P1_Ins, P1_Cont, P2_Ins, P2_Cont]

State Pattern Definition:

0 = OK (no alarm)
1 = Alarm (threshold exceeded)

Example: [0, 0, 1, 0] = P2 insulation alarm, all others OK

Payload Showcase

Message Type 3

Raw Payload (hex):

39 30 31 32 38 30 30 37 37 32 35 35 38 37 34 01 00 00 00 00 00 00 00 6B 26 00 00 1B 00 00 00 58 1F 00 00 64 00 00 00 A0 0D

Breakdown

FieldBytesValueInterpretation
IMSI16B39 30 31 32 38 30 30 37 37 32 35 35 38 37 34901280077255874
FrameCounter4B01 00 00 001 (first message)
P1 Insulation State1B00OK
P1 Continuity State1B00OK
P2 Insulation State1B00OK
P2 Continuity State1B00OK
P1 Insulation Resistance4B6B 26 00 009835 Ω (9.8 kΩ)
P1 Continuity Resistance4B1B 00 00 0027 Ω
P2 Insulation Resistance4B58 1F 00 008024 Ω (8.0 kΩ)
P2 Continuity Resistance4B64 00 00 00100 Ω
Battery Voltage2BA0 0D3488 mV (3.488V)

Summary

Device: 901280077255874

Status: All measurements healthy, no alarms detected

P1 Group:

  • Insulation: 9.8 kΩ (within limits)
  • Continuity: 27 Ω (reference resistor baseline)

P2 Group:

  • Insulation: 8.0 kΩ (within limits)
  • Continuity: 100 Ω (good condition)

Battery: 3.488V (good condition, plan replacement within 1 year)

Message Type 2

Raw Payload (hex):

39 30 31 32 38 30 30 37 37 32 35 35 38 37 34 01 00 00 00 00 00 00 00 77 26 00 00 1B 00 00 00 58 1F 00 00 64 00 00 00

Breakdown

FieldBytesValueInterpretation
IMSI16B39 30 31 32 38 30 30 37 37 32 35 35 38 37 34901280077255874
FrameCounter4B01 00 00 001 (first message)
P1 Insulation State1B00OK
P1 Continuity State1B00OK
P2 Insulation State1B00OK
P2 Continuity State1B00OK
P1 Insulation Resistance4B77 26 00 009847 Ω (9.8 kΩ)
P1 Continuity Resistance4B1B 00 00 0027 Ω
P2 Insulation Resistance4B58 1F 00 008024 Ω (8.0 kΩ)
P2 Continuity Resistance4B64 00 00 00100 Ω

Summary

Device: 901280077255874

Status: All measurements healthy, no alarms detected

P1 Group:

  • Insulation: 9.8 kΩ (within limits)
  • Continuity: 27 Ω (reference resistor baseline)

P2 Group:

  • Insulation: 8.0 kΩ (within limits)
  • Continuity: 100 Ω (good condition)

Note: This is Message Type 2 (status + resistance). Battery voltage is not included.

Message Type 1

Raw Payload (hex):

39 30 31 32 38 30 30 37 37 32 35 35 38 37 34 01 00 00 00 00 00 00 00

Breakdown

FieldBytesValueInterpretation
IMSI16B39 30 31 32 38 30 30 37 37 32 35 35 38 37 34901280077255874
FrameCounter4B01 00 00 001
P1 Insulation State1B00OK
P1 Continuity State1B00OK
P2 Insulation State1B00OK
P2 Continuity State1B00OK

Summary

Type 1 contains only the first 24 bytes of Type 2.

All channels report healthy (no alarms). Resistance values are omitted.

  • Device: 901280077255874
  • P1: Both OK
  • P2: Both OK
  • Size: 24 bytes (vs 40 bytes for Type 2)
In case of any questions or problems, please contact us at support@acrios.com.