Development

Tigase IoT Framework is an extended version of Jaxmpp Bot Framework which allows you to take all benefits and use all features provided by Jaxmpp Bot Framework, such as usage of Tigase Kernel as IoC container and so on.

Note

For more detailed information about usage of Jaxmpp Bot Framework please look into documentation of Jaxmpp Bot Framework.

<ivalue> <title>Values</title>

By default Tigase IoT Framework requires you to wrap value read from sensor in a class implementing tigase.iot.framework.devices.IValue. This requirement was introduces to bond together timestamp (marking time when value was initially read) with a value and allows you to add additional informations as well, ie. temperature can be passed with correct scale, etc.

Currently we provided following implementations for storing following values:

  • temperature - tigase.iot.framework.values.Temperature
  • light - tigase.iot.framework.values.Light
  • movement - tigase.iot.framework.values.Movement

For this classes we provide you with proper serializers and parsers used for transferring this values over XMPP protocol.

Warning

If you decide to add a new class then you will need to implement and provide Tigase IoT Framework with new parser and serializer (a bean extending AbstractValueFormatter with a support for your newly created implementation of IValue interface).

Warning

Additionally you will need to provide proper support for parsing this data in client library as in other case data from your sensor will not be available in UI.

</ivalue>
<device-types> <title>Device types</title>

In our framework device type is just a string to make it easy to extend it by adding new device/sensor types. Currently we provide devices with following device types and only this device types are supported by UI:

  • movement-sensor
  • tv-sensor
  • light-dimmer
  • light-sensor
  • temperature-sensor

To add support for a new device type, you need to override createDevice() method of Devices classes from client-library module and add there support for new device type. This will add support for a new device type in a client model layer. Additionally you need to add support for this new device inside DevicesListViewImpl to add support in a presentation layer.

Note

This separation on a client side is create for a reason. It is done in such a way to make presentation layer separate from model layer and allow reuse of a model layer in other clients with different presentation layer.

</device-types>

New sensor

To add support for new a new sensor it is required to create a class which bases on tigase.iot.framework.devices.AbstractSensor. This simple base class will take over of all required tasks and provide you with access to configuration and event bus.

Next you need to implement support for reading data/value from your sensor. When you have received new value, then wrap it in instance of class implementing interface tigase.iot.framework.devices.IValue and call updateValue() method from your class extending AbstractSensor. This method will then fire event with new value which will be delivered to every device which will be listening to state changes of your sensor.

Constructor of an AbstractSensor class requires in a parameter a type of a device - string value. This value is later on published in configuration of a device and used by UI to detect device type and use proper controls to display sensor and it’s state. Currently there is only a support for a few device types.

Note

After you have your class implemented, you need to compile it and add to classpath of Tigase IoT Framework project and add it to configuration as a @Bean

Example (support for a PIR sensor - HC SR501). 

/*
 * HC_SR501.java
 *
 * Tigase IoT Framework
 * Copyright (C) 2011-2017 "Tigase, Inc." <office@tigase.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. Look for COPYING file in the top folder.
 * If not, see http://www.gnu.org/licenses/.
 */

package tigase.iot.framework.rpi.sensors.pir;

import com.pi4j.io.gpio.*;
import com.pi4j.io.gpio.event.GpioPinDigitalStateChangeEvent;
import com.pi4j.io.gpio.event.GpioPinListenerDigital;
import tigase.iot.framework.devices.AbstractSensor;
import tigase.iot.framework.devices.IConfigurationAware;
import tigase.iot.framework.values.Movement;
import tigase.kernel.beans.Initializable;
import tigase.kernel.beans.Inject;
import tigase.kernel.beans.UnregisterAware;
import tigase.kernel.beans.config.ConfigField;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
 * Implementation of a sensor responsible for reading data from HC-SR501 sensor.
 *
 * Created by andrzej on 23.10.2016.
 */
public class HC_SR501
		extends AbstractSensor<Movement>
		implements Initializable, UnregisterAware, GpioPinListenerDigital, IConfigurationAware {

	private static final Logger log = Logger.getLogger(HC_SR501.class.getCanonicalName());

	@ConfigField(desc = "WiringPi Pin number")
	private int pin = 21;         // equals to broadcom 5

	@ConfigField(desc = "Delay time in ms")
	private long timeout = 5 * 60 * 1000;

	@Inject
	private ScheduledExecutorService scheduledExecutorService;

	private GpioController gpio;
	private GpioPinDigitalInput input;
	private ScheduledFuture future;

	public HC_SR501() {
		super("movement-sensor", "Motion sensor", "HC-SR501");
	}

	@Override
	public void beforeUnregister() {
		if (input != null) {
			input.removeListener(this);
			gpio.unprovisionPin(input);
		}
	}

	@Override
	public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent event) {
		if (event.getEdge() == PinEdge.RISING) {
			if (future != null) {
				future.cancel(false);
			}

			if (!isMovementDetected()) {
				updateValue(new Movement(true));
			}

			future = scheduledExecutorService.schedule(() -> updateValue(new Movement(false)), timeout,
													   TimeUnit.MILLISECONDS);
		}
	}

	public boolean isMovementDetected() {
		Movement val = getValue();

		if (val == null) {
			return false;
		}

		return val.getValue();
	}

	public void setPin(Integer pin) {
		this.pin = pin;
		if (gpio != null) {
			if (input != null) {
				input.removeListener(this);
				gpio.unprovisionPin(input);
				input = null;
			}
			initializeInput();
		}
	}

	@Override
	public void initialize() {
		super.initialize();
		try {
			gpio = GpioFactory.getInstance();
		} catch (Throwable ex) {
			throw new RuntimeException("Failed to retrieve instance of GpioFactory!", ex);
		}
		if (input == null) {
			initializeInput();
		}
	}

	private void initializeInput() {
		input = gpio.provisionDigitalInputPin(RaspiPin.getPinByAddress(pin), PinPullResistance.PULL_DOWN);
		input.setShutdownOptions(true);

		input.addListener(this);
	}
}

New sensor with periodical reads

If you want to create a support for a sensor which requires reads from actual sensor from time to time, then you should create class extending tigase.iot.framework.devices.AbstractPeriodSensor. This class will allow you to specify how often to read data from a sensor and implement actual read from a sensor inside <T extends IValue> T readValue() method. Inside this method you will also need to wrap value you read in instance of a class implementing tigase.iot.framework.devices.IValue

Constructor of an AbstractSensor class requires in a parameter a type of a device - string value, and default period in which data from sensor will be read. Device type is later on published in configuration of a device and used by UI to detect device type and use proper controls to display sensor and it’s state. Currently there is only a support for a few device types.

Note

After you have your class implemented, you need to compile it and add to classpath of Tigase IoT Framework project and add it to configuration as a @Bean

Note

You may not use AbstractPeriodSensor as your direct base class. We provide you with other classes like I2CAbstractPeriodDevice or W1AbstractPeriodDevice. They all are based on AbstractPeriodSensor and provide same functionality but allow you to access I2C, 1Wire easier.

Warning

Usage of W1AbstractPeriodDevice is tricky. To use it you need to use W1Master bean, enable it and register your implementation within W1Master providing it with key - implementation of com.pi4j.io.w1.W1DeviceType. Classes tigase.iot.framework.rpi.sensors.w1.DS1820 and tigase.iot.framework.rpi.sensors.w1.DS1820DeviceType are good examples of how to do this.

Example (support for I2C sensor - BH1750). 

/*
 * BH1750.java
 *
 * Tigase IoT Framework
 * Copyright (C) 2011-2017 "Tigase, Inc." <office@tigase.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. Look for COPYING file in the top folder.
 * If not, see http://www.gnu.org/licenses/.
 */

package tigase.iot.framework.rpi.sensors.light;

import com.pi4j.io.i2c.I2CDevice;
import tigase.iot.framework.rpi.sensors.I2CAbstractPeriodDevice;
import tigase.iot.framework.values.Light;
import tigase.kernel.beans.Initializable;
import tigase.kernel.beans.UnregisterAware;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Implementation of a sensor which reads data from the connection BH1750 sensor.
 *
 * Created by andrzej on 23.10.2016.
 */
public class BH1750
		extends I2CAbstractPeriodDevice<Light>
		implements Initializable, UnregisterAware {

	private static final Logger log = Logger.getLogger(BH1750.class.getCanonicalName());

	public BH1750() {
		super("light-sensor", "Light sensor", "BH1750", 60 * 1000, "23");
	}

	@Override
	protected Light readValue(I2CDevice device) throws IOException {
		device.write((byte) 0x20);

		byte[] data = new byte[2];

		int r = device.read(data, 0, 2);
		if (r != 2) {
			throw new RuntimeException("Read error: read only " + r + " bytes");
		}

		int v1 = ((data[0] & 0xff) << 8) | (data[1] & 0xff);
		if (log.isLoggable(Level.FINEST)) {
			log.log(Level.FINEST,
					"read data: " + Integer.toHexString(data[0] & 0xff) + Integer.toHexString(data[1] & 0xff) +
							", value = " + v1);
		}

		return new Light(v1, Light.Unit.lm);
	}

}

New device

Not all devices are sensors. In typical use case you will have many devices providing some state by measuring data using external sensors and we call them here sensors. However very often you will have some devices which need to react on changed state of other devices or react on user action - this we call executor devices.

To implement a new executor device you need to create a new implementation of a sensor (executor device is a sensor as well as it reports it current state), which also implements IExecutorDevice inteface:

public interface IExecutorDevice<T> {

	void setValue(T value);

}

Note

In typical usage T should implement IValue interface.

This method will be automatically called if new state will be published for this device (new state will be published on a device state node).

If your device requires to listen to states published by other sensors then it needs to implement NodesObserver with method getObserverdNodes() which needs to return list of state node in which your device is interrested in. Additionally you will have to implement following method:

@HandleEvent
public void onValueChanged(ExtendedPubSubNodesManager.ValueChangedEvent event) {
}

It will be called whenever new state/value will be published by any sensor or a device. You need to check source variable of a ValueChangedEvent to look for a node of a device in which state you are interested in. In value variable of a ValueChangedEvent you will find newly published device state which node you will find in source variable.

Quickstart

Tigase IoT Framework provides you with examples for creating support for a new devices in Java and Python. At https://tigase.tech/projects/tigase-iot-framework-examples/ you will find a project with examples which will help you get started with adding support for a new device. All you need to do is:

Clone a repository containing examples

git clone https://git.tigase.tech/tigase-iot-framework-examples/ tigase-iot-framework-examples

Choose a language of your choice

We provide support for Java and Python, so you may write code for communication in Java or Python and only do some basic mapping in Java to expose your driver to the IoT Framework.

Open project with examples for the language which you selected

You may edit source codes of the project in any text editor you want. But we would suggest usage of some IDE with support for Java and Gradle.

Modify example drivers

At this point, you may select proper template and fill it with your code for communicating with the sensor.

Run your code

To make it easier, our example projects contain gradle run task, so you can start IoT framework with your driver with just one command:

./gradlew run

This command will start Tigase IoT Framework (new instance) and will try to connect to the IoT hub. All configuration of this instances will be stored in etc/config.tdsl file in your examples project directory.

Package your driver

When you are done, you may run

./gradlew jar

which will create a jar file which you may add to your existing Tigase IoT Framework installation.

Note

Jar file will be located at build/libs directory in your examples project directory.

Creating new project using Tigase IoT Framework

Creating project

If you would like to create new project using Tigase IoT Framework, then simplest solution will be to create new project using Gradle with following entries in @build.gradle@ file:

Example project file for Gradle. 

group 'your.project.group'
version 'your.project-version'

apply plugin: 'java'
apply plugin: 'application'

mainClassName = "tigase.bot.runtime.Main"
sourceCompatibility = 1.8
targetCompatibility = 1.8

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
}

repositories {
    maven {url "https://oss.sonatype.org/content/groups/public"}
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile group: 'tigase.iot', name: 'iot-runtime', version: '1.0.0-SNAPSHOT'
    compile group: 'tigase.iot', name: 'iot-devices', version: '1.0.0-SNAPSHOT'
    compile group: 'tigase.iot', name: 'iot-devices-rpi', version: '1.0.0-SNAPSHOT'
}

Now you can focus on implementation of support for devices as described in other parts of this section.

Configuration of a project

If classes containing implementations for your sensors are packaged and available in Tigase IoT Framework class path, then Tigase IoT Framework will load them and allow you to create new devices using drivers which you provided.

Building project

Use of following command will build you project and package it to single zip archive with all dependencies and default configuration for easy deployment.

./gradlew distZip

Running project

To run this project on a device (ie. Raspberry Pi), copy distribution package to a device and unzip archive. Inside in bin directory there are project startup files named same as your project name, ie. test-project

After running following command, project will start and try to connect to XMPP server as defined in project configuration file.

./bin/test-project

Note

It is possible to pass additional options or configuration options during startup. For a detailed description of this options please look into section describing running of Tigase IoT Framework.

Warning

If your project adds new type of sensors or new types of data then you will need to extend client and client-library modules to add support for them before you will be able to see this new device in a UI. Unresolved directive in /opt/teamcity/buildAgent/work/3ded5378ef0287cf/target/dependencies/docs/rel1/rel2/rel3/iot-framework-documentation/index.asciidoc - include::text/User_interface.asciidoc[] == Internal design

In our design IoT devices are connecting to Tigase IoT Hub acting as a central hub for all IoT devices. For communication IoT devices uses PubSub component deployed on XMPP server which acts as a message broker.

Initialization

After connection application creates PubSub nodes for every device or sensor connected to RPi and registered in application. Node name is always generated in following form: devices/iot-device-id, where iot-device-id is ID of a IoT device connected to RPi. This node is created with type collection. This newly created node is always a subnode of root PubSub node used by IoT devices named devices.

As a result for devices with id’s iot-1, iot-2 and iot-3 at this point we have following nodes created:

devices
Root node (collection)
devices/iot-1
Contains data related to iot-1 device (collection)
devices/iot-2
Contains data related to iot-2 device (collection)
devices/iot-3
Contains data related to iot-3 device (collection)

After this is completed then for every device framework creates 2 leaf subnodes for every device:

config
Contains configuration of a device (including information about device type, options, etc.)
state
Contains current state of a device (and previous states depending on configuration)

So for a device listed about we will get following nodes:

devices

Root node (collection)

devices/iot-1

Contains data related to iot-1 device (collection)

devices/iot-1/state
Current state of iot-1 device
devices/iot-1/config
Configuration of iot-1 device
devices/iot-2

Contains data related to iot-2 device (collection)

devices/iot-2/state
Current state of iot-2 device
devices/iot-2/config
Configuration of iot-2 device
devices/iot-3

Contains data related to iot-3 device (collection)

devices/iot-3/state
Current state of iot-3 device
devices/iot-3/config
Configuration of iot-3 device

At this point application is ready for work.

Publishing data from sensors

When a sensor changes it’s state it emits event informing application that it’s state has changed. When application receives this information it serializes it to format similar to format used in XEP-0323: Internet of Things - Sensor Data for data representation and publishes this data as a payload to device’s state PubSub node, ie. for iot-1 measuring light intensity in lm following payload will be published at devices/iot-1/state:

<timestamp value="2016-12-10T19:56:26.460Z">
    <numeric unit="lm" automaticReadout="true" value="37" momentary="true" name="Light"/>
</timestamp>

Reacting on change of sensor state

Executor devices need to listen to changes of state of sensor devices and adjust state. To do so, executor device reports to application which sensors it want’s to observe and application automatically subscribes to particular nodes. After that every change of state published to nodes observer by device will be received from PubSub component by application, which will decode it from XML payload and will fire event which will be forwarded to executor device. This event will contain important information related to this change, like timestamp, value, unit, etc.

Publishing data to device

It is possible to publish value to a device, which allows you to change state of an executor device, ie. publish data forcing light to be turned on. Our framework will automatically detect this change of published state and forward it to executor device resulting in light being turned on.

Configuration

Usually devices keep configuration on device itself. In our design only initial configuration is stored on device, which means that there is no need to locally store additional data between restart of a device.

For configuration storage we use config PubSub node of a particular device. This way a change to a configuration of device, new configuration needs to be published on device configuration node. Updated configuration will be automatically retrieved by application and applied to particular device without restart of an application.

We decided to keep in configuration all important information, ie. list of nodes device observes, etc. Due to that it is possible to retrieve device configuration in a web based client and change it, ie. to change sensor from which temperature is retrieved to adjust temperature in a room.

Configuration in stored in form of a XEP-0004: Data Forms, which makes it flexible and expandable.

User Interface

For user interface we decided to use web based application due to fact that using using web page you can manage your IoT devices from a computer, mobile phone, tablet or smart tv.

It is very simple to retrieve list of devices as every subnode of a devices node represents device. Retrieving config node of that node allows us you easily to retrieve device type and current configuration, while using device type and data retrieved from state node allows to easily retrieve and observe state of a sensor or a device.