Overview
In this section, you will learn how to extend your code in the Cumulocity IoT Linux agent as a Lua plugin. There is also a C++ Device integration tutorial explaining how to integrate your device with the Cumulocity IoT C++ SDK. You can also find a simple Lua example there.
SECTION | CONTENT |
---|---|
Lua plugin tutorial - Hello world | A hello world tutorial that showcases log levels |
Lua plugin tutorial - Sending measurements | How to get values from a configuration file, how to use a timer function and how to send values to the Cumulocity IoT platform with a SmartREST1.0 template |
Lua plugin tutorial - Restart device | A tutorial that showcases how to get real-time notifications and handle operations by restarting a device |
Lua plugin tutorial - Software management | Practical example on how to manage Debian packages with Cumulocity IoT |
File-backed or memory-backed buffering | How to switch between two buffering methods in case of a lost connection |
Lua plugin tutorial - Hello world
Hello world example
For the first example, let’s display “Hello world” as a debug message in the agent log file. Create a hello.lua file under the /lua directory or copy an existing example code using:
cp lua/example/hello.lua lua/
Here is the Lua script.
-- hello.lua: lua/example/hello.lua
function init() -- init() works like main function in C/C++
srDebug("Hello world!") -- Debug
srInfo("Info message") -- Info
srNotice("Notice message") -- Notice
srError("Error message") -- Error
srCritical("Critical message") -- Critical
end
The agent supports the following log levels: “Debug”, “Info”, “Notice”, “Error” and “Critical”.
Change lua.plugins
in your cumulocity-agent.conf file:.
lua.plugins=hello
Lua is a scripting language so that you don’t need to recompile the agent. Just copy your hello.lua file to /usr/share/cumulocity-agent/lua and the modified cumulocity-agent.conf file to /usr/share/cumulocity-agent to make them reachable by the agent. Or, simply run:
sudo make uninstall
sudo make install
Then copy both files to the destination.
Finally, run the agent. You will see that the debug messages are recorded with timestamps in your log file. By default, the path to log file is /var/log/cumulocity-agent.log.
Jun 16 12:57:05 DEBUG: Hello world!
Jun 16 12:57:05 INFO: Info message
Jun 16 12:57:05 NOTICE: Notice message
Jun 16 12:57:05 ERROR: Error message
Jun 16 12:57:05 CRITICAL: Critical message
Info: Once you started the agent and changed some parameters from the Cumulocity IoT tenant (i.e. Measurement sending interval), the agent loads the configurations from /var/lib/cumulocity-agent/cumulocity-agent.conf. In this case, run
sudo make uninstall
to remove the file before copying the modified cumulocity-agent.conf file.
Lua plugin tutorial - Sending measurements
Let’s try sending CPU measurements to Cumulocity IoT. In this section, you will learn how to use the pre-defined timer function, to read parameters defined in cumulocity-agent.conf file and to send measurements using an existing SmartREST1.0 template.
Sending measurements example
This example sends a test CPU usage measurement (20%) to Cumulocity IoT platform using a random interval (10 seconds) which is configured in cumulocity-agent.conf as example.cpu.interval
.
First, add a line to your cumulocity-agent.conf file.
example.cpu.interval=10
Next, create a cpumeasurements.lua file under the /lua directory or copy the existing example code by
cp lua/example/hello.lua lua/
Here is the Lua script.
-- cpumeasurements.lua: lua/example/cpumesurements.lua
local cpuTimer
function init()
local intervalInSec = cdb:get('example.cpu.interval') -- Get the interval from the cumulocity-agent.conf file
cpuTimer = c8y:addTimer(intervalInSec * 1000, 'sendCPU') -- Add the timer to the agent scheduler
cpuTimer:start() -- Start the timer
return 0
end
function sendCPU()
local value = 20 -- Test CPU usage (20%)
c8y:send("326," .. c8y.ID .. ',' .. value) -- Send the test CPU usage percentage to the Cumulocity IoT as measurments
end
cdb:get(key) -> value
returns the value of the corresponding key set in your cumulocity-agent.conf. It is very useful if you want to have custom configurable variables. In the Lua script, cdb:get('example.cpu.interval')
returns 10
as configured above.
c8y:addTimer(interval, callback) -> timer
needs two arguments. The first argument is the interval of your timer in milliseconds. The second argument is a function to a callback. It returns a timer object.
timer:start()
starts the timer object. In this example, cpuTimer:start()
. To learn more about the functions of the timer object, check your cumulocity-sdk-c/src/master/doc/lua.html file.
The function sendCPU()
is called every 10 seconds. It creates a CPU usage measurement value (set to 20%) and sends it to the Cumulocity IoT platform.
c8y:send(request, prio)
can have two arguments. The second argument is optional. The first argument is a comma-separed request. In this example, 326
means that this request will be translated to template No.326 in srtemplate.txt. No.326 is
10,326,POST,/measurement/measurements,application/json,,%%,DATE UNSIGNED NUMBER,"{""time"":""%%"",""source"":{""id"":""%%""},""type"":""c8y_CPUMeasurement"",""c8y_CPUMeasurement"":{""Workload"":{""value"":%%,""unit"":""%""}}}"
c8y.ID
returns your device ID. Thus, the content of the sending request is actually 326,<device ID>,20
. To learn more about SmartREST1.0, refer to the Reference guide > SmartREST section. The second argument of c8y:send()
is optional so it is omitted in this example. The detail of c8y:send()
is documented in your cumulocity-sdk-c/src/master/doc/lua.html file.
Before you run the agent again, change lua.plugins
in your cumulocity-agent.conf file:
lua.plugins=hello,cpumeasurments
Deploy cpumeasurements.lua like the Hello world example. Then run the agent. You can check your device in the Device Management application. The CPU Measurement (20%) will be reported periodically.
Lua plugin tutorial - Restart device
Besides sending requests, e.g., measurements to the Cumulocity IoT platform, another important function is handling incoming messages from Cumulocity IoT; either responses from GET queries or real-time operations.
Here, two examples are presented. The first example only shows you how to handle the c8y_Restart
operation in Lua. It is a simplified version of the ex-06-lua example in the Cumulocity IoT C++ SDK. The second example shows you a more practical implementation including saving the operation ID after rebooting.
Restart device example - simple
First, this example sends the operation status EXECUTING when it receives the c8y_Restart
operation. Then, it logs “Executing restart..” in the log file, and sends SUCCESSFUL as the operation status update to the server.
In the beginning, the agent needs to send c8y_Restart
as c8y_SupportedOperations
to notify this agent can handle restart operation.
Edit the src/demoagent.cc file like this to add Q(c8y_Restart)
.
const char *ops = ",\"" Q2(c8y_Command) Q(c8y_ModbusDevice) Q(c8y_SetRegister)
Q(c8y_ModbusConfiguration) Q(c8y_SerialConfiguration) Q(c8y_SetCoil)
Q(c8y_LogfileRequest) Q(c8y_RemoteAccessConnect)
Q(c8y_CANopenAddDevice) Q(c8y_CANopenRemoveDevice)
Q(c8y_CANopenConfiguration) Q(c8y_Restart)"\"";
Then recompile your agent. Now your agent will send the c8y_Restart
operation when it starts up.
Next, create a restart-simple.lua file under the /lua directory or copy the existing example code
cp lua/example/restart-simple.lua lua/
Here is the Lua script.
-- restart-simple.lua: lua/restart-simple.lua
function restart(r)
srDebug('Agent received c8y_Restart operation!')
c8y:send('303,' .. r:value(2) .. ',EXECUTING', 1)
srDebug('Executing restart..')
c8y:send('303,' .. r:value(2) .. ',SUCCESSFUL', 1)
end
function init()
c8y:addMsgHandler(804, 'restart')
return 0 -- signify successful initialization
end
c8y:addMsgHandler(MsgID, callback)
registers a message callback for the message ID. In this example, the message ID is 804, which is:
11,804,,$.c8y_Restart,$.id,$.deviceId
11
means it is a response template. 804
is the message ID. The blank field is a base JSON path. $.c8y_Restart
is a conditional JSON path, which is necessary for this example to identify the operation. $.id
receives the operation ID and $.deviceId
holds the device ID. For more details on the SmartREST response template, refer to Reference guide > SmartREST > Template.
When the agent receives the message ID, this message handler triggers to invoke restart()
. r
is the recursive variable. So, r:value(2)
points the received operation ID.
The operation status needs to transit PENDING->EXECUTING->SUCCESSFUL/FAILED. The agent needs to update the operation status to EXECUTING first. This is what
c8y:send('303,' .. r:value(2) .. ',EXECUTING', 1)
is doing. In practice, the agent needs to execute reboot afterwards, but since this is a simple example, replace it by logging debug message “Executing restart..”. This message will be buffered when the connection gets lost as the message priority is marked 1
.
After finishing the execution, the agent needs to inform that it is done using the following code.
c8y:send('303,' .. r:value(2) .. ',SUCCESSFUL', 1)
In case of failure, you can also mark FAILED with failure reason by using message template 304.
c8y:send('304,' .. r:value(2) .. ',Write your failure reason')
Now, it is your time to try it out. Before you run the agent again, change lua.plugins
in your cumulocity-agent.conf file:
lua.plugins=hello,cpumeasurments,restart-simple
Deploy restart-simple.lua like Hello world example. Then run your agent.
Now go to your Cumulocity IoT tenant, execute a restart operation as shown in the image below. Afterwards, you should see the message printed in the log file and the operation status set to SUCCESSFUL in your control tab.
Restart device example - practical
The first example does not execute the real rebooting command. For practical usage, you need to take into account how to keep the operation ID before/after rebooting a device.
Here is the easiest example to overcome this problem.
-- restart.lua: lua/example/restart.lua
local fpath = '/usr/share/cumulocity-agent/restart.txt'
function restart(r)
c8y:send('303,' .. r:value(2) .. ',EXECUTING', 1)
local file = io.open(fpath, 'w')
if not file then
c8y:send('304,' .. r:value(2) .. ',"Failed to store Operation ID"', 1)
return
end
file:write(r:value(2)) -- write the operation ID to the local file
file:close()
local ret = os.execute('reboot')
if ret == true then ret = 0 end -- for Lua5.2 and 5.3
if ret == nil then ret = -1 end -- for Lua5.2 and 5.3
if ret ~= 0 then
os.remove(fpath) -- remove the local file when error occurs
c8y:send('304,' .. r:value(2) .. ',"Error code: ' .. ret .. '"', 1)
end
end
function init()
c8y:addMsgHandler(804, 'restart')
local file = io.open(fpath, 'r')
local opid
if file then -- file should be exist after rebooting
opid = file:read('*n')
file:close()
os.remove(fpath) -- delete the temporary local file
end
if opid then
c8y:send('303,' .. opid .. ',SUCCESSFUL', 1)
end
return 0
end
It stores the operation ID in a local file before triggering the reboot
command. After the reboot, the agent sends SUCCESSFUL with the stored operation ID to the server.
os.execute()
is a Lua command, which is equivalent to the C system()
function. It passes commands to be executed by an operating system shell. os.execute('reboot')
calls the Linux reboot command. You can adjust it for your system.
Lua plugin tutorial - Software management
For the last example, let’s write a script to support software management. For details on our software management feature, refer to Device Management > Managing device data.
Software management example
This section introduces a Lua plugin that handles c8y_SoftwareList
operation, sending the installed package list to the Cumulocity IoT platfrom and triggering the installation or removal of packages from there.
This example assumes that the device supports Debian packages.
First, the agent needs to send c8y_SoftwareList
as c8y_SupportedOperations
as we did in the restart example section.
Edit src/demoagent.cc and add Q(c8y_SoftwareList)
. Then recompile the agent.
Now the agent will send a c8y_SoftwareList
operation when it starts up.
Create a software.lua file under the /lua directory or by copying the existing example code.
cp lua/example/software.lua lua/
Let’s take a look at the example code step by step.
In the beginning, you can find the apt
commands to install/remove/list Debian packages. If your device supports a different package controlling system, modify this part.
-- Linux commands
local cmd_list = 'apt list --installed'
local cmd_install = 'apt install -y'
local cmd_remove = 'apt remove -y'
-- File extention
local file_ext = '.deb'
Next, go into the init() function.
function init()
c8y:addMsgHandler(837, 'clear')
c8y:addMsgHandler(814, 'aggregate')
c8y:addMsgHandler(815, 'perform')
c8y:send('319,' .. c8y.ID .. ',' .. pack(pkg_list()))
return 0
end
Before receiving any operation, it sends a list of installed software with message template 319
. You can find the c8y_SoftwareList
format in the Device information guide.
pkg_list()
returns a table. If your package control system is not apt
, you also need to change how to extract software names and versions from the command you defined.
local function pkg_list()
local tbl = {}
local file = io.popen(cmd_list)
for line in file:lines() do
-- This pattern needs to be modified if not using apt
local name, version = string.match(line, '([%w%-%.]+)/.- (.-) .+')
if name and version then tbl[name] = version end
end
file:close()
return tbl
end
If you create any c8y_SoftwareList
operation from the UI, the agent will receive the list of software packages which are supposed to be installed. In other words, the agent also receives information about unchanged packages with the message template 814
. The aggregate
function sums up information about all received packages in a table.
After the aggregation finishes, the perform
function is called. The function:
- Updates the operation status to EXECUTING
- Validates package names
- Creates a list for software packages to be installed
- Creates a list for software packages to be removed
- Downloads software packages from the server (inventory/binaries)
- Removes software packages by the pre-defined command
- Installs software packages by the pre-defined command
- Updates the operation status to FAILED (with a reason) if any of the above tasks failed
- Updates the operation status to SUCCESSFUL
- Sends the updated package list to the server
Before you run the agent again, change lua.plugins
in your cumulocity-agent.conf file:
lua.plugins=hello,cpumeasurments,restart,software
Deploy software.lua like the Hello world example. Then run the agent.
Now go to your Cumulocity IoT tenant, create a software operation. You’ll see the operation is managed by this script.
Info: MQTT connection has a payload limit. If the result of
cmd_list
(e.g.apt list --installed
) is huge, the agent might fail to send its package list. It is recommended to drop uninteresting packages from the sending list or pick up only interesting packages. For example, if you want to manage onlylua
andmodbus
packages, you can definecmd_list
toapt list --installed | grep -e lua -e modbus
.
File-backed or memory-backed buffering
The Cumulocity IoT C++ SDK offers two message buffering methods in case of connection lost. To switch it, you just need to change the arguments to be parsed to SrReporter()
. The Cumulocity IoT Linux agent chooses memory-backed buffering by default.
The user guide of the C++ SDK mentions the pros and cons of each buffering technique in the SrReporter()
description.
Info: It implements a capacity limited buffering technique for counteracting long periods of network error. This buffering technique can be further categorized into memory backed and file backed buffering. Memory backed buffering is more efficient since there is no file I/O involved, while the buffering is limited by the available memory and doesn’t survive a sudden outage. Oppositely, file backed buffering performs a lot of file I/O operations, but at the same time, its capacity is much larger and buffered messages will not be lost in case of a sudden outage.
This section shows you how to change to file-backed buffering method in the Cumulocity IoT Linux agent. Change from
rpt = new SrReporter(server + port, agent.deviceID(), agent.XID(),
agent.tenant() + '/' + agent.username(),
agent.password(), agent.egress, agent.ingress);
to
rpt = new SrReporter(server + port, agent.deviceID(), agent.XID(),
agent.tenant() + '/' + agent.username(), agent.password(),
agent.egress, agent.ingress, 10000, "/path/to/file.cache");
You just need to add two new arguments when constructing the SrRepoter
object:
- 10000: Capacity of the request buffer
- /path/to/file.cache: The file path for file-backed buffering