HomePort
HomePort API Specification

Table of Contents

Internal architecture

HomePort data model

The internal class diagram for reference, most but not all types are available to the outside through references (direct access to members are generally not possible):

Class diagram (Probably outdated)

Enumerations for error handling

All functions in the API returns hpd_err_t, which is:

Todo:
Should really reconsider whether to have errors on free functions - really difficult to handle (there's a reason you're not suppose to throw in destructors!)
enum hpd_error {
HPD_E_SUCCESS = 0, //< The operation was successful.
HPD_E_UNKNOWN = 1, //< An unknown error
HPD_E_NULL, //< Null pointer error.
HPD_E_ALLOC, //< Memory allocation error.
HPD_E_STATE, //< An object is in an invalid state.
HPD_E_ARGUMENT, //< An argument is invalid.
HPD_E_NOT_UNIQUE, //< An argument is not unique.
HPD_E_NOT_FOUND, //< The object could not be found.
};

For the request-response relationships HomePort continues to use HTTP status codes to describe errors, as many modules relies on HTTP connections. From the HTTP protocol, these are:

#define HPD_HTTP_STATUS_CODE_MAP(XX) \
XX(100, Continue) \
XX(101, Switching Protocols) \
XX(200, OK) \
XX(201, Created) \
XX(202, Accepted) \
XX(203, Non-Authoritative Information) \
XX(204, No Content) \
XX(205, Reset Content) \
XX(206, Partial Content) \
XX(300, Multiple Choices) \
XX(301, Moved Permanently) \
XX(302, Found) \
XX(303, See Other) \
XX(304, Not Modified) \
XX(305, Use Proxy) \
XX(306, (Unused)) \
XX(307, Temporary Redirect) \
XX(400, Bad Request) \
XX(401, Unauthorized) \
XX(402, Payment Required) \
XX(403, Forbidden) \
XX(404, Not Found) \
XX(405, Method Not Allowed) \
XX(406, Not Acceptable) \
XX(407, Proxy Authentication Required) \
XX(408, Request Timeout) \
XX(409, Conflict) \
XX(410, Gone) \
XX(411, Length Required) \
XX(412, Precondition Failed) \
XX(413, Request Entity Too Large) \
XX(414, Request-URI Too Long) \
XX(415, Unsupported Media Type) \
XX(416, Requested Range Not Satisfiable) \
XX(417, Expectation Failed) \
XX(500, Internal Server Error) \
XX(501, Not Implemented) \
XX(502, Bad Gateway) \
XX(503, Service Unavailable) \
XX(504, Gateway Timeout) \
XX(505, HTTP Version Not Supported)
enum hpd_status {
#define XX(num, str) HPD_S_##num = num,
#undef XX
};

Attribute default keys

The entities in the data model are described using attributes. These are flexible key-value pairs, where the keys can be chosen freely. However, we do define the following default keys for consistencies between modules:

Todo:
As with default keys, we could add default values as well; Type = Temperature, Unit = C, etc. Seluxit and Zigbee can be used for inspiration (Rene’s work).
static const char * const HPD_ATTR_NETWORK = "network";
static const char * const HPD_ATTR_DESC = "description";
static const char * const HPD_ATTR_NAME = "name";
static const char * const HPD_ATTR_VENDOR = "vendor";
static const char * const HPD_ATTR_PRODUCT = "product";
static const char * const HPD_ATTR_VERSION = "version";
static const char * const HPD_ATTR_LOCATION = "location";
static const char * const HPD_ATTR_TYPE = "type";
static const char * const HPD_ATTR_UNIT = "unit";
static const char * const HPD_ATTR_MAX = "max";
static const char * const HPD_ATTR_MIN = "min";
static const char * const HPD_ATTR_STEP = "step";
static const char * const HPD_ATTR_SCALE = "scale";
static const char * const HPD_ATTR_VALUES = "values";
static const char * const HPD_ATTR_PROTOCOL = "protocol";

Method and Port enumeration

Services have a set of actions, currently for get and put only, but these are define in a enumeration, thus can easily be expanded later:

enum hpd_method {
HPD_M_NONE = -1, //< Method that does not exist, must be first below valid methods
HPD_M_GET = 0, //< HTTP GET like method
HPD_M_PUT, //< HTTP PUT like method
HPD_M_COUNT //< Last
};

Custom user data and free functions

HomePort's data model allow module developers to store custom pointers to user data of their own choice. HomePort will not touch this data. To allow memory management of custom data pointers, HomePort generally take a on_free callback function with the data pointer. This function will be called when HomePort is free'ing the object and can be used by the module developer to ensure that the custom data is free'd as well. The function pointer can be NULL, if it is not necessary to free the data.

typedef void (*hpd_free_f) (void *data); //< Free function, used to free user supplied data.

Main Program

hpd_error_t hpd_module(hpd_t *hpd, const char *id, const hpd_module_def_t *module_def);
hpd_error_t hpd_start(hpd_t *hpd, int argc, char *argv[]);

First, the main program allocates memory for HomePort (hpd_t) with hpd_alloc(). Then it tells HomePort of all modules that it wishes to include with hpd_module(). We shall use the term module for both adapters and applications. All modules are assigned a unique id by the main program, which allows running multiple instances of the same module. module_def is a public structure, of which each module should provide an instance (explained below).

Main program

The main program will then start HomePort with hpd_start(). Note that this function does not return, but will keep running until hpd_stop() is called. hpd_stop() will be called automatically on sigint and sigterm. Finally, the main program will deallocate the memory with hpd_free().

Any pointer to hpd_t is valid between calls to hpd_alloc() and hpd_free(), and thus can generally be stored by modules in their entire lifetime.

Shared API (Adapters and Applications) - Part I

Modules

A module will provide an instance of the module_def structure along with implementations of the five included functions. module_def is a structure where the members are publicly available.

hpd_parse_opt_f on_parse_opt; //< Should return HPD_E_ARGUMENT if name is not known
};

The five functions:

typedef hpd_error_t (*hpd_create_f) (void **data, const hpd_module_t *context);
typedef hpd_error_t (*hpd_destroy_f) (void *data);
typedef hpd_error_t (*hpd_start_f) (void *data, hpd_t *hpd);
typedef hpd_error_t (*hpd_stop_f) (void *data, hpd_t *hpd);
typedef hpd_error_t (*hpd_parse_opt_f) (void *data, const char *name, const char *arg);

In here, on_create and on_destroy are used to allocate/deallocate globally available memory in data, which can be used freely. on_start and on_stop are used to initialise/stop the underlying connection and add watchers to the event loop – whatever is required for the module to be considered running. To handle program argument given to the executable, a module can also define on_parse_opt, which may be called between on_create and on_start due to arguments given to the executable.

Module functions and object lifetime

The following three functions are shared between adapters and applications:

hpd_error_t hpd_module_add_option(const hpd_module_t *context, const char *name, const char *arg, int flags,
const char *doc);
hpd_error_t hpd_module_get_id(const hpd_module_t *context, const char **id);

For arguments to work, the module will have to add any options using hpd_module_add_option() during on_create. Options will be named –id-name, where id is the module identifier, as given by the main program. The module can call hpd_module_get_id() to obtain this id. During on_start and afterwards, the event loop will be accessible through hpd_get_loop(). On here, the module can put io-watchers, timer-watchers, and more to trigger on events.

A module can rely on the pointer to its hpd_module_t (as given in on_create()) will be valid for its entire lifetime, that is between calls to on_create and on_destroy. The pointer to its id (as obtained with hpd_module_get_id()) is valid in the same interval. The pointer to the event loop (as obtained with hpd_get_loop()) is valid between calls to on_start and on_stop.

Logging

hpd_error_t hpd_logf(const hpd_module_t *context, hpd_log_level_t level, const char *file, int line, const char *fmt, ...);
hpd_error_t hpd_vlogf(const hpd_module_t *context, hpd_log_level_t level, const char *file, int line, const char *fmt, va_list vp);
#define HPD_LOG_ERROR(CONTEXT, FMT, ...) hpd_logf((CONTEXT), HPD_L_ERROR, __FILE__, __LINE__, (FMT), ##__VA_ARGS__)
#define HPD_LOG_WARN(CONTEXT, FMT, ...) hpd_logf((CONTEXT), HPD_L_WARN , __FILE__, __LINE__, (FMT), ##__VA_ARGS__)
#define HPD_LOG_INFO(CONTEXT, FMT, ...) hpd_logf((CONTEXT), HPD_L_INFO , __FILE__, __LINE__, (FMT), ##__VA_ARGS__)
#define HPD_LOG_DEBUG(CONTEXT, FMT, ...) hpd_logf((CONTEXT), HPD_L_DEBUG, __FILE__, __LINE__, (FMT), ##__VA_ARGS__)
#define HPD_LOG_VERBOSE(CONTEXT, FMT, ...) hpd_logf((CONTEXT), HPD_L_VERBOSE, __FILE__, __LINE__, (FMT), ##__VA_ARGS__)
#define HPD_LOG_RETURN(CONTEXT, E, FMT, ...) do { HPD_LOG_DEBUG((CONTEXT), (FMT), ##__VA_ARGS__); return (E); } while(0)
#define HPD_LOG_RETURN_E_NULL(CONTEXT) HPD_LOG_RETURN((CONTEXT), HPD_E_NULL, "Unexpected null pointer.")
#define HPD_LOG_RETURN_E_ALLOC(CONTEXT) HPD_LOG_RETURN((CONTEXT), HPD_E_ALLOC, "Unable to allocate memory.")
// TODO New function, check if it can be used elsewhere
#define HPD_LOG_RETURN_E_SNPRINTF(CONTEXT) HPD_LOG_RETURN((CONTEXT), HPD_E_UNKNOWN, "snprintf failed.")

Indirect references

Through the following sections we shall utilise indirect references to objects in the data mode (adapters, devices, services, parameters). These prevent module developers from keeping pointers to objects that eventually will be free'd, thus making these pointers invalid. To allocate and free these references:

If a module developers explicitly call *_id_alloc(), they are also responsible for calling *_id_free().

Adapter API

The functions in the Adapter API are only available for adapters.

Adapter structure (hpd_adapter_t)

To maintain adapter structures in the data model:

Todo:
Maybe change name of the structure from “adapter” to “network” to highlight the differences between modules and adapters. A module can make multiple networks of devices.

alloc/free are used to manage memory. attach/detach are used to hand the object to HomePort. If an object is attached, it will be visibly through the navigation functions (shown later) and HomePort will ensure that the memory is managed. For example this means that if an adapter is freed, all devices, services, etc. below it will automatically be freed also. Note that the reverse applies too; if an object is detached, HomePort do not know about it and it is up to the module to manage the memory. set_data/get_data functions can be used, by the adapter, to store custom data within HomePort. If on_free is set, HomePort will call this function to free this custom data, when the object is freed.

As to when to use a direct pointer to the adapter and when to use an indirect reference, see the following diagram. hpd_adapter_alloc() allocates the instance of hpd_adapter_t, which is referred to using a pointer - the module developer is in control of the memory. The object is changed (set functions), and finally hpd_adapter_attach() is called. After this call the pointer will be invalid and we can only refer to the object using instances of hpd_adapter_id_t (indirect references) - HomePort manages the memory.

Lifetime of adapter structures (Outside view, not exact implementation)

After a call to hpd_adapter_detach(), the indirect reference will still be allocated, but will now result in HPD_E_NOT_FOUND, if we try to use it. hpd_adapter_detach() also returns a pointer to hpd_adapter_t – it is now the responsibility of the module to free the object.

Implementation details:
hpd_adapter_attach() will make a copy of hpd_adapter_t, thus effectively invalidating the pointer. hpd_adapter_detach() will return a pointer to this copy, which was stored in the internal data model.

Device structure (hpd_device_t)

Devices are managed in a similar manner:

Service structure (hpd_service_t)

Services furthermore allow setting an action, which is a method and a function pointer. Currently only get and put is support, but it could theoretically be expanded to others:

typedef hpd_status_t (*hpd_action_f) (void *data, hpd_request_t *req); //< Action function for handling requests on services.

Parameter structure (hpd_parameter_t)

Finally, functions to manage parameters:

Shared API (Adapters and Applications) - Part II

Data model getters

To get members from the objects, created by an adapter, applications have the following functions:

hpd_error_t hpd_pair_get(const hpd_pair_t *pair, const char **key, const char **value);

Again, we have defines to ease foreach loops:

#define hpd_service_foreach_action(RC, ACTION, ID) for ( \
(RC) = hpd_service_first_action((ID), &(ACTION)); \
!(RC) && (ACTION); \
(RC) = hpd_service_next_action(&(ACTION)))
#define hpd_adapter_foreach_attr(RC, PAIR, ID) for ( \
(RC) = hpd_adapter_first_attr((ID), &(PAIR)); \
!(RC) && (PAIR); \
(RC) = hpd_adapter_next_attr(&(PAIR)))
#define hpd_device_foreach_attr(RC, PAIR, ID) for ( \
(RC) = hpd_device_first_attr((ID), &(PAIR)); \
!(RC) && (PAIR); \
(RC) = hpd_device_next_attr(&(PAIR)))
#define hpd_service_foreach_attr(RC, PAIR, ID) for ( \
(RC) = hpd_service_first_attr((ID), &(PAIR)); \
!(RC) && (PAIR); \
(RC) = hpd_service_next_attr(&(PAIR)))
#define hpd_parameter_foreach_attr(RC, PAIR, ID) for ( \
(RC) = hpd_parameter_first_attr((ID), &(PAIR)); \
!(RC) && (PAIR); \
(RC) = hpd_parameter_next_attr(&(PAIR)))

Data model navigation

The HomePort data model can be navigated by both adapters and applications, thus only the later will find use for browsing through models created by another module. To do so, the following functions are available:

In brief: Get functions will navigate upwards in the hierarchy, first functions will get the first object in the list under the given object, next will give the next one.

Because first-next function are a bit complicated to use, the following defines are also available to create simple foreach loops (based on the same principle from libev):

#define hpd_foreach_adapter(RC, ADAPTER, HPD) for ( \
(RC) = hpd_first_adapter((HPD), &(ADAPTER)); \
!(RC) && (ADAPTER); \
(RC) = hpd_next_adapter(&(ADAPTER)))
#define hpd_foreach_device(RC, DEVICE, HPD) for ( \
(RC) = hpd_first_device((HPD), &(DEVICE)); \
!(RC) && (DEVICE); \
(RC) = hpd_next_device(&(DEVICE)))
#define hpd_foreach_service(RC, SERVICE, HPD) for ( \
(RC) = hpd_first_service((HPD), &(SERVICE)); \
!(RC) && (SERVICE); \
(RC) = hpd_next_service(&(SERVICE)))
#define hpd_adapter_foreach_device(RC, DEVICE, ADAPTER) for ( \
(RC) = hpd_adapter_first_device((ADAPTER), &(DEVICE)); \
!(RC) && (DEVICE); \
(RC) = hpd_adapter_next_device(&(DEVICE)))
#define hpd_adapter_foreach_service(RC, SERVICE, ADAPTER) for ( \
(RC) = hpd_adapter_first_service((ADAPTER), &(SERVICE)); \
!(RC) && (SERVICE); \
(RC) = hpd_adapter_next_service(&(SERVICE)))
#define hpd_device_foreach_service(RC, SERVICE, DEVICE) for ( \
(RC) = hpd_device_first_service((DEVICE), &(SERVICE)); \
!(RC) && (SERVICE); \
(RC) = hpd_device_next_service(&(SERVICE)))
#define hpd_service_foreach_parameter(RC, PARAMETER, SERVICE) for ( \
(RC) = hpd_service_first_parameter((SERVICE), &(PARAMETER)); \
!(RC) && (PARAMETER); \
(RC) = hpd_service_next_parameter(&(PARAMETER)))

Communication

Shared API - Value structure

The value structure is used in both get and put requests and thus is available for both adapters and applications:

hpd_error_t hpd_value_alloc(hpd_value_t **value, const char *body, int len);
// TODO New function, check if it can be used elsewhere
hpd_error_t hpd_value_allocf(hpd_value_t **value, const char *fmt, ...);
hpd_error_t hpd_value_vallocf(hpd_value_t **value, const char *fmt, va_list vp);
hpd_error_t hpd_value_set_header(hpd_value_t *value, const char *key, const char *val);
hpd_error_t hpd_value_get_body(const hpd_value_t *value, const char **body, size_t *len);
hpd_error_t hpd_value_get_header(const hpd_value_t *value, const char *key, const char **val);
#define hpd_value_foreach_header(RC, PAIR, VALUE) for ( \
(RC) = hpd_value_first_header((VALUE), &(PAIR)); \
!(RC) && (PAIR); \
(RC) = hpd_value_next_header(&(PAIR)))

The body of a value is the actual value, where the headers are additional attributes, which are currently unused but will be used to indicate time-to-live for caching, but are defined generic so they can easily be expanded without breaking the API. Note it is also possible for adapters and applications to store their own custom headers, similar to the principle in HTTP headers.

The len in hpd_value_alloc can be set to HPD_NULL_TERMINATED if body is a \0 terminated string:

#define HPD_NULL_TERMINATED -1

Adapter API - Communication

Action functions will need functions related to requests/responses to manage these (functions to manage values are shared with applications and will be shown later):

And when, we are considering communication, an adapter should call the following function, whenever a service has changed value:

Lifetime of request/response structures
Lifetime of request/response structures

Application API - Communication

The functions in the Application API are only available for applications.

For hpd_request_t, hpd_response_t and hpd_listener_t functions we need the following callbacks:

typedef void (*hpd_response_f) (void *data, const hpd_response_t *res);
typedef void (*hpd_value_f) (void *data, const hpd_service_id_t *service, const hpd_value_t *val);
typedef void (*hpd_device_f) (void *data, const hpd_device_id_t *device);

The functions for requests and response are the reverse of an adapter (an application can create requests and read the values of a response):

In addition, an application can also create listeners. Listeners can be created on any object (HomePort, adapter, device, and service) and will be called for that object and everything below if the conditions are met. E.g., a listener with a value callback on an adapter will be called if any service under that adapter changes value. Function to manage listeners:

The foreach function will cause the given listener to be called for each device that is already attached.

Lifetime of request/response structures
Todo:
Finish listener sequence chart
Lifetime of listener structures