Skip to content

μcuREST Tutorial

Getting Started

With μcuREST you can implement REST services and/or a web application for communication with your embedded application. First you’ll need to decide which resources (variables or functions of your application) you wish to expose via HTTP and assign URI to those resources. Then define a μcuRESTresource map, binding URI names to the resources. Use this map to instantiate a micurest::application and start a network stack for that application.

Defining a Resource Map

The easiest way to start is to take an example and adjust/replace the resource map according to your application needs. The resource map is a variadic template function parametrized with the resource entries. The following code sample lists five simplest resource entries of different types.

/* declaration of application resources */
extern cstring hello_html ();
extern char hello_text[32];
extern bool getLed();
extern void setLed(bool);
extern unsigned data;
extern unsigned char blob[1024];
extern size_t blob_size;

/* definition of resource names */
namespace name {
  NAME(hello_html)
  NAME(led)
  NAME(hello)
  NAME(data)
  NAME(blob)
}

/* map of URIs to application resources */
const directory& resourceMap(){
  return Root<
    resource::FileConstString<name::hello_html, hello_html, media::html>,
    resource::FileText<name::hello, sizeof(hello_text), hello_text>,
    resource::FileFunctions<name::led, bool, getLed, setLed>,
    resource::FileVariable<name::data, unsigned, &data>,
    resource::FileBinary<name::blob, &blob_size, sizeof(blob), blob>
  >();
}

Once you get familiar with resource::File* templates, you may switch to their short codes F:

/* map of URIs to application resources (same as above) */
const directory& resourceMap(){
  return Root<
    F<name::hello_html, hello_html, media::html>,
    F<name::hello, sizeof(hello_text), hello_text>,
    F<name::led, bool, getLed, setLed>,
    F<name::data, unsigned, &data>,
    F<name::blob, &blob_size, sizeof(blob), blob>
  >();

Instantiating and Running an Application

Interaction with the network stack depends on the underlying stack implementation. In a poll-style stack, such as Wiznet’s for Arduino, main application instantiates application, initializes the server and runs the server in the loop:

micurest::application MyApp(resourceMap);
tcp::server server(MyApp);
void setup() {
  server.listen(80);
}
void loop() {
  server.run();
// ...
}

In an event driven network stack, such as lwIP, the server is driven by events coming from the network stack, so it is sufficient to instantiate the application and initializes the server:

micurest::application MyApp(resourceMap);
tcp::server server(MyApp);
void setup() {
  server.listen(80);
}

Resource Map Elements

Root

A Resource Map root is a directory reference returned by the Root variadic template function, parametrized with top-level entries. There is no limitation on number of entries or number of resource maps (except, of course, those exposed by compiler or target platform capabilities).

const directory& resourceMap(){
  return Root<…>();
}

Entries

In the examples on top of this tutorial

resource::File… templates combine resource name and its content. In a more generic case, resource name and its content are introduced with different templates – resource::Entry… and resource::Node… . The first one is responsible for matching resource name and the latter – for providing the content.

resource::EntryGenericResource<name::anything, resource::NodeJSON<MyClass::json>>

or in short codes:

E<name::anything, N<MyClass::json>>

μcuREST provides several templates for defining entries:

  • resource::EntryGenericResource – a generic named entry
  • resource::EntryIndexedResource – indexed homogeneous entries (see below)
  • resource::EntryDynamicResource – indexed heterogeneous entries

Resource Names

Direct use of string literals as templates parameters is not supported by C++11. Therefore μcuREST defines names as functions returning cstring1. This workaround does not allow inline name definitions, e.g. a name should be declared before its use. As a good practice, names are placed in namespace name.

Indexed Entries

Described so far use cases operate with on static resource names. But what if your application needs to expose a set of similar resources, such as array of values or number of GPIO pins? To address this use case μcuREST offers indexed entries with vectorized nodes. When matching the URI part to an indexed entry, μcuREST calls an identity parser, associated with the entry. The parser processes the URI part and stores the identity in the message. Then μcuREST accesses the array or calls the functions using the identity as the index. In latter case you need to provide three functions: a getter, a setter and a detector. The following examples illustrate use of an indexed entry with functions and with an array.

extern bool digital_has(size_t index);
extern byte digital_read(size_t index);
extern void digital_write(size_t index, byte val);

const directory& resourceMap(){
  return Root<
    resource::EntryIndexedResource<identity::numeric,
		resource::NodeIndexedVector<
			accessor::vector<byte,
				digital_read, digital_write, digital_has>>>
	>();
}

extern unsigned array[12];

const directory& resourceMap(){
  return Root<
    resource::EntryIndexedResource<identity::numeric,
		resource::NodeIndexedVector<
			accessor::array<unsigned, details::countof(array), array>>>
	>();
}

In the example above all entries are of the same nature – elements of unsigned array. In a more advanced cases an entry may index elements of various nature. This is achieved with resource::EntryDynamicResource template that accepts an identity parser and a function returning entry reference for the identity passed as a parameter:

const details::node&  mynodes(const identity::type& id) {
	return (id&1) ? resource::NodeJSON<Male::json>() : resource::NodeJSON<Female::json>();  
}

const directory& resourceMap() noexcept {
  return Root<resource::EntryDynamicResource<identity::numeric, mynodes>>();
}

Identity Parser

Currently μcuREST implements only one identity parser – numeric. If your application needs another kind of index, you may use your own parser instead. It is a function with rather simple signature:

bool parser(const char_t* str, identity::type& identity); // on success fill identity and return true.

Nodes

μcuREST provides several templates for defining nodes:

  • resource::NodeTextFuction<M,V> – text content of media type M returned by function V
  • resource::NodeProvider<M,P> – text content of media type M written by function P
  • resource::NodeAction<F,L> – function F called on PUT with a payload string of max length L
  • resource::NodeJSON<V> – a JSON value, provided via COJSON structure V
  • resource::NodeJSONRpc<V,M> – a JSON value with alternate media type M
  • resource::NodeIndexedVector – an indexable node bound to a vector via accessor X (accessor::vector or accessor::bunch

Files

μcuREST provides several templates for defining “files” (combination of entry and node):

  • resource::FileAccessor<id,X> – an application resource accessible via accessor X (cojson::accessor::*)
  • resource::FileVariable<id,T,V> – a static variable type T accessible via its pointer V
  • resource::FileFunctions<id,T,G,P> – a pair of functions G/P called on GET/PUT and returing/accepting value of type T
  • resource::FileConstString<id,F,M> – GET-only string content of type M provided by function F
  • resource::FileText<id,N,V> – plain text zero-terminated char buffer V of maximal length N
  • resource::FileBinary<id,L,N,V> – An r/w octet-stream stored in char buffer V of of maximal length N and its current length stored in L

Directories

μcuREST allows hierarchical structuring of the resource map by organizing entries in directories with template resource::Dir

  • resource::Dir<id,...L> – directory with name id nesting a set of entries L…

Short Codes

μcuREST provides optional short names for all Resource Map elements:

  • resource::File…F
  • resource::Entry…E
  • resource::Node…N
  • resource::DirD

Custom Nodes

If none of the listed above templates suite needs of your application, you may define your own Node. A Node is a function returning const reference to an instance of resource::node implementing get and put methods as needed:

const resource::node& MyNode() noexcept {
	static const struct local : resource::node {
		void get(details::message& msg) const noexcept {
			msg.status(status_t::OK);
			ostream& out(msg.obody());
			//...
		}
		void put(details::message& msg) const noexcept {
			istream& in(msg.ibody());
			//...
			msg.status(status_t::No_Content);
		}

	} l;
	return l;
}

Configuration

μcuREST deploys Cascaded Configuration Sets. Each module that uses configurable options has a .ccs file with a default configuration. To change some of the configurable options, you need to specialize Configuration template with a target, a build or both, override options in the body of that template, specialize configuration selector and place all these in the configuration.h file. For example:

#include "micurest/network_spark_socket.ccs"
namespace configuration {
struct Current : Is<target::Photon, build::Default> {};
template<typename build>
struct Configuration<micurest::network_spark_socket::proto::tcp,target::Photon,build>
 : Configuration<micurest::network_spark_socket::proto::tcp,target::All,build> {
	static constexpr short inactivity_timeout = 5000;
};
}

Modularization

In a large or complex project, the application’s Resource Map may not be fitting/suiting a single compilation unit. Since every entry and every node in the resource map is eventually a function, the standard C/C++ modularization approach works for μcuREST as well:

/* definition of resource names */
namespace name {
  NAME(mynode1)
  NAME(mynode2)
}

/* declaration of resource map nodes and entries, implemented in other source files */
extern const resource::node&  mynode1();
extern const resource::node&  mynode2();
extern const resource::entry& myentry();

const directory& resourceMap5() noexcept {
  return Root<
    E<name::mynode1, mynode1>,
    E<name::mynode2, mynode2>,
	myentry
  >();
}

Cross Origin Resource Sharing

MCU is a constrained device – it has very limited storage and computation power. You should take into account these limitations in your application design. A reasonable limitation for every HTTP response is one MTU (~1400) – it does not bring network latency into MCU’s loop, makes the timings predictable and does not much distract MCU’s from its primary tasks. On the other hand, a rich web UI may require large file volumes overwhelming 1.4K limit. To solve this problem, your application may use resources hosted on a cloud server – scripts, stylesheets, images, HTML pages etc.

Using shared scripts, stylesheets, images from a cloud server is easy – once the server is configured for CORS, you just link shared resources in the application’s HTML pages. With the HTML pages itself, (which also may easily exceed 1.4K in size), simple linking hits browser’s security restrictions – JS from such page is not allowed to access MCU because of same origin policy. To workaround this restriction, your application may link a bootstrap script from the cloud, and the script then constructs HTML page on the fly, as many JS frameworks normally do single-pager apps. μcuREST examples also use this approach and load shared HTML pages into an inner iframe.


1cstring is a typedef to const char* on all platforms, except avr, where names are allocated on progmem and cstring is defined as progmem<char> instead.

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*

*