Skip to content

Lighting Sample Walkthrough

Carsten Stocklöw edited this page Apr 24, 2018 · 16 revisions

Table of Contents

Overview about the tasks

In this section, we till take a look at the Lighting Sample which is located in the samples repository. It is separated into a server project that will manage some virtual light sources and their brightness, and a client project that allows to query all light sources and switch them on or off as described in the section Running the lighting sample.

Here we will give only a quick overview about the meaning of all classes and the basic part of the code. Our example is a shorted version of that example, which is concentrating only at the functionality to switch on/off the lamps to have some code for further usage.

The Lighting ontology

The lighting sample uses the lighting ontology.

The integration of the lighting ontology is done by adding the following <dependency> to the maven pom.xml (you may need to adapt the version number to the version you are using):

 <dependency>
        <groupId>org.universAAL.ontology</groupId>
        <artifactId>ont.lighting</artifactId>
        <version>3.4.0</version>
 </dependency>

Please note that client and server have no direct dependency to each other. They only communicate via the lighting ontology.

The Server Project

The project smp.lighting.server.osgi manages a set of four virtual light sources. The classes in the main package org.universAAL.samples.lighting.server implement the interaction with the universAAL buses whereas the classes in the package org.universAAL.samples.lighting.server.unit_impl are meant to implement the interaction with a low-level protocol (like KNX, Z-Wave..) to control the actual physical light sources. In this example, the light sources are only virtual and their current state (of/off) is shown in a GUI. However, you can use this example to attach your own binding of a low-level protocol to universAAL.

The class "ProvidedLightingService"

The class ProvidedLightingService is used to define the Service Profiles of the lighting server. Using such a helper class for the definition of our Service Profiles allows us to classes small. Note that this is not a must, though. If you prefer, you can define the Service Profiles for your Service Callee application in the same class and this is perfectly fine.

 public class ProvidedLightingService {
    public static final ServiceProfile[] profiles = new ServiceProfile[4];

First thing we need to do is to define a URI for the namespace of the server. Therefore we add following attribute to the class:

 public static final String LIGHTING_SERVER_NAMESPACE = System.getProperty(
    "org.universAAL.middleware.samples.lighting.server.namespace",
    "http://sample.universAAL.org/LightingServer.owl#");

As long as the namespace is a valid URI prefix there are no futher restrictions about the naming. In this example, we allow for the namespace to be configurable by the system property org.universAAL.middleware.samples.lighting.server.namespace. That way, you can start up different lighting servers in different instances of the middleware.

Next we need three additional URIs, one for every function we want to provide in our service. So we will add the following attributes to ProvidedLightingService:

  // allows return of controlled lamps
  static final String SERVICE_GET_CONTROLLED_LAMPS = LIGHTING_SERVER_NAMESPACE + "getControlledLamps";
  // allows return of information about the controlled lamps
  static final String SERVICE_GET_LAMP_INFO = LIGHTING_SERVER_NAMESPACE + "getLampInfo";
  // allow to switch off a light
  static final String SERVICE_TURN_OFF = LIGHTING_SERVER_NAMESPACE + "turnOff";
  // allow to switch on a light
  static final String SERVICE_TURN_ON = LIGHTING_SERVER_NAMESPACE + "turnOn";

There are no restrictions for the name of this attributes except of best practice like shown above (e.g. capital letters for the static variables, meaningful names, ...) and that the values must begin with the previously defined namespace (LIGHTING_SERVER_NAMESPACE). For every function like turnOff we will later create a so called ServiceProfile (respective its own instance of a service-based class). This profile is a well defined description what a service can handle and what outputs we can expect. In this very simple version of the lighting example we only want to provide services to switch on/off the lamps or get the lamps controlled by the server. But for the profiles to switch on/off the lamps we will later need an input that indicated which lamp we want to change and we need an output for the controlled lamps. Also for this in-/outputs we need to define URI's and therefore new attributes:

 static final String INPUT_LAMP_URI = LIGHTING_SERVER_NAMESPACE + "lampURI";
 static final String OUTPUT_CONTROLLED_LAMPS = LIGHTING_SERVER_NAMESPACE + "controlledLamps";

Now we will implement step by step the static part of the class. First we start the static block somewhere in the class (best after the static attributes)

 static {

At next we define two variables that will simplify the later code:

  String[] ppControls = new String[] { Lighting.PROP_LIGHT_SOURCE};
  String[] ppBrightness = new String[] { Lighting.PROP_LIGHT_SOURCE, LightSource.PROP_BRIGHTNESS};  

The "pp" at the beginning of the local variables is a short for Property-Path. If we are later going to access properties (reading, changing, ...) a Property-Path denotes the path in the RDF-Graph. So ppControls indicates the path to the controlled lamps from the server. ppBrightness indicates the property PROP_BRIGHTNESS from the controlled lamps. If ppBrigthness would be a object-property we could continue this list.

Now we can define our needed services:

  Lighting getControlledLamps = new Lighting(SERVICE_GET_CONTROLLED_LAMPS);
  getControlledLamps.addOutput(OUTPUT_CONTROLLED_LAMPS, LightSource.MY_URI, 0, 0, ppControls);
  profiles[0] = getControlledLamps.getProfile();

  Lighting turnOff = new Lighting(SERVICE_TURN_OFF);
  turnOff.addFilteringInput(INPUT_LAMP_URI, LightSource.MY_URI, 1, 1, ppControls);
  turnOff.getProfile().addChangeEffect(ppBrightness, new Integer(0));
  profiles[1] = turnOff.getProfile();

  Lighting turnOn= new Lighting(SERVICE_TURN_OFF);
  turnOn.addFilteringInput(INPUT_LAMP_URI, LightSource.MY_URI, 1, 1, ppControls);
  turnOn.getProfile().addChangeEffect(ppBrightness, new Integer(100));
  profiles[2] = turnOn.getProfile();
  ...

First we are going to create the service to get the controlled lamps of the server. Therefore we create a new instance of Lighting by passing the URI we want to use for the service (SERVICE_GET_CONTROLLED_LAMPS). The only thing this service has is an output which we define by using addOutput from the class Service. Here we need pass the URI of the output (parameter 1) first. With parameter 3 and 4 (max./min. cardinality) we specify that the service delivers an indefinite number of LightSource (parameter 2) objects that are controlled by this class of services (parameter 5).

Both following definitions are quite similar, but have different values (on=100, off=0) to be set for the source brightness. First we create for both a new instance of Lighting by passing the URI of the service we want to create (SERVICE_TURN_ON or SERVICE_TURN_OFF). Next is to indicate that we need an input for this service. This can be done by using addFilteringInput. Here this adds a filter to the path ppControls which let only passes one device that is of type LightSource. Finally we use the provided method addChangeEffect (getProfile() is a public method with return value of type ServiceProfile and is part of the base class Service) to indicate the property we are want to change and the value to set (could be also done by an appropriate additional input).

Maybe you ask yourself at this point "Why do I need to have this profiles defined in this way?". Here we have one main features using the semantic middleware universAAL. Later, when services are requested, we do not need to give the concrete Name of a profile (and therefore a special function to perform). To a request we will only add the inputs we want to give and the outputs we expect and the middleware use the profile (from e.g. all available services based on Lighting) that best fits to our needs. This allows us to decouple caller and callee completely.

Last thing to do is to save the profiles in the array profiles.

The class "LightingProvider"

Now we have created the services with its needed profiles and it is up to provide them on the Service-Bus what we will do in the class LightingProvider. Like you can see there the class is based on ServiceCallee.

 public class LightingProvider extends ServiceCallee {

ServiceCallee is the base for all classes that want to provide services. To handle everything up to best practice we define a URI prefix for the lamps we want to control:

  static final String LAMP_URI_PREFIX = ProvidedLightingService.LIGHTING_SERVER_NAMESPACE + "controlledLamp";
Also we create an instance of MyLighting that holds our "virtual lamps":
  private MyLighting theServer = new MyLighting();;
At next we are going to add a universAAL specific attribute:
  private ContextPublisher cp; // will be created in the constructor
The ContextPublisher is located in org.universAAL.middleware.context. After a value of a lamp has changed by handling an appropriate service-call, we want to publish this change an the context-bus. Therefore we will need a ContextPublisher for further use.

To every service call there is an appropriate response. In case the call succeeds it contains the requested outputs and is marked as successful. If there is some error in handling of the call (e.g. a wrong or missing input) we need to response with an error message. Here we are using a standard response:

  private static final ServiceResponse invalidInput = new ServiceResponse(CallStatus.serviceSpecificFailure);
  static {
        invalidInput.addOutput(new ProcessOutput(ServiceResponse.PROP_SERVICE_SPECIFIC_ERROR,"Invalid input!"));
  } 
ServiceResponse is a provided class of the middleware also like org.universAAL.middleware.service.CallStatus. In the static part we are add an output to the result including a short message ("Invalid input") by using function addOutput provided by ServiceResponse. This needs to have a org.universAAL.middleware.service.owls.process.ProcessOutput as input. Will see this again later so here it is enough to see that we handle a standard value (ServiceResponse.PROP_SERVICE_SPECIFIC_ERROR) and a message ("Invalid input!").

The constructor is given like in the following:

  protected LightingProvider(ModuleContext context) {
     super(context, ProvidedLightingService.profiles);
     ContextProvider info =  new ContextProvider(ProvidedLightingService.LIGHTING_SERVER_NAMESPACE + "LightingContextProvider");
     // every ContextProvider has a type that give a first idea of its background
     info.setType(ContextProviderType.controller);
     // create the default Context-Publisher
     cp = new DefaultContextPublisher(context, info);
     ...
  }
Every Context-Publisher is part of an org.universAAL.middleware.context.owl.ContextProvider. We create this by passing a well suited URI, add the type of it (info.setType and passing a org.universAAL.middleware.context.owl.ContextProviderType) and use it to create the DefaultContextPublisher.

Now we come to the "core-method" of every service-callee:

    public ServiceResponse handleCall(ServiceCall call) {
       String operation = call.getProcessURI();
 
       if (operation.startsWith(ProvidedLightingService.SERVICE_GET_CONTROLLED_LAMPS))
          return getControlledLamps();
 
       Object input = call.getInputValue(ProvidedLightingService.INPUT_LAMP_URI);
 
       if (operation.startsWith(ProvidedLightingService.SERVICE_TURN_OFF))
          return turnOff(input.toString());
 
       if (operation.startsWith(ProvidedLightingService.SERVICE_TURN_ON))
          return turnOn(input.toString());
 
       return null;
   }
To handleCall every valid request at the service-bus that matches to one of our services is passed from the middleware. Here we use the defined URIs of the services to determine the needed request. With call.getProcessURI() we get the URI of the profile that had best fit to the original request passed from the service-bus (if there was any similarities) to the local node. First the class ServiceCall provides the function getProcessURI, which is at least in the prefix similar to the Service-URIs. Therefore we simply have to show if it corresponds to our defined profiles and create/return an appropriate request (the individual methods are handled below). For SERVICE_GET_CONTROLLED_LAMPS we do not have any input, but for the other both services. To get the input we can use getInputValue and passing the URI of the input we want to have. Now lets have a look on how we are going to create a ServiceResponse in the getControlledLamps method:
 private ServiceResponse getControlledLamps() {
    ServiceResponse sr = new ServiceResponse(CallStatus.succeeded);
    sr.addOutput(new ProcessOutput(SCalleeProvidedService.OUTPUT_CONTROLLED_LAMPS,
       controlledLamps));
    return sr;
 }
As you can see we are first creating a ServiceResponse object with status "succeeded". To add the output we create a ProcessOutput where we pass the URI of the output and the list of controlled lamps (must be a list, not an array!). The handling of the lamp switch request is also quite simple:
 private ServiceResponse turnOn(String lampURI) {
    try {
       theServer.turnOn(extractLocalIDfromLampURI(lampURI));
       return new ServiceResponse(CallStatus.succeeded);
    } catch (Exception e) {
       return invalidInput;
    }
 }
First we are parsing the index of the lamp from the passed URI and then change the value of the appropriate lamp. If you want to connect real light-source (e.g. via KNX) here is the place to add calls to native libs or whatever. In case the last letter of the URI is not a number we return the previous defined error-response invalidInput.

The internal MyLighting server (in package ..unit_impl) is implemented in a way that changes in a light source brightness will cause a call of the method lampStateChanged, and calling theServer.turnOn(..) will also trigger such a notification. This is the place to publish an appropriate context event:

 public void lampStateChanged(int lampID, String loc, boolean isOn) {
    // Create an object that defines a specific lamp
    LightSource ls = new LightSource(constructLampURIfromLocalID(lampID));
    // Set the properties of the light (location and brightness)
    ls.setBrightness(isOn ? 100 : 0);
    // create a context event and publish it with the light source
    // as subject and the property that changed as predicate
    cp.publish(new ContextEvent(ls, LightSource.PROP_SOURCE_BRIGHTNESS));
 }
At this point the server-project is finished and you can build it. If the build succeed you can process with the client.

The Client Project

After we have valid project to manage light sources we now need to have a client that is able to get connected to the server (locally or remote). This client will provide a small Swing-GUI to select a light and switch its status. By subscribing to the context-bus we also support to have several clients which status is always up to date.

The class "LightingConsumer"

The LightingConsumer subscribes to context events:

 public class LightingConsumer extends ContextSubscriber {
    private static ServiceCaller caller;

Since we want to receive context events in this class we take org.universAAL.middleware.context.ContextSubscriber as a base. To make this example as easy as possible we will also make our needed server calls in this class and use a standard implementation of the org.universAAL.middleware.service.ServiceCaller. We create the default service caller in the constuctor of the client:

    caller = new DefaultServiceCaller(context);

Next we define again two needed URI's:

  private static final String LIGHTING_CONSUMER_NAMESPACE
     = "http://sample.universAAL.org/LightingConsumer.owl#";
  private static final String OUTPUT_LIST_OF_LAMPS
     = LIGHTING_CONSUMER_NAMESPACE + "controlledLamps";

First is the namespace of the client and second one the URI for the output of a request for the controlled lamps. Now we will add method to create the needed request to receive the available light sources in the system:

  public ServiceRequest getAllLampsRequest() {
     ServiceRequest getAllLamps = new ServiceRequest(new Lighting(), null);
     getAllLamps.addRequiredOutput(OUTPUT_LIST_OF_LAMPS,
        new String[] { Lighting.PROP_LIGHT_SOURCE});
     return getAllLamps;
  }

We pass to the constructor of the ServiceRequest the class org.universAAL.tools.samples.lighting.ont.owl.Lighting, to indicate the type of services we are searching for. The second parameter offers to provide a user that is involved in the process, but we can still leave it used. What we need is a call to addRequiredOutput. This to indicate that we expect to get an output at the path Lighting.PROP_LIGHT_SOURCE. This output is identified by the URI OUTPUT_LIST_OF_LAMPS. This URI does not have to be similar with the output URI in the server-project! This is only to identify the output later in the request like is done in the following method:

    public String[] getControlledLamps() {
       ServiceResponse sr = caller.call(getAllLampsRequest());
 
       if (sr.getCallStatus() == CallStatus.succeeded) {
          List<LightSource> lampList = sr.getOutput(OUTPUT_LIST_OF_LAMPS, true);
          String[] lamps = new String[lampList.size()];
          for (int i=0; i<lamps.length; i++)
             lamps[i] = ((LightSource)lampList.get(i)).getURI();
          return lamps;
       }
 
       return null;
    }

Here we use getAllLampsRequest to create a request that we pass to the call method of the ServiceCaller. As return value we get a org.universAAL.middleware.service.ServiceResponse object, like we already seen for the invalid_input attribute from the server. From where we got the result we do not know (without further steps) and all this is completely up to the system. If the call status of the request is not CallStatus.succeeded it has been failed and we have to return null. Otherwise we have a look at the requested output by passing the URI of the output to the method getOutput from ServiceResponse. Like we defined in the server this is a list with LightSources. We iterate the list, extract the URI's and return the result.

One more service request is needed to control the different lamps after we received them. This is done with the following methods:

  public static boolean turnOn(String lampURI) {
     ServiceResponse sr = caller.call(turnOnRequest(lampURI));
     return sr.getCallStatus() == CallStatus.succeeded
  }

  public static ServiceRequest turnOnRequest(String lampURI) {
     ServiceRequest turnOn = new ServiceRequest(new Lighting(), null);
     turnOn.addValueFilter(
        new String[] { Lighting.PROP_CONTROLS },
        new LightSource(lampURI));
     turnOn.addChangeEffect(
        new String[] { Lighting.PROP_CONTROLS, LightSource.PROP_SOURCE_BRIGHTNESS },
        new Integer(100));
     return turnOn;
  }

Lets first have a look at the method turnOn. Here we again pass a ServiceRequest object to the call method of ServiceCaller and return if the operation has been a success or not. A little bit more complicated is turnOnRequest. The creation of the ServiceRequest object is similar to the one in getAllLampsRequest. But then we have no output but need to define an input value. First we use addValueFilter to filter the affected elements of the request to be a part of Lighting.PROP_LIGHT_SOURCE and a light source with lampURI. Then we add the change effect wit addChangeEffect and pass the path to the property to change and the value to set the light source to. This means that, starting from the Lighting class of the ontology

  1. at property path controls needs to be the value lampURI
  2. the value at property path controls - brightness needs to be changed to 100.
Accordingly, the methods to turn off a light source is realized, but the change effect takes the value '0' (turning off is equivalent to setting the brightness to 0).

This was all about the call of services, but still open is the handling of the context subscription. Therefore we first need to create appropriate patterns in the method getContextSubscriptionParams:

   private static ContextEventPattern[] getContextSubscriptionParams() {
      ContextEventPattern cep = new ContextEventPattern();
      cep.addRestriction(MergedRestriction.getAllValuesRestriction(
         ContextEvent.PROP_RDF_SUBJECT, LightSource.MY_URI));
      return new ContextEventPattern[] {cep};
   }

We take use of the provided class org.universAAL.middleware.context.ContextEventPattern and create a new instance of it. Then we add a MergedRestriction.getAllValuesRestriction to restrict the passed events to all those where the subject (ContextEvent.PROP_RDF_SUBJECT) is (an instance of) a light source (LightSource.MY_URI). This will make the subscribe receive all events with a light source as subject. You could further restrict it to receive only changes of the brightness property by adding another restriction to the predicate (ContextEvent.PROP_RDF_PREDICATE).

We give the pattern back as array so that we can directly pass it to the parent constructor:

  public LightingConsumer(ModuleContext context) {
     super(context, getContextSubscriptionParams());
     ...
  }

All context-events that passes the filter are then handled in the method handleContextEvent:

  public void handleContextEvent(ContextEvent event) {
     System.out.println("Received a context-event with subject "
        + event.getSubjectURI() + " and predicate "
        + event.getRDFPredicate());
  }

Here we only make a small text out about the incoming events. In principle you can put here everything you want to do to handle the new context information.

Lighting GUI

To really control the lamps we need to have a small user interface. This GUI is in implemented by class LightClient. You can query the set of controlled light sources; they are shown in a list view. Select one of those light sources to control it. The buttons "On" and "Off" will perform the appropriate action. Additionally, you can set a dimming value and request a service to dim the light source. However, since the server only support to switch the light source on or off, dimming to a value other than 0 or 100 will result in an error: the bus will not find a matching service for that request.

Support:

Found a problem?
  • Report suggestions, missing, outdated or wrong documentation creating an Issue with "documentation" tag
Clone this wiki locally