Whiteboard

In simplified terms Whiteboard is an asyncronous communication library for handling internal and external communication in Movesense (excluding the Gatt service communication with mobile client). Full description of the Movesense API with some examples can be found here. See also Getting started for an example of creating a simple new data resource provider. More elaborate functional examples can be found under Sample Applications.

Following are the key components of using the Whiteboard REST-like services in sensor software via the Whiteboard C++ programming interface.

LaunchableModule

A launchable module is a component that allows its running state to be controlled by the system through the StartupProvider. Typically a launchable module initializes providers and clients of some feature. The interface can be used without any service provider or client if only initialization is needed and there is no need to use the messaging system for operation.

The StarupProvider interface for starting and stopping services is also accessible from outside the sensor over any active communication route. This may be usefull in e.g. some system testing scenarios.

Interface

A new launchable module needs to inherit the whiteboard::LaunchableModule interface.

#include <whiteboard/LaunchableModule.h>

class TestService FINAL : private whiteboard::LaunchableModule

These pure virtual method implementations are required:

virtual bool initModule() = 0;
virtual void deinitModule() = 0;
virtual bool startModule() = 0;
virtual void stopModule() = 0;

Construction

#include <app_execution_contexts/resources.h>

const char* const TestService::LAUNCHABLE_NAME = "TestSvc";

TestService::TestService()
    : LaunchableModule(LAUNCHABLE_NAME, WB_EXEC_CTX_APPLICATION)
{}

Execution contexts are defined under the "executionContexts" section in the application's yaml files (app_root.yaml in most of the provided examples). Each context is a separate thread and Whiteboard provides a safe event based framework for switching between contexts.

By default two execution contexts are available in the examples:

It must be noted that the pre-built system library expects the first execution context to be "application" and the second "meas". The order or naming of these cannot be changed or otherwise the application project will not copile. The properties of these execution contexts may however be adjusted according to needs. Also more execution contexts can be added by defining them in YAML but they will consume RAM for the thread's stack memory and the messaging queues.

Adding new launchable modules

In App.cpp file a new launchable module can be added as follows:

#include "TestService.h"
MOVESENSE_PROVIDERS_BEGIN(1)
MOVESENSE_PROVIDER_DEF(TestService)
MOVESENSE_PROVIDERS_END(1)

RTT logs can be used to verify the module is loaded correctly.

Operation

Life cycle of the module

Launchable modules use two stage startup (initialization and start) and shutdown (stop and deinitialization) procedures. Upon bootup a system wide initialization is done after which the secondary start methods are called to put the system in a ready state. In a service provider's case the provider should be ready for e.g. client subscriptions to the service interface specifiend in YAML. These C++ interface methods should be properly implemented for correct operation of the module.

Virtual bool initModule() method

The startup service provider will call this method to trigger the initialization of the module (hardware configuration etc.). No API resources can be used during the execution of this method as the system is still initialising.

Return true if initialization was successful, false otherwise.

Example implementation:

bool TestService::initModule()
{
    bool result = initalizeTestHarware();
    mModuleState = WB_RES::ModuleStateValues::INITIALIZED;
    return result;
}

Virtual bool startModule() method

Startup service provider will call this method to indicate the operation of the module can be started.

Return true if startup was successful, false otherwise.

Example implementation:

bool TestService::startModule()
{
    // read configuration that is accessible after initialization
    readDefaultConfig();
    acquireTimingResources();
    mModuleState = WB_RES::ModuleStateValues::STARTED;
    return true;
}

Virtual void stopModule() method

Startup service provider will call this method to indicate the operation of the module is not allowed.

Example implementation:

void TestService::stopModule()
{
    if (mIsActive)
    {
        fullStop();
    }
    releaseTimingResources();
    mModuleState = WB_RES::ModuleStateValues::STOPPED;
}

Virtual void deinitModule() method

Startup service provider will call this method to trigger the de-initialization of the module. No API resources can be used during the execution of this method as the system may be e.g. deinitialising to power down.

Example implementation:

void TestService::deinitModule()
{
    deinitalizeTestHarware();
    mModuleState = WB_RES::ModuleStateValues::UNINITIALIZED;
}

Functions

These are mainly for the system to query the state of the modules, but may be used by the modules if needed.

const char* getLaunchableName()

Get the name of the module.

Returns Pointer to the module name.

WB_RES::ModuleState getModuleState() const

Get current state of the module

Returns State of the module.

ExecutionContextId getExecutionContextId() const

Get the execution context of the launchable module.

Returns The execution contxet ID.

ResourceProvider

Base class for creating new service providers. Via this interface class new resources can be registered for client usage. For interfaces designed to be used over BLE power consumption consideration should be made for performing some data calculation on the sensor. This is to avoid sending all of the raw data and using lot of BLE bandwidth.

Interface

The interface is a two way interface, the system features available for resource providers can be accessed via the member functions. On the other hand, callback methods for required specific service operation types (GET, PUT, POST, DELETE) need to be overridden.

Extend the class with ResourceProvider.

#include <whiteboard/LaunchableModule.h>
#include <whiteboard/ResourceProvider.h>

class TestService FINAL : private whiteboard::ResourceProvider,
                          public whiteboard::LaunchableModule

Construction

Service providers operate in their designated execution contexts (threads) and the context defintion given to the constructor must be the same as what is specifiend in YAML for the resources that are registered for the provider. Trying to operate on a resource that is specified for some other execution context will result in an error.

const char* const TestService::LAUNCHABLE_NAME = "TestSvc";

TestService::TestService()
    : ResourceProvider(WBDEBUG_NAME(__FUNCTION__), WB_RES::LOCAL::SAMPLE_TEST1::EXECUTION_CONTEXT),
      LaunchableModule(LAUNCHABLE_NAME, WB_RES::LOCAL::SAMPLE_TEST1::EXECUTION_CONTEXT)
{}

Registering and unregistering resources

Even though it's possible to register resources one by one, it is a good practise to create an array of local resource IDs and register the whole array at once.

static const whiteboard::LocalResourceId sProviderResources[] =
{
    WB_RES::LOCAL::SAMPLE_TEST1::LID,
    WB_RES::LOCAL::SAMPLE_TEST2::LID,
};

Registration of the resources should be done in the initModule so that clients can subscribe to them in the startModule() method.

bool TestService::initModule()
{
    if (registerProviderResources(sProviderResources) != whiteboard::HTTP_CODE_OK)
    {
        mModuleState = WB_RES::ModuleStateValues::INITFAILED;
        return false;
    }

    mModuleState = WB_RES::ModuleStateValues::INITIALIZED;
    return true;
}

Also it is a good practise to unregister resources during deinitialization.

void TestService::deinitModule()
{
    unregisterProviderResources(sProviderResources);
    mModuleState = WB_RES::ModuleStateValues::UNINITIALIZED;
}

Handling incoming requests

There are several types of requests. The operations that are specified in the YAML interface must be implemented by the service provider. All required callbacks of the following need to be overridden:

virtual void onGetRequest(const Request& rRequest, const ParameterList& rParameters)
virtual void onPutRequest(const Request& rRequest, const ParameterList& rParameters)
virtual void onPostRequest(const Request& rRequest, const ParameterList& rParameters)
virtual void onDeleteRequest(const Request& rRequest, const ParameterList& rParameters)

Default implementation: Returns whiteboard::HTTP_CODE_NOT_IMPLEMENTED

Example implementation:

void TestService::onGetRequest(const whiteboard::Request& request,
                                     const whiteboard::ParameterList& parameters)
{
    // in order to save ROM, this could also be:
    // ASSERT(mModuleState == WB_RES::ModuleStateValues::STARTED);
    if (mModuleState != WB_RES::ModuleStateValues::STARTED)
    {
        return returnResult(request, wb::HTTP_CODE_SERVICE_UNAVAILABLE);
    }

    switch (request.getResourceId().localResourceId)
    {
    case WB_RES::LOCAL::SAMPLE_TEST1::LID:
    {
        WB_RES::Test test;
        test.value = 1;
        return returnResult(request, whiteboard::HTTP_CODE_OK, ResponseOptions::Empty, test);
    }
    break;

    case WB_RES::LOCAL::SAMPLE_TEST2::LID:
    {
        WB_RES::Test test;
        test.value = 2;
        return returnResult(request, whiteboard::HTTP_CODE_OK, ResponseOptions::Empty, test);
    }
    break;

    default:
        return returnResult(request, whiteboard::HTTP_CODE_NOT_FOUND);
    }
}

Subcriptions and notifications are a special case of operation. The standard REST operation is that one request is followed by an response to it. Subscriptions allow multiple responses (notifications) without new requests. Notifications may be periodic or event based.

Override these methods only if subscribers and their subscription parameters need to be tracked. Check "Updating resource" to see how to send data.

virtual void onSubscribe(const Request& rRequest, const ParameterList& rParameters)
virtual void onUnubscribe(const Request& rRequest, const ParameterList& rParameters)

Default implementation: Returns whiteboard::HTTP_CODE_OK

Any incoming request must be closed with a call returnResult(...) within the scope of the callback if the request object is not stored for later processing.

template <typename V = NoType>
void returnResult(const Request& rRequest, Result resultCode, const ResponseOptions& rOptions = ResponseOptions::Empty, const V& result = NoType::NoValue)

template <typename RESPONSE, typename V = NoType, typename EnableIf<!IsSame<RESPONSE, Result>::value, int>::type = 0>
void returnResult(const Request& rRequest, const RESPONSE& rResponse, const ResponseOptions& rOptions = ResponseOptions::Empty, const V& result = NoType::NoValue)

Returns result for a request

Updating resource - notification to subscribers

template <typename V, typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result updateResource(LocalResourceId localResourceId, const ResponseOptions& rOptions, const V& rValue, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Sends resource changed notification for all subscribed clients. Data format must be as specified in YAML.

May not be called from onSubscribe() nor onUnsubscribe().

Example implementation

char buf[32];
counter++;
snprintf(buf, 32, "Hello World #%u!", counter);

WB_RES::HelloWorldValue hello;
hello.greeting = buf;

// and update the resources/resources
updateResource(WB_RES::LOCAL::SAMPLE_HELLOWORLD(), ResponseOptions::Empty, hello);

Other callback options

Functions handling Diconnections

virtual void onClientUnavailable(ClientId clientId);

Local client has become unavailable. It is the client's responsibility to e.g. renew subscriptions.


virtual void onRemoteWhiteboardDisconnected(WhiteboardId whiteboardId);

Connection to remote whiteboard lost. Remote clients will resubscribe if needed.

Functions

const char* getName() const

Returns the name of a resource provider.


ProviderId getId() const

Returns ID of the registered provider

LocalProviderId getLocalProviderId() const

Returns local ID of the registered provider

ExecutionContextId getExecutionContextId() const

Returns Reference to the provider's execution context

Result getLocalResource(const char* pFullPath, ResourceId& rResourceId)

Resolves local resource from path string.

Result isResourceSubscribed(ResourceId resourceId)

Query if a given resource has any active subscribers. It is good practise to stop operation to save power if no active subscribers.

Result enumerateSubscriptions(ResourceId resourceId, IEnumerateSubscriptionsFunctor& rFunctor)

Helper function for reiterating through all remaining subscribers.

ResourceClient

Services provided by the provider modules can be accessed via the ResourceClient interface class. Examples can be found in the samples direcory. One of the simplest examples is the blinky_app sample.

Interface

Client class should inherit the ResourceClient class, and also the LaunchableModule class if any startup time initialisation needs to get done.

#include <whiteboard/ResourceClient.h>

class TestClient FINAL : private whiteboard::ResourceClient

Constructor

ResourceClient(const char* pClientName, ExecutionContextId executionContextId);
const char* const TestService::LAUNCHABLE_NAME = "TestSvc";

TestService::TestService()
    : ResourceClient(WBDEBUG_NAME(__FUNCTION__), WB_EXEC_CTX_APPLICATION)
{}

Resource IDs

All resources have unique IDs which can be found in the source files generated from YAML files. IDs can be queried by the resource paths, but for local resources it is recommed to directly use the generated explicit constant ID values. When using path paramers ({param name} in the path string) with device local resources the getResource(...) or asyncGetResource(...) path string based resolver method must be used in conjunction with releaseResource(...). This is because the path paramerters need to be stored by the system for the duration of the usage of the resource. A call to releaseResource(...) then releases also the stored path parameters.

Local resources

Result getResource(const char* pFullPath, ResourceId& rResourceId, const RequestOptions& rOptions = RequestOptions::Empty)

Resolves resource from path string. Only for local paths. Use asyncGetResource to resolve external paths.

Returns Result of the operation

Result releaseResource(ResourceId resourceId, const RequestOptions& rOptions = RequestOptions::Empty);

Performs resource release. Release is intented to indicate to the provider's whiteboard, that this client does not use the resource anymore. Only for local paths, use asyncReleaseResource for external resourceIds.

Returns Result of the operation

External resources

Result asyncGetResource(const char* pFullPath, const AsyncRequestOptions& rOptions = AsyncRequestOptions::Empty);

Performs asynchronous resource resolving

Returns result of the operation

virtual void onGetResourceResult(RequestId requestId, ResourceId resourceId, Result resultCode);

Callback for asynchronous resource requests

Result asyncReleaseResource(ResourceId resourceId, const AsyncRequestOptions& rOptions = AsyncRequestOptions::Empty);

Performs asynchronous resource release. Release is intented to indicate to the provider's whiteboard, that this client does not use the resource anymore.

Returns Result of the operation

virtual void onReleaseResourceResult(RequestId requestId, ResourceId resourceId, Result resultCode);

Callback for asynchronous resource requests

Request types

GET

template <typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result asyncGet(ResourceId resourceId, const AsyncRequestOptions& rOptions, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Performs asynchronous GET request. P1 .. P8 are optional.

Returns Result of the operation.

virtual void onGetResult(RequestId requestId, ResourceId resourceId, Result resultCode, const Value& rResultData)

Callback for asynchronous resource GET requests

PUT

template <typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result asyncPut(ResourceId resourceId, const AsyncRequestOptions& rOptions, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Performs asynchronous PUT request. P1 .. P8 are Optional.

Returns Result of the operation

virtual void onPutResult(RequestId requestId, ResourceId resourceId, Result resultCode, const Value& rResultData);

Callback for asynchronous resource PUT requests

POST

template <typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result asyncPost(ResourceId resourceId, const AsyncRequestOptions& rOptions, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Performs asynchronous POST request. P1 .. P8 are optional.

Returns Result of the operation

virtual void onPostResult(RequestId requestId, ResourceId resourceId, Result resultCode, const Value& rResultData);

Callback for POST operation result

DELETE

template <typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result asyncDelete(ResourceId resourceId, const AsyncRequestOptions& rOptions, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Performs asynchronous DELETE request. P1 .. P8 are optional

Returns Result of the operation

virtual void onDeleteResult(RequestId requestId, ResourceId resourceId, Result resultCode, const Value& rResultData);

Callback for DELETE operation result

Subscriptions fo continuous data transfer

Resource subscriptions allow for continuously sending data from the service provider to any number of clients. Fundamentally the provider controls the frequency of notifications. Subscription parameters may be used for clients requesting specific frequencies for the updates.

Subscribe

template <typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result asyncSubscribe(ResourceId resourceId, const AsyncRequestOptions& rOptions, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Performs asynchronous SUBSCRIBE request. P1 .. P8 are optional.

Returns Result of the operation

virtual void onSubscribeResult(RequestId requestId, ResourceId resourceId, Result resultCode, const Value& rResultData);

Callback for asynchronous SUBSCRIBE requests

void asyncSubscribeLocalResources(size_t numberOfResources, const LocalResourceId* const pResourceIds, bool isCriticalSubscription = true, bool noResponseExpected = false)

template <size_t NUMBER_OF_RESOURCES>
inline void asyncSubscribeLocalResources(const LocalResourceId(&rResourceIds)[NUMBER_OF_RESOURCES], bool isCriticalSubscription = true, bool noResponseExpected = false)

inline void asyncSubscribeLocalResource(const LocalResourceId localResourceId, bool isCriticalSubscription = true, bool noResponseExpected = false)

Subscribes array of local Whiteboard resources

This function asserts if asynchronous dispatch fails or if any synchronous (same event thread) subscriptions fail.

Unsubscribe

template <typename P1, typename P2, typename P3, typename P4, typename P5, typename P6, typename P7, typename P8>
inline Result asyncUnsubscribe(ResourceId resourceId, const AsyncRequestOptions& rOptions, const P1& rP1, const P2& rP2, const P3& rP3, const P4& rP4, const P5& rP5, const P6& rP6, const P7& rP7, const P8& rP8)

Performs asynchronous UNSUBSCRIBE request. P1 .. P8 are optional

Returns Result of the operation

virtual void onUnsubscribeResult(RequestId requestId, ResourceId resourceId, Result resultCode, const Value& rResultData);

Callback for asynchronous UNSUBSCRIBE requests

void asyncUnsubscribeLocalResources(size_t numberOfResources, const LocalResourceId* const pResourceIds, bool noResponseExpected = false);

template <size_t NUMBER_OF_RESOURCES>
inline void asyncUnsubscribeLocalResources(const LocalResourceId(&rResourceIds)[NUMBER_OF_RESOURCES], bool noResponseExpected = false)

inline void asyncUnsubscribeLocalResource(const LocalResourceId localResourceId, bool noResponseExpected = false)

Unubscribes array of local Whiteboard resources

Notify

virtual void onNotify(ResourceId resourceId, const Value& rValue, const ParameterList& rParameters);

Callback for resource notifications.

Other callback options

Detect disconnection

Same disconnection notification is available for both clients and providers.

virtual void onRemoteWhiteboardDisconnected(WhiteboardId whiteboardId);

Remote whiteboard disconnect notification handler.

This can be used for example to cleanup possible subscription related state in the client-end. Whiteboard automatically cleans subscriptions and path variable allocations related to the disconnected whiteboard, so the client will not get further notifications and does not need to call unsubscribe or releaseResource either.

virtual void onResourceUnavailable(ResourceId resourceId);

Local resource unavailability handler.

This can be used for example to cleanup possible subscription related state in the client-end. Whiteboard automatically cleans subscriptions and path variable allocations related to the removed resource, so the client will not get further notifications and does not need to call unsubscribe or releaseResource either.

Functions

const char* getName() const;

Gets name of the client

ClientId getId() const;

Gets ID of the client

Returns ID of the registered client or ID_INVALID_CLIENT if client has not been registered

LocalClientId getLocalClientId() const;

Gets local client ID of the client

Returns Local ID of the registered client or ID_INVALID_LOCAL_CLIENT if client has not been registered

ExecutionContextId getExecutionContextId() const;

Gets ID of client's execution context

Returns ID of client's execution context

Timers

A common use case for timers is periodic measurements from which the results are sent to subscribers as notifications. Timers are available via the the ResourceClient and ResourceProvider interfaces. They can be used to do delayed or recurring work.

TimerId startTimer(size_t periodMs, bool isContinuous = false);

Starts a continuous timer with given period. Override whiteboard::ResourceClient::onTimer to handle timer notifications.
If previous timer message has not been processed, for example due to execution context blocking on long operation - and short interval continuous timer is in use, the timer messages might be filtered out to prevent flooding the message queues.

Starting a timer from interrupt is not supported.

Returns ID of the started timer or ID_INVALID_TIMER, if starting a new timer fails.

bool rescheduleTimer(TimerId timerId, size_t periodMs);

Reschedules the continuous timer with a new period; The existing timer is stopped and restarted (but not deallocated).

A timer message with the previous timer interval might have been queued before processing the rescheduling and hence the first time might fire in period less than the new period.

bool stopTimer(TimerId timerId);

Stops a timer started with startTimer. No notifications of the given timer are received after this call returns.
In case of continuous timer, a notification might be received after the stop if the stop is called from another thread than the clients execution context. Hence using timer start / stop from another thread is discouraged.
Stopping a timer from interrupt is not supported.

Returns true on success, false if timer was already stopped or this provider did not own the timer, or if id was illegal.

virtual void onTimer(TimerId timerId);

Callback for timer notifications. It is call in the provider or client context. It is safe to use ex. updateResource(...)

This sample shows how to create a continuous work in a few steps.

Creating a timer

The new member whiteboard::TimerId is needed and will be used for timer.

class TestClient FINAL : private whiteboard::ResourceClient,
                         public whiteboard::LaunchableModule
{
    ...

private:
    whiteboard::TimerId mTimer;
};

Good practise it to set variable to whiteboard::ID_INVALID_TIMER

TestClient::TestClient()
    : ResourceClient(WBDEBUG_NAME(__FUNCTION__), WB_EXEC_CTX_APPLICATION),
      LaunchableModule(LAUNCHABLE_NAME, WB_EXEC_CTX_APPLICATION)
{
    mTimer = whiteboard::ID_INVALID_TIMER;
}

Usage

Starting the timer

If timer should be started right after module is started, then the best location will be startModule()

bool TestClient::startModule()
{
    mModuleState = WB_RES::ModuleStateValues::STARTED;
    mTimer = startTimer(1000, true);

    return true;
}

Stopping the timer

Timer should be stopped when stopModule will be called or before that in any part of the application.

void TestClient::stopModule()
{
    stopTimer(mTimer);
    mTimer == whiteboard::ID_INVALID_TIMER;
}

The timer's callback

Timer will be calling onTimer. If there are more than one timer it can be recognized by timerId.

void TestClient::onTimer(whiteboard::TimerId timerId)
{
    if (timerId == mTimer)
    {
        // DO SOME WORK HERE
    }
}