The implemented LUA version is 5.1.5. The LUA API changes will be reflected within the firmware changelog.
The following LUA functions are implemented in the firmware:
"byte"
"char"
"dump"
"find"
"format"
"gfind"
"gmatch"
"gsub"
"len"
"lower"
"match"
"rep"
"reverse"
"sub"
"upper"
Turns ON or OFF the on board LED. It can be also used for debugging purposes.
--turns on blue LED
api.ledControl(1)
Pause the execution for ms milliseconds.
api.delayms(1000) --delay one second
api.ledControl(1) -- turn on blue LED
api.delayms(1000) -- wait 1 second
api.ledControl(0) -- turn off blue LED
Generate random number within given range.
Available only for LoRaWAN devices (function uses it's module to generate random number)
-- generates random number within interval from 5 to 20
ran=api.randInt(5, 20)
Returns an unique integer number in range <0; 2^32>.
--get an unique number
num = api.getUniqueNumber()
Get curent battery voltage in mV.
--get battery voltage value in mV
mv = api.getBatteryVoltage()
Returns current number of milliseconds since startup. Counts up to 2^32 and then restarts from 0. By default it's using RTC derived tick which is incremented during sleep, but it's possible to explicitly select systick, which isn't.
--get a timestamp, can be used for timing
timestamp = api.getTick()
-- to show the amount of milliseconds you can use print() function
print(timestamp)
Prints contents of variable as hexadecimal string (dumps array into console)
--print string "123ef" as hexadecimal
api.dumpArray("123ef")
--OUTPUT 00 : 31 32 33 65 66
Performs operation with given floating point data.
Only if it's allowed by the firmware.
Convert function available from FW version v2.5.1
-- Multiply IEE 754 float with a floating point constant and return coerced value
-- S - string float representation, e.g. “2.234”
-- B - binary little endian IEEE 754 representation as 4 characters/bytes in a string
ret = api.float("mul", "SSN", "12.8", "1000.0")
print(ret)
-- output = 12800
ret = api.float("mul", "SSS", "12.8", "1000.0")
print(ret)
-- output = 12800.0000
-- hexadecimal interpretation of a number 12.8 is 0x414CCCCD (IEE 754)
x=string.char(0xCD,0xCC,0x4C,0x41)
ret = api.float("mul", "BSS", x, "1000.0")
print(ret)
-- output = 12800.0000
-- Convert integer to IEE 754 float
-- N - Input in little endian integer(for convert only)/Returns ouput as little endian integer
-- I - Binary little endian integer representation as 4 characters/bytes in a string(for convert only).
-- B - binary little endian IEEE 754 representation as 4 characters/bytes in a string
-- > - returns result as big endian, if used as output IEEE 754 representation as 4 characters/bytes in a string
-- < - returns result as little endian, if used as output IEEE 754 representation as 4 characters/bytes in a string (default value)
y=string.char(0xCD,0xCC,0x4C,0x42)
ret = api.float("convert", "IB", y)
api.dumpArray(ret)
-- output = 9A 99 84 4E
y=125
ret = api.float("convert", "NB>", y)
api.dumpArray(ret)
-- output = 42 FA 00 00
Saves a persistent variable value, can be used between different wake up iterations.
OR
--set persistent variable value from index 1000 to value of 3424
--can be used to send different data between wake-ups
--for variables persistent between device reset,
--use indexes 48 to 1071
api.setVar(1000, 3424)
myVariable = "This is some string to store to eeprom"
api.setVar(29, #myVariable) -- save the length for later readout
api.setVar(30, "bytes", myVariable) -- consumes in total ceil(#myVariable / 4) 32-bit indexes in EEPROM/RAM/HEE
Returns persistent variable value, can be used between different wake up iterations.
OR
--get persistent variable value from index 1000
--can be used to send different data between wake-ups
--for variables persistent between device reset,
--use indexes 48 to 1071
slotNumber = api.getVar(1000)
stringLength = api.getVar(29)
myReadout = api.getVar(30, "bytes", stringLength)
print(myReadout) -- should print "myVariable" content!
Schedules the next wake up event of the device to provided day of month (day), hour, minute and second. The provided wake up date is therefore absolute and not relative as in wakeUpIn().
--schedules next wake up to the 25th, 2:22:58
status = api.wakeUpAt(25, 2, 22, 58)
Schedules the next wake up event of the device after specified time interval. The provided wake up date is therefore relative and not absolute as in wakeUpAt(). Note: The input arguments are not limited, but the total period specified must not exceed 31 days. (e.g. hour = 40, days = 2 gives a period of 3 days and 16 hours).
--schedules next wake up in 1 day and 122 minutes
status = api.wakeUpIn(1, 0, 122, 0)
Returns current date and time set on this device. The time can be synchronized over LoRa, or when uploading LUA script using LUA scripting interface.
--read current date and time
y,M,d,h,m,s = api.getTimeDate()
Set date and time on this device.
--set date and time
api.setTimeDate(2022, 1, 31, 23, 59, 0)
Turn ON/OFF voltage source
Only for variants with voltage source.
The voltage option is available only for variant with adjustable option.
--turn on voltage source
api.voltageSourceState(1)
--turn off voltage source
api.voltageSourceState(0)
Read stdin (standard input) data or control button (get status, wait for button).
This function is using serial line, therefore use of this function in an interactive console (GUI) is not possible.
Write to stdout (standard output) without adding line termination. Can be useful when
implementing raw serial protocols.
- out (string) - string buffer to print to serial console (without adding newlines, raw print)
- size (int)(optional) - size of out to print (default prints entire out in other words #out)
Controls the amount of prints into the serial line.
--turn off timestamp
api.setVerbosity(0,"TIMESTAMP_MODE")
Executes a system command or Lua fragment (additionaly uploaded Lua API extension, according to https://ieeexplore.ieee.org/document/8733437)
def CB_calculateCrc16(data):
def crc16_ccitt_update(byte, crc):
# For each bit in the data byte, starting from the leftmost bit
for i in range(7, -1, -1):
# If leftmost bit of the CRC is 1, we will XOR with
# the polynomial later
xor_flag = (crc & 0x8000) != 0
# Shift the CRC, and append the next bit of the
# message to the rightmost side of the CRC
crc = crc << 1
if (byte & (1 << i)) != 0:
crc = crc | 1
# Perform the XOR with the polynomial
if xor_flag:
crc = crc ^ 0x1021
return crc & 0xFFFF
retCrc = 0xFFFF
# last is the payload
for i in range(len(data)):
retCrc = crc16_ccitt_update(data[i], retCrc)
# Augment 16 zero-bits
for i in range(2):
retCrc = crc16_ccitt_update(0, retCrc)
return retCrc
-- get converter model and power source
api.exec("_sysinfo")["model"]["name"] --result e.g.: ACR_CV_101N_R_EAC
api.exec("_sysinfo")["model"]["source"] --result e.g.: ac
Interface to create and manipulate memory-efficient tables and maps.
Check multimode script or sendOnce script for example usage.
Here is an example of inserting a string into the table:
--Setup table--
api.table("set-max-id", 1)
api.table("new", 1, "STR", 100)
--Inserting the value--
api.table("insert", 1, "Hello")
--Getting the value--
y=api.table("get-index", 1, 1) print(y)
--Response--
~> [STDOUT]: Hello
--Getting index with the value--
s=api.table("index-of",1,"Hello") print(s)
--Response--
~> [STDOUT]: 1
--Getting the length of the index--
a=api.table("get-index-length",1,1) print(a)
--Response--
~> [STDOUT]: 5
--Clearing the table--
api.table("clear",1,1)
y=api.table("get-index", 1, 1) print(y)
--Response--
~> [STDOUT]: nil
--Removing table--
api.table("dispose",1)
y=api.table("get-index", 1, 1) print(y)
--Response--
~> [HWAPI_COMMON]: Cannot index in non-existent table ID 1
~> [STDOUT]: -1
Sends buffer msg to LoRa. Acknowledged or non-acknowledged transport can be used using ack parameter. Maximum execution time is limited by timeout in miliseconds.
Universal LoRaWAN converter has reserved 3 uplink ports - 123, 124 and 125. Received data are processed by firmware itself.
Port 123 is used for configuration
Ports 124 and 125 are used for manufacturer purposes (firmware update, etc.).
--sends 0xCCBBAA35 to LoRa with 20s timeout and acknowledged mode
msg = pack.pack('<b4', 0xCC, 0xBB, 0xAA, 0x35)
status, port, answer = api.loraSend(1,20000, msg)
if status >= 1 then --checks if device received more than one 1 byte
api.dumpArray(answer) --if true, print the data
end
Sends join request.
Rejoin is made automaticaly once per 7 days by default, but it can be changed using following "api.loraSetup" function.
-- check if joined, if not join ...
api.loraJoin()
-- join!
api.loraJoin(1)
Sets parameters defined by strings s1 and s2.
This function resets previous activation settings. Meaning, if you call repeatedly api.loraSetup("ACTIVATION", "OTAA") it'll send join request each time this function is called.
Before setting up the device to class B or C, verify it's supported/enabled by the Network server you use. Otherwise, it won't switch the class.
“ACTIVATION”
- Type of activation“ADR”
- Adaptive Data Rate“DR”
- Data Rate“DC”
- Duty Cycle“RUN_FSM”
- Run internal LoRaWAN FSM for x milliseconds
“CLASS”
- Sets or gets current LoRaWAN class
“OTAA-REJOIN-CONFIRMED-FAILED-COUNTER"
- Sets number of confirmed messages, which must fail before new join is forced (cannot be lower than 5, which is also default number)"OTAA-MINIMAL-CONFIRMED-PERIOD"
- Sets period of time (in milliseconds) to send join request (by default 7 days)"OTAA-REJOIN-CONFIRMED-COUNTER"
- Sets maximum of unconfirmed messages in row (default and minimal values is 5), every nth message is forcefully sent as confirmed"POWER"
- Sets the power to defined value in dBm
"OTAA"
, "ABP"
"ON"
, "OFF"
, "DCTIME"
, "GET"
If ADR is turned off and DR is manually set, please keep in mind if the join request is performed (by calling api.loraSend after startup, or by calling api.loraJoin), it will assign a different DR value during the join request.
If activation type ABP is used and ADR is on, when attempting to set DR, it will always asign DR = 0.
--set activation type as ABP
api.loraSetup("ACTIVATION", "ABP")
--set data rate to 0
api.loraSetup("DR", 0)
Listens for message until received or timeout in class C.
Firstly set the device to class B or C before using this function and verify it's supported by the Network server.
--Listen on LoRa for a message with timout of 10 seconds
api.loraSetup("CLASS", "C")
status, port, buffer = api.loraListenClassBC(10000)
Sets only one LoRa credential.
--Set one LoRa credential (in this case devEUI)
api.loraSetCredential("devEUI", "3333333333333333")
Sets LoRa credentials.
--Set LoRa credentials
api.loraSetCredentials("22011221","3333333333333333","44444444444444444444444444444444","55555555555555555555555555555555","70B344440013333")
Sends buffer msg to NB on specified IP, port and protocol type. Maximum length of Rx and Tx messages is 512 Bytes. Maximum execution time is limited by timeout miliseconds.
-- sends "test message" string to IP 185.8.239.192 on port 5566 with 6s timeout
status,answer = api.nbSend("185.8.239.192", 5566, "test message", 6000, "UDP")
Sends an AT command to NB module with specified timeout. Or use one of the defined commands.
-- Get IMSI number of SIM card with timeout of 4 seconds
res = api.nbAT("AT+CIMI", 4000)
print(res)
-- IMEI --
x=api.nbAT("AT+CGSN")
start = string.find(x, "\n")
finish = string.find(x, "\r", start+1)
imei = string.sub(x,(start+1),(finish-1))
print("IMEI: " .. imei)
-- Get signal strength
res,b = api.nbAT("AT+CSQ")
print(res)
-- AT_DEBUG_OFF - turn off
-- AT_DEBUG_ON - turn on
-- REINITIALIZE - re-initialize the NB-IoT module
-- ICCID? - returns ICCID value of current SIM card
-- IMSI? - returns IMSI value of current SIM card
-- IMEI? - returns IMEI value of the modem
api.nbAT("AT_DEBUG_ON")
-- If there is no downlink for given value, set timeout to 30 seconds and re-initialize the module.
- api.nbAT("DOWNLINK_MAX_PERIOD", 3*24*3600*1000)
--if NB-IoT module is initialized 5 times in a row -> sleep 4 hours and then reset
- api.nbAT("REINIT_HARD_LIMIT_RETRY", 5)
-- "1" if DOWNLINK_MAX_PERIOD situation happened since reset.
- api.nbAT("FORCED_REINIT_DONE?")
Enables internal buffer to receive NB data. Increases memory efficiency.
Transmits Dali command to specified address.
-- initialize Dali device
ans = api.daliTransaction(0xA500)
Read analog value on defined pin.
Disabled by defaul, FW & HW changes required.
AnalogChannel=1
mV = api.AnalogReadPin(AnalogChannel)
print("Raw value: " .. tostring(mV) .. " mV")
Waits for specified event on defined pin. If event occurs, device wakes up and calls onWake() function.
Read DIO pin state.
Writes DIO pin state.
-- set logical 1 to pins 2 and 3, and logical 0 to pins 1 and 4
api.DIOwritePin(1, 0)
api.DIOwritePin(2, 1)
api.DIOwritePin(3, 1)
api.DIOwritePin(4, 0)
-- read state of pins 1 to 4
print("pin 1 set to:", api.DIOreadPin(1))
print("pin 2 set to:", api.DIOreadPin(2))
print("pin 3 set to:", api.DIOreadPin(3))
print("pin 4 set to:", api.DIOreadPin(4))
Returns temperature in milli degrees of Celsius (m°C) from DS18B20. In case of using multiple DS18B20 connected to one data pin, it's possible to address one DS18B20 with ID.
DS18B20 is functional after hardware changes. Also has to be allowed by the firmware.
-- Search all devices on the 1-Wire bus
found=api.ds18b20Search(1,2)
-- `found` contains a table of addresses of found devices
-- or `nil` when no device was found
-- Print count of devices found
if (found)
then
print("Found " .. #found .. " devices")
-- Loop over all addresses
for i = 1,#found,1
do
temp=api.ds18b20GetTemp(1,2,found[i])
temp = temp/1000
print("Teplomer #" .. i .. " " .. temp .. "C")
end
else
print("No devices found!")
end
--Without addressing (one sensor=one data pin)
res1 = api.ds18b20GetTemp(1, 2) -- get temperature from sensor connected to data pin 2
print("Temperature: "..tostring(res).." mDeg.C")
Prints into console address of all the devices.
--Find addresses of 3 sensors
api.ds18b20Search(1, 2, 3)
Reports current value of S0 channel counter (specified in channel input argument).
By calling this function, an internal shadow variable for the channel counter is updated, so that the counter for onThreshold() event is reset.
--Read the value of S0 channel 3 and stores to given variable (val)
val = api.S0readCounter(3)
Used to set the value of S0 counter in a non-volatile memory. Typicaly used on startup to restore current value.
--Set counter value for channel 0 to the value of 100
api.S0initializeCounter(1, 100)
Defines a threshold between current value of S0 channel counter and last reported value. When difference of these last two reaches the value, the onThreshold() event is called.
--Set threshold for channel 2 to the value of 10000
api.S0setThreshold(2, 10000)
Transmit "msg" and wait given "timeout" milliseconds for the answer. The transmission is retried "retry" times.
Turn on MBus using mbusState first.
--Send MBus frame [0x10, 0x50, 0x30, 0x16] and wait 5s for the answer twice
msg = pack.pack('<b4', 0x10, 0x50, 0x30, 0x16)
status,c,a,ci,ans = api.mbusTransaction(msg, 5000, 2)
Configures the MBus communication interface.
Configuration from GUI is used by default, but it can be overriden using this function.
Turn on M-Bus using mbusState after setting up M-Bus parameters using this function.
--setup M-Bus interface to 9600 Baud, 8E2
api.mbusSetup(9600, 2, 2, 8)
Turns on the M-Bus circuitry.
Use api.mbusState(1) before api.mbusTransaction() and api.mbusState(0) after to reduce consumption. The consumption significantly raises if the circuitry is turned on for too long.
Do not use the api.mbusState(1) during LoRaWAN or NB-IoT message transmition.
Valid only for new topology, otherwise there's no return value
api.mbusState(1) --turn on M-Bus
Scan for Mbus devices.
api.mbusSetup(2400,2,1,8)
api.mbusState(1) --turn on M-Bus
e,cnt = api.mbusScan("read") -- scan for everything(ID, man, ...) & use default timeout 3000 ms
for i = 1, cnt do
print(string.format("%08X", (e[i].identification)))
end
api.mbusState(0) --turn off M-Bus
This function is used for secondary addressing using 0xFD address internally. For more details check the example.
Example how to use api.mbusFilter for one secondary address
x=pack.pack("<I",0x22003287) -- store little endian unsigned integer value of 0x22003287 to "x" variable
api.mbusFilter("populate",x) -- insert the meter ID to mbusFilter
status,_,_,_,_,raw = api.mbusTransaction(0,3000,1) -- "mbusFilter" is a table to manage secondary addresses starting with index 0. Since ID "0x22003287" is first inserted value, the index of this ID is "0"
api.dumpArray(raw) -- print the content of variable raw
Example using while function to get complete mbus frame (if it's available)
--"i"=index in the table created by api.mbusFilter() (starting with 0)
status,_,_,_,_,raw = api.mbusTransaction(i,3000,1)
if status > 1 then
while status > 1 do
status,_,_,_,_,rawNext = api.mbusTransaction("",200,1)
if status > 1 then
raw = raw .. rawNext
else
break
end
end
end
api.dumpArray(raw)
Function to create and manipulate internal table conatining secondary addresses.
baudrate = 2400 -- baudrate: up to 921600 baud
parity = 2 -- communication parity: 0 for none, 1 for odd and 2 for even parity
stopBits = 1 -- number of stop bits: 1 or 2
dataBits = 8 -- number of data bits: 7 or 8
-- b18 = bytes to pack in total
-- 0x02 = number of physical quantities/filters in this first group
-- 0x03 = number of following bytes
-- 0xFD, 0xDC, 0xFF = first applied filter in first group
-- 0xFD, 0xC9, 0xFF = second applied filter in first group
api.mbusVifDifFilter("populate",pack.pack("b18", 0x02, -- first group (index 0)
0x03, 0xFD, 0xDC, 0xFF, -- current
0x03, 0xFD, 0xC9, 0xFF, -- voltage
0x02, -- second group (index 1)
0x03, 0xFD, 0xDA, 0xFF, -- current
0x03, 0xFD, 0xC8, 0xFF)) -- voltage
api.mbusSetup(baudrate,parity,stopBits,dataBits)
api.mbusState(1)
api.delayms(2000)
-- choose only one index
api.mbusVifDifFilter("activate", 0) -- index 0
-- api.mbusVifDifFilter("activate", 1) -- index 1
b=pack.pack('<b5', 0x10, 0x7B, 0xFE, (0x7B+0xFE)%256, 0x16)
status,_,_,_,_,raw = api.mbusTransaction(b,3000,1)
api.mbusState(0)
print("From MBus Calorimeter: ")
api.dumpArray(raw)
Function filtering received M-Bus frame by given group of bytes (VIF/DIF)
Changes the configuration of W M-bus.
--setup W M-bus interface to power of 10 dBm, role master/concentrator and T2 mode
api.wmbusSetup(10, "master", "T2")
Sets W M-bus C field.
-- Set W M-bus C field as 128
api.wmbusSetCField(128)
Sets W M-bus header.
-- Set W M-bus header for specific device
print("--- W M-bus T1 Digital Thermometer ---")
api.wmbusSetup(10, "meter", "T1")
api.wmbusSetCField(17)
api.wmbusSetHeader(123, 123456, 2, 33)
Sends frame through W M-bus.
-- Send W M-bus frame
api.wmbusSendFrame(212, "foobar")
Waits timeout milliseconds for data reception from W M-bus.
-- Receive W M-bus data with 2000 ms timeout
status, cfield, manid, id, ver, devtype, ci, payload = api.wmbusReceiveFrame(2000)
print(status)
print(id)
print(payload)
Waits timeout milliseconds for data reception from W M-bus.
Only available for W M-bus in concentrator role.
x=string.char(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF)
status, cfield, manid, id, ver, devtype, ci, payload = api.wmbusReceiveSendFrame(200, x, 2000)
print(status)
print(id)
print(payload)
Send data and receive from W M-bus device
This function is only available for W M-bus in meter role.
x=string.char(0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF)
status, cfield, manid, id, ver, devtype, ci, payload = api.wmbusReceiveSendFrame(200, x)
print(status)
print(id)
print(payload)
Fubction and it's operations to set and filter received Wireless M-Bus frame.
This function is only available for W M-bus in meter role.
Sends msg to RS485 bus.
Turn on RS485 using rs485State first.
--sends 'test' string to RS485
api.rs485Send('test')
Change the configuration of RS485 interface.
or
--setup RS485 interface to 9600 Baud, 8E1
api.rs485Setup(9600, 2, 1, 8)
Turns on the RS485 circuitry.
Must be used before rs485Send or rs485Receive.
api.rs485State(0) --turn off RS485
Waits timeout milliseconds for data reception from RS485 bus.
Turn on RS485 using rs485State() function first.
After the first character is received the inter-character (i.e. inter-byte timeout) delay is 10ms. This can be changed by giving a integer number to arg1.
--waits 1s for answer from RS485 bus
ans,len = api.rs485Receive(1000)
--waits 1s for answer from RS485 bus and gets everything until it finds "!" character
--stop garbage collector so it doesn't limit the memory (higher speed, prevent's loosing characters at 9600bd)
collectgarbage("stop")
ans,len = api.rs485Receive(1000,"!")
--restart the garbage collector (the memory will overflow, without it)
collectgarbage("restart")
Calculates Modbus request checksum.
--calculate checksum for Modbus request 110100010002
req = pack.pack('<b6', 0x11,0x01,0x00,0x01,0x00,0x02)
crc = api.modbusCrc(req) --crc = "EE9B"
The api.modbusRTU() function is used to communicate with a modbus device over a serial connection using the RTU (Remote Terminal Unit) protocol. It can be used to read or write one or more registers of various data types. The function takes several parameters, including the modbus device address, the starting register address, the function code, the type specification of the register(s), the number of registers to read or write, and several timeout and retry parameters.
The typeSpec parameter is a Lua string that specifies the data type of the register(s) being read or written. It uses the Lua Packing and Unpacking syntax to specify the type and formatting of the data. For example, to read a single 16-bit unsigned integer, the typeSpec parameter would be "H".
The function returns the value(s) read from or written to the modbus register(s), according to the specified typeSpec. For example, if typeSpec is "H" and registerCount is 2, the function would return two 16-bit unsigned integers.
If the function fails to communicate with the modbus device, it will retry the operation according to the retry parameter. If the operation still fails after the specified number of retries, the function will return nil.
The function returns the value(s) read from the modbus register(s), according to the specified typeSpec.
--setup the RS485 and turn on the RS485 module
api.rs485Setup(9600,2,1,8) api.rs485State(1)
--read 0x4003 register (modbus ID in case of INEPRO PRO1)
x=api.modbusRTU(10,0x4003,3,">h",1,2000,3,10) print(x)
Processes NVT message and either sets baudrate, datasize, parity or stop size for MBUS or MODBUS.
-- If massage received (buf) send it to RS485
ret,port,buf = api.loraSend(0,1000,data)
if buf ~= nil then
buf, nvtans = api.nvtProcess(buf)
api.rs485Send(buf)
end
Encodes message to NVT format
ans,len = api.rs485Receive(50)
ans = api.nvtEncode(ans)
api.loraSend(0,1,ans)
Please check our public GitLab repository HERE. The links to the scripts below are with specific commit sha, therefore it might not be the newest.
Here you can find default LoRaWAN LUA scripts for IoT Converter
ACR_CV_100L_I4_X_s0_emulation_persistent_4_channels_big_endian_rev2.lua
Payload example
Payload | Variable | Value |
---|---|---|
05 | 05 | 05 |
0cf2 | batery | 3.3V |
00021b29 | s01 (current) | 138025 |
00021b15 | s01 (old) | 138005 |
00021aff | s01 (older) | 137983 |
0000b547 | s02 (current) | 46407 |
0000b541 | s02 (old) | 46401 |
0000b53b | s02 (older) | 46395 |
00000000 | s03 (current) | 0 |
00000000 | s03 (old) | 0 |
00000000 | s03 (older) | 0 |
00000015 | s04 (current) | 21 |
00000015 | s04 (old) | 21 |
00000015 | s04 (older) | 21 |
Here you can find default NB-IoT LUA scripts for IoT Converter.