Linux Agent Developer guide

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. cpumeasurments

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. restarted-device

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. restarted-device

After the aggregation finishes, the perform function is called. The function:

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 only lua and modbus packages, you can define cmd_list to apt 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: