15 KiB
SDK - Using the Application Framework
Introduction
To simplify application development, the OpenMTC SDK provides an application framework for writing oneM2M compliant applications with minimal effort.
This framework allows specifying application parameters in a declarative way and is furthermore fully event driven.
The OpenMTC application framework resides in the openmtc_app.onem2m
module.
Application Basics
To write an application a conventional Python class has to be implemented. There are only two restrictions on this class:
- It must derive from the provided base class
openmtc_app.onem2m.XAE
- It must override the _on_register method.
The overall flow of operations is simple. Upon startup, the framework will register the application with the CSE and once that has successfully happened, call the _on_register
method of the application. This is the entry point where the application can start actually doing things. This includes:
- Creating some containers
- Creating more applications
- Subscribe for data from the CSE
- Setup any device specific communication
Upon shutdown, the application itself and any resources it has created will automatically be deleted by the framework.
Note: The framework also automatically handles expiration time updates for all resources that have been created.
Properties
The following properties are available on objects of type openmtc_app.onem2m.XAE
:
application
- theApplication
object that represents this application in the CSE's resource tree.client
- an instance ofopenmtc_onem2m.client.http.OneM2MHTTPClient
connected to the CSElogger
- alogging.Logger
object. Please refer to the Python logging documentation for details.
Methods
The following methods are available on an openmtc_app.onem2m.XAE
object:
-
get_application(application, path)
- used to retrieve an Application resource.
application
old app instance or appIdpath
(optional) path in the resource tree- Returns the
Application
object that was retrieved
-
create_application(application, path)
- used to create a new Application resource in the CSE's resource tree, besides the one that has been registered at startup.
application
Application instance or appId as strpath
(optional) path in the resource tree at which the new application should be created.- Returns the
Application
object that represents the application that has been created at the CSE.
-
discover(path, filter_criteria, unstructured)
- used to discover resources.
path
the target resource/path parenting the Container. e.g. the remote CSEfilter_criteria
(optional) FilterCriteria for the for the discoveryunstructured
(optional) setdiscovery_result_type
- Returns the content of the discovery
-
create_container(target, container, labels, max_nr_of_instances)
- used to create a Container resource as children of an application.
target
the target resource/path parenting the Containercontainer
the Container resource or a valid container IDlabels
(optional) the container's searchStringsmax_nr_of_instances
the container's maximum number of instances (0=unlimited)- Returns the container that was created
-
get_resource(path, app_local)
- used to retrieve a resource
path
is the path to the resourceapp_local
if set toTrue
path will be appended to the path of the Application
-
push_content(container, content, fmt, text)
- used to push actual data into a container. Therefore creates a ContentInstance resource in the given container, wrapping the content. Defaults to serialising the content as JSON and base64 encodes it. Will attempt to create the container, if not found.
container
container at which the content should be createdcontent
the actual data. Currently, only Pythonstring
,list
anddict
objects are supported.fmt
(optional)text
(optional)- Returns the created ContentInstance resource
-
get_content(container)
- used to retrieve the latest ContentInstance of a Container.
container
container to retrieve content from- Returns the latest ContentInstance of the specified Container
-
add_container_subscription(container, handler, data_handler, filter_criteria)
- used to create a Subscription to the ContentInstances of the given Container.
container
the Container or it's pathhandler
reference of the notification handling functiondata_handler
(optional) reference of the function parsing/decoding the datafilter_criteria
(optional) FilterCriteria for the subscription
-
emit(message, event)
- publish data via
socket.io
to all connected clients. message
is the data to be pushed.event
specifies thesocket.io
event channel name.
- publish data via
Application Shutdown
Upon shutdown, the shutdown
method will be called with no arguments. Implementations may override it to implement their own clean-up facilities.
After the shutdown
method has finished, all resources created by the application framework, including the application itself, will be removed.
Writing Applications
This example shows how a simple device application might be written. The goal of this application is to first register itself with the CSE and then continuously read sensor data from some hardware device and push it to the CSE.
Note that the actual sensor reading parts of the application is left out - it is represented simply by an imagined read_sensor_data
method. This example will solely focus on the OpenMTC SDK aspects of the application.
A minimal Application
A minimal application is simply a class that extends the application base class openmtc_app.onem2m.XAE
and overrides the _on_register
method. Such an application could look like this.
This file can be found here.
# Example 13: Minimal application
from openmtc_app.onem2m import XAE
class MyAE(XAE):
# when this is called the application is registered
# and can start doing something
def _on_register(self):
pass
Upon running, this application would be registered under the name MyAE.
Running the Application
Applications are run by encapsulating them in a so-called runner. These runners provide external functions such as maintaining a server component for receiving notifications.
Currently, two runner implementations are provided, both built upon the popular Flask framework. These are:
openmtc_app.flask_runner.SimpleFlaskRunner
openmtc_app.flask_runner.FlaskRunner
The difference between these two is that FlaskRunner
provides some additional functionality, which is not included in SimpleFlaskRunner
. This is mainly related to web-browser support/integration:
FlaskRunner
supports websocksets through the socket.io abstraction layer.FlaskRunner
is more scalable.
However, in certain cases the simpler implementation of SimpleFlaskRunner
can have advantages as well:
FlaskRunner
is built upon the gevent asynchronous I/O framework. While this provides for a very scalable solution, it might produce undesirable results and strange behaviour when applications implement their own I/O mechanisms, e.g. for communicating with a device. In contrastSimpleFlaskRunner
will not exhibit these pitfalls.- While
FlaskRunner
provides a more scalable solution when a lot of I/O happens, the resource consumption ofSimpleFlaskRunner
will be less in cases where only few requests are performed - especially when requests are performed strictly sequential.
Hint: Both implementations will automatically serve static www content from a static
directory in the application's root directory.
To invoke a runner, we have to:
- instantiate our application class
- instantiate the runner class, passing in the application object in the runner's constructor
- invoke the runner's
run
method, passing the URI of the CSE we want to communicate with
The following example shows how this is done:
This file can be found here.
# Example 14a: Invoking a FlaskRunner
from openmtc_app.onem2m import XAE
from openmtc_app.flask_runner import FlaskRunner
class MyAE(XAE):
def _on_register(self):
pass
app_instance = MyAE()
runner = FlaskRunner(app_instance)
runner.run("http://localhost:8000")
Note: This example (and most of the following ones) will only work as shown, if a gateway
instance is running in the background of the localhost. This can be launched by running the openmtc-open-source/openmtc-gevent/run_gateway
script.
At this point the runner will start the app which results in the app being registered at the CSE. Once that has happened, the _on_register
method will be called upon which our application can actually start its operation.
The above example will register the application under its default application ID, which in this case, is derived from the name of the application's class: MyAE
. In some cases - for example when multiple instances of the same application class are run against the same CSE - it may be desirable to override the default application ID. This can be achieved simply by setting the name
parameter when instantiating the application class.
This file can be found here.
# Example 14b: Invoking a FlaskRunner with custom name
from openmtc_app.onem2m import XAE
from openmtc_app.flask_runner import FlaskRunner
class MyAE(XAE):
def _on_register(self):
pass
app_instance = MyAE(name="someAppName")
runner = FlaskRunner(app_instance)
runner.run("http://localhost:8000")
The invocation in this example tells the framework to register the application under the ID someAppName
.
Providing additional Information
We can provide/override some static configuration information about our application in a declarative manner. This information includes:
app_id
- The default application ID.name
- The default application name. (default = None)labels
- The default application labels.default_lifetime
- The default application lifetime. (default = 3600)max_nr_of_instances
- The default application maximum number of instances. (default = 3)cse_base
- The default application CSE-base. (default = "onem2m")
This file can be found here.
# Example 15: Running App with Static Information
from openmtc_app.onem2m import XAE
class MyAE(XAE):
app_id = "AnotherAppID"
labels =["keyword1", "keyword2"]
In the above example, the application would by default be registered under the ID anotherAppID
. Moreover, its labels
attribute would be set to ["keyword1", "keyword2"]
.
Creating a Container
At this point, the next step for most if not all device applications will be to create a container to store any sensor data it reads. This is achieved by calling the create_container
method.
This file can be found here.
# Example 16: Creating a simple Container
from openmtc_app.onem2m import XAE
from openmtc_app.flask_runner import FlaskRunner
class MyAE(XAE):
def _on_register(self):
container = self.create_container(None, "myContainer")
app_instance = MyAE()
runner = FlaskRunner(app_instance)
runner.run("http://localhost:8000")
Note how we pass None
as the first parameter to create_container
. This implies to the framework that the container should be created as a child of the registered application (this invocation is thus equivalent to self.create_container(self.application, "myContainer"
). We could pass different application objects in the first parameter which we would have obtained by calling self.create_application
.
Also note how we pass a simple string object as the second parameter. This will serve as the id
attribute of the container. The framework will internally create a Container
object with the specified ID which will otherwise be configured with some default parameters. If we wanted to provide more details upon container creation, we would have to pass in a full container object.
This file can be found here.
# Example 17: Creating a custom Container
from openmtc_app.onem2m import XAE
from openmtc_app.flask_runner import FlaskRunner
from openmtc_onem2m.model import Container
class MyAE(XAE):
def _on_register(self):
# create a container
container = Container(
resourceName = "myContainer",
maxNrOfInstances=100,
maxByteSize=1024 ** 3 )
container = self.create_container(None, container)
app_instance = MyAE()
runner = FlaskRunner(app_instance)
runner.run("http://localhost:8000")
Here we create an explicit Container
object which limits the size of the container to a maximum of 100 content instances and a maximum size of 3 MByte.
Pushing Data
The final step for our application is to read data from the actual sensor hardware and forward it to the CSE as a content instance. As mentioned before, reading the data is represented by a fictional function read_sensor_data
, the implementation of which is out of scope for the purpose of this document. In our imagination it will return a single value.
The modus operandi of this particular application is to simply read sensor data using the aforementioned function, forward it to the CSE and then wait 60 seconds before starting the whole procedure again. This will be done over and over again, thus we will encapsulate this scheme in an endless loop.
This file can be found here.
# Example 18: Pushing Data
from openmtc_app.onem2m import XAE
from openmtc_app.flask_runner import FlaskRunner
from time import sleep
from somewhere import read_sensor_data
class MyAE(XAE):
def _on_register(self):
container = self.create_container(None, "myContainer")
while True:
value = read_sensor_data() # read measurements
data = {"value": value}
self.push_content(container, data)
sleep(60)
app_instance = MyAE()
runner = FlaskRunner(app_instance)
runner.run("http://localhost:8000")
Note how the actual forwarding of the data is performed by using the push_content
method. push_content
takes as first argument a Container
object that specifies the destination container of the data. The second argument is simply the data that is to be pushed in the form of a Python dict
or list
object. Internally, the framework will wrap this in a ContentInstance
object and forward it to the CSE.