macchina.io EDGE

Getting Started with macchina.io EDGE

System Requirements

macchina.io EDGE can be installed on Linux and macOS systems. On Linux, cross-compilation for embedded Linux targets is supported.

For the first steps, it is recommended to build and install macchina.io EDGE on a desktop Linux or macOS system. This will get you up and running within a few minutes. You can also build macchina.io EDGE on systems like a Raspberry Pi or a BeagleBone Black, but this will take a couple of hours. Alternatively, you can cross-compile macchina.io EDGE for an embedded Linux target, however, this will require a couple of extra steps in order to get everything working.

Instructions for cross-compiling macchina.io EDGE can be found in the Wiki.

Getting macchina.io EDGE

macchina.io EDGE can be obtained from GitHub, by cloning the macchina.io EDGE repository. Alternatively, you can also download a release archive in .zip or .tar.gz format from the macchina.io EDGE GitHub Releases page.

To clone the macchina.io EDGE GitHub repository:

$ git clone https://github.com/macchina-io/macchina.io.git

For the latest stable release, you may want to check out the main branch:

$ cd macchina.io
$ git checkout main

The default branch is the develop branch, which contains the latest and greatest features, but is not recommended for production use.

Building macchina.io EDGE

Prerequisites

macchina.io EDGE uses the build system from the POCO C++ Libraries, which is based on GNU Make. You'll also need a recent version of the GNU C++ compiler (at least 5.0) or Clang 3.4 (on macOS: Xcode Clang/Apple LLVM 8.0 command-line tools). Make sure you have the OpenSSL headers and libraries installed.

Furthermore, Python 3.9 or later is required for building the V8 JavaScript engine.

Ubuntu/Debian/Raspbian

Install all required packages via:

$ sudo apt-get install g++ make libssl-dev python-is-python3

macOS

Install OpenSSL (1.1.1 or 3.x) and Python 3.9 (or newer) via Homebrew:

$ brew install openssl python@3.9

NOTE: On macOS, the python command must run Python 3.9 (or newer) in order to build V8. If installed via Homebrew, add the following directory to your $PATH:

Intel: /usr/local/opt/python@3.9/libexec/bin

Apple Silicon: /opt/homebrew/opt/python@3.9/libexec/bin

Compiling macchina.io EDGE

To build macchina.io EDGE, simply invoke GNU Make in the top-level macchina.io EDGE source directory. On a multicore system with sufficient memory, you may want to run a parallel build, by specifying the -j option.

$ make -s -j8

This command will actually build both debug and release build configurations. To speed things up, you may want to build the release configuration only:

$ make -s -j8 DEFAULT_TARGET=shared_release

On a typical desktop or notebook machine, building macchina.io EDGE takes about 10 to 15 minutes. If you're building on a system like a Raspberry Pi or Tinkerforge RED Brick, building macchina.io EDGE will take a couple of hours. Don't attempt a parallel build (-j8) on these systems. Furthermore, you may run out of memory during the build, especially while building the V8 library. Try to restart the build and make sure as much memory as possible is available, by stopping all unnecessary processes. It may be better to cross-compile on a desktop system and transfer the binaries to the target system.

Cross Compiling macchina.io EDGE

Note: if you want to run macchina.io EDGE on your desktop system only, you can safely skip this section and continue with Running macchina.io EDGE.

macchina.io EDGE can be cross-compiled for an embedded Linux target. To cross-compile for a target, a build configuration for the target must be created, or an existing build configuration selected. A couple of configurations for various Embedded Linux targets can be found in the platform/build/config directory. A build configuration specifies the toolchain/compiler to be used as well as any special compiler or linker arguments required for the target. It's best to create a new configuration by copying an existing one and copying it into the platform/build/config directory. Good starting points for ARM-based targets are BeagleBoard or Yocto. If you have a basic understanding of cross-compiling for embedded Linux systems, the build configuration files should be pretty self explaining.

NOTE: There is also a complete Yocto layer for macchina.io EDGE available from Applied Informatics. This may be the preferred way of building macchina.io EDGE for Yocto-based Linux platforms. Please contact support@appinf.com for details.

There is also a Yocto build configuration available. This is intended to be used with a standalone Yocto toolchain or SDK.

You can find more information about build configurations and the build system in general in then POCO C++ Libraries GNU Make Build System document.

Before cross-compiling for an embedded Linux target, you'll need to compile the sources for a few libraries and tools for the host system. You can do that with the following command:

$ make -s -j8 hosttools

Note: You should not build the entire macchina.io EDGE for both host and target in the same directory tree, except for the required host tools and libraries. After you've built macchina.io EDGE for the target system, the bundle files will no longer be usable on the host as they contain the target binaries only.

If you have found an existing, or created a new suitable build configuration for your target, you can build for the target with the following command (assuming the Yocto build configuration; replace this with your own):

$ POCO_CONFIG=Yocto LINKMODE=SHARED make -s -j8 DEFAULT_TARGET=shared_release

Note: unless you intend to debug on the target, it is sufficient to build the release configuration only.

After the build completes, you'll need to copy the following files to the target:

  • The following shared libraries (libPoco*.so.*) from platform/lib/Linux/<target_arch>: Foundation, XML, JSON, Util, Net, Zip, Crypto, NetSSL, Geo, WebTunnel, RemotingNG and OSP.
  • All bundle files (*.bndl) from platform/OSP/bundles, devices/bundles, protocols/bundles, services/bundles, webui/bundles, samples/bundles.
  • The macchina.io EDGE server executable: server/bin/Linux/<target_arch>/macchina.
  • The macchina.io EDGE server configuration file: server/macchina.properties.

The bundles should all be copied into a separate directory on the target. A recommended filesystem layout on the target would be:

<macchina_root>/
    lib/
        bundles/
    bin/
    etc/

All shared libraries should go into the lib directory, and all bundles go into the lib/bundles directory.

You can use the install_runtime Makefile target to copy all these files into a directory hierarchy like above:

$ POCO_CONFIG=Yocto make -s PRODUCT=runtime INSTALLDIR=/path/to/macchina-install-dir install

Then you can simply pack the directory into an archive (e.g., .tar.gz) file and transfer it to the target.

Cross-Compiling from a 64-bit Host to a 32-bit target

Cross-compiling the V8 JavaScript engine from a 64-bit build host (typically x86_86) to a 32-bit target like (i386, i686 or armv7l) additionally (to the native toolchain) requires a 32-bit toolchain on the host system, as well as the ability to run 32-bit executables compiled with the host-based 32-bit toolchain on the host.

Basically the V8 build system will invoke the native compiler with the -m32 option to build a 32-bit executable that sets up a JavaScript snapshot, and will then run the compiled 32-bit executable on the host to generate that snapshot.

On a Ubuntu or Debian host, the necessary features can be installed with the following commands (for Ubuntu 20.04):

$ sudo dpkg --add-architecture i386
$ sudo apt update
$ sudo apt install -y binutils-multiarch libc6-dev:i386 libstdc++-9-dev:i386 gcc-9-multilib g++-9-multilib

For Ubuntu 22.04, the third command changes slightly, to accommodate for the newer GCC version:

$ sudo dpkg --add-architecture i386
$ sudo apt update
$ sudo apt install -y binutils-multiarch libc6-dev:i386 libstdc++-11-dev:i386 gcc-11-multilib g++-11-multilib

Running macchina.io EDGE

After the macchina.io EDGE build completes, you can directly run macchina.io EDGE without an additional install step. To run the macchina.io EDGE server:

  1. Set your system's shared library search path to include the macchina.io EDGE libraries and the codeCache directory (if necessary, see below). This can be done by sourcing the env.bash script located in the macchina.io EDGE root directory.
  2. Run the macchina.io EDGE server program.

To run macchina.io EDGE on a Linux system:

$ cd /path/to/macchina.io
$ . env.bash
$ cd server
$ bin/Linux/x86_64/macchina

To run macchina.io EDGE on an macOS (Intel) system:

$ cd /path/to/macchina.io
$ . env.bash
$ cd server
$ bin/Darwin/x86_64/macchina

For an Apple Silicon (ARM) system, the path of the executable will be bin/Darwin/arm64/macchina.

Note: if you macchina.io EDGE configuration file macchina.properties is not located in the same directory as the server executable, or in a parent directory of it, you'll need to specify the path to the configuration file when starting the server:

$ bin/Darwin/x86_64/macchina --config=/path/to/macchina.properties

On Debian-based systems, it is necessary to include the codeCache directory in LD_LIBRARY_PATH. If you see lots of errors during startup, complaining about shared libraries that cannot be found, do this:

$ export LD_LIBRARY_PATH=/path/to/macchina.io/platform/lib/Linux/x86_64:/path/to/macchina.io/server/bin/Linux/x86_64/codeCache

Note: the env.bash script already does this for you.

If you run on an embedded device using the directory structure created with the install_runtime make target, start as follows:

$ export LD_LIBRARY_PATH=/path/to/macchina-install-dir/lib:/path/to/macchina-install-dir/bin/codeCache
$ /path/to/macchina-install-dir/bin/macchina --config=/path/to/macchina-install-dir/etc/macchina.properties

After the macchina.io EDGE server has started, which may take a few seconds on slower systems, direct your web browser of choice to http://localhost:22080 (or the IP address of your device). You'll see the macchina.io EDGE login screen. User username "admin" and password "admin" to log in. After successfully logging in you'll see the Launcher page of the web interface. Try out the different web apps to familiarize yourself with the web user interface.

The default configuration of macchina.io EDGE enables simulation of various sensors, including a temperature and an ambient light sensor. You can use these simulated sensors in the same way as you'd use any real connected sensors in your code. The available sensors and their current measurements can be seen on the "Sensors & Devices" app.

There's also the "Sensor Log" app which displays current and time series sensor data from a configurable set of sensors.

To get the most out of macchina.io EDGE, you may want to get some real sensors. In the developer preview release of macchina.io EDGE, only some Tinkerforge sensors are supported. More sensors and devices will be supported in future releases. You can of course also implement you own sensor types, once you're familiar with the macchina.io EDGE Software Development Kit (SDK).

Writing Your First JavaScript Program

In order to write your first JavaScript program that runs on macchina.io EDGE, open the "Playground" app. The Playground allows you to create and edit a JavaScript program directly in the browser, using a comfortable text editor.

The macchina.io EDGE Playground, a JavaScript editor
The macchina.io EDGE Playground, a JavaScript editor

Note that when working with the Playground app, it's a good idea to have a nearby console window with macchina.io EDGE's log output open to see script output or error messages. You can also open the "Console" app in a separate browser window.

Getting Sensor Data

The Playground comes pre-loaded with the macchina.io EDGE variant of the "Hello, world!" program - a little program that finds a temperature sensor and obtains the current temperature from the sensor. The program is reproduced below for reference.

//
// Playground Sample Script
//
// This script will look for a temperature sensor and, if found,
// output the current temperature.
//
// Feel free to modify this script as you like to try out
// macchina.io features.
//

// Search for temperature sensors in the Service Registry
var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
if (temperatureRefs.length > 0)
{
    // Found at least one temperature sensor entry in the registry.
    // Resolve the first one.
    var temperatureSensor = temperatureRefs[0].instance();
    // Get current temperature.
    var temperature = temperatureSensor.value();
    logger.information('Current temperature: ' + temperature);
}
else
{
    logger.error('No temperature sensor found.');
}

The program shows two things:

  1. How to obtain a sensor service object from the Service Registry.
  2. How to write logging output.

To find available sensors in the system, you use the macchina.io EDGE Service Registry. All sensors and devices supported by macchina.io EDGE will always be available as service objects through that registry. In order to find a specific object, the service registry supports a simple query expression language that allows you to find services based on their properties. In the example above, we specifically look for a temperature sensor. In macchina.io EDGE, sensors always have a property named io.macchina.physicalQuantity that can be used to search for sensors that measure a specific physical quantity. The find method will return an array of service references. Service references are different from actual service objects. Their purpose is to store service properties (like the mentioned io.macchina.physicalQuantity), as well as a reference to the actual service object. The actual service object can be obtained by calling the instance() function, like we do in the sample.

Once we have the temperature sensor object, we can use it to obtain the current value by calling the value() function.

In the Playground app, you can run the program by clicking the "Run" button above the editor. Do it and observe the logging output of the macchina server. It should contain something like:

2015-03-07 13:12:31.630 [Information] osp.core.BundleLoader<2>: Bundle io.macchina.webui.playground.sandbox started
2015-03-07 13:12:31.630 [Information] osp.bundle.com.appinf.osp.js<2>: Starting script sandbox.js from bundle io.macchina.webui.playground.sandbox.
2015-03-07 13:12:31.631 [Information] osp.web.access<2>: POST /macchina/playground/run.json HTTP/1.1
2015-03-07 13:12:31.688 [Information] osp.bundle.io.macchina.webui.playground.sandbox<28>: Current temperature: 26.999999999999975

You can also update the macchina.io EDGE web console from the launcher screen to see the output.

The macchina.io EDGE Web Console
The macchina.io EDGE Web Console

In addition to querying the service registry directly for a specific kind of sensor or, more generally, service, a program can also be notified if a service that matches a query is registered with the service registry. This is done using a ServiceListener, and it's specifically useful for services that may appear or disappear at any time.

The following short sample shows the usage of the ServiceListener.

var listener = serviceRegistry.createListener(
    'io.macchina.physicalQuantity == "temperature"',
    (ref) => {
        console.log("Temperature sensor registered: %O", ref);
        var sensor = ref.instance();
        console.log("Current temperature: %f", sensor.value());
    },
    (ref) => {
        console.log("Temperature sensor unregistered: %O", ref);
    });

The createListener() method of the serviceRegistry object takes a query expression (like find()) and two callback functions. The first one will called after a matching service has been registered. The second one will be called after the service has been unregistered.

Periodically Querying Sensor Data

As a next step, we're going to modify the program so that it will periodically output the current temperature. We do this by setting up a timer. Here's the modified code:

var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
if (temperatureRefs.length > 0)
{
    var temperatureSensor = temperatureRefs[0].instance();
    logger.information('Found temperature sensor.');
    setInterval(
        () => {
            logger.information('Current temperature: ' + temperatureSensor.value());
        },
        1000
    );
}
else
{
    logger.error('No temperature sensor found.');
}

Replace the code in the editor with the above code and click the Restart button. You'll see the temperature being printed once every second.

Handling Sensor Events

As the final step, we'll print the temperature only when it changes. For that purpose, sensor objects provide an event that we can bind to a callback function. Whenever the sensor value changes, our callback function will be called and the current temperature will be written to the log.

var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
if (temperatureRefs.length > 0)
{
    var temperatureSensor = temperatureRefs[0].instance();
    temperatureSensor.on('valueChanged',
        event => {
            logger.information('Temperature changed: ' + event.data);
        }
    );
}
else
{
    logger.error('No temperature sensor found.');
}

Important: When subscribing to an event, make sure that the object that provides the event is protected from the JavaScript garbage collector. This is typically done by storing the object in a global variable. Subscribing to an event on an object stored in a local variable (in a function or block) declared with let or const will typically work for some time, until the garbage collector decides to remove the object. After this, no further events will be delivered.

Sending Sensor Data Using MQTT

Let's now extend the sample to send sensor data over the internet to a server, using the MQTT protocol. We're going to use the MQTT sandbox server provided by the Eclipse foundation, which is pre-configured in the macchina.io EDGE configuration file. We'll return to the example using the timer and modify it to send the current temperature every 10 seconds.

var temperatureSensor;
var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
if (temperatureRefs.length > 0)
{
    logger.information('Temperature sensor found.');
    temperatureSensor = temperatureRefs[0].instance();
}
else
{
    logger.error('No temperature sensor found.');
}

var mqttClientRefs = serviceRegistry.find('io.macchina.mqtt.id == "eclipse"');
if (mqttClientRefs.length > 0)
{
    logger.information("MQTT Client found.");

    var mqttClient;
    mqttClient = mqttClientRefs[0].instance();

    setInterval(
        () => {
            var payload = JSON.stringify(temperatureSensor.value());
            logger.information('Sending temperature: ' + payload);
            // Send payload using QoS 0
            mqttClient.publish('macchina-io/samples/temperature', payload, 0);
        },
        10000
    );
}
else
{
    logger.error('No MQTT Client found.');
}

The MQTT client is another service that's available from the service registry. We can discover it by looking for a io.macchina.mqtt.serverURI property. Based on the value of this property, we could also look for a specific client, e.g. "tcp://iot.eclipse.org:1883".

HTTP Web Services

As a final example in this tutorial, we'll show how to invoke a HTTP based web service from JavaScript. If the temperature exceeds a certain limit, we'll send a SMS message using Twilio. Note that for this example to work, you'll need to have a Twilio account with the ability to send SMS messages.

var net = require('net');

function sendSMS(message)
{
    var username = application.config.getString('twilio.username');
    var password = application.config.getString('twilio.password');
    var from = application.config.getString('twilio.from');
    var to = application.config.getString('twilio.to');

    var twilioHttpRequest = new net.HTTPRequest('POST', 'https://api.twilio.com/2010-04-01/Accounts/' + username + '/SMS/Messages');
    twilioHttpRequest.authenticate(username, password);
    twilioHttpRequest.contentType = 'application/x-www-form-urlencoded';
    twilioHttpRequest.content =
        'From=' + encodeURIComponent(from) +
        '&To=' + encodeURIComponent(to) +
        '&Body=' + encodeURIComponent(message);

    var response = twilioHttpRequest.send((result) => {
        logger.information('SMS sent with HTTP status: ' + result.response.status);
        logger.information(result.response.content);
    });
}

var smsSent = false;

var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"');
if (temperatureRefs.length > 0)
{
    var temperatureSensor = temperatureRefs[0].instance();
    temperatureSensor.on('valueChanged',
        (event) => {
            logger.information('Temperature changed: ' + event.data);
            if (event.data > 30 && !smsSent)
            {
            	logger.warning('Temperature limit exceeded. Sending SMS.');
                sendSMS('Temperature limit exceeded!');
                smsSent = true;
            }
        }
    );
}
else
{
    logger.error('No temperature sensor found.');
}

All new code is in the sendSMS() function. We're using the application.config object to access configuration settings from the macchina.properties configuration file. For this example to work you'll need to add the following lines to the configuration file:

twilio.username = <your_twilio_account_sid>
twilio.password = <your_twilio_auth_token>
twilio.from = <your_twilio_outgoing_phone_number>
twilio.to = <recipient_phone_number>

Of course you'll need to replace the placeholders (<your_twilio_account_sid>, etc.) with the proper values from your Twilio account. We're using the HTTPRequest class from the net module for sending the HTTPS POST request to the Twilio API. The HTTPS request is sent asynchronously. We'll notified of the result via the callback function we provide to the send() function.

Securely control IoT edge devices from anywhere   Connect a Device