© Peter Hoddie and Lizzie Prader 2020
P. Hoddie, L. PraderIoT Development for ESP32 and ESP8266 with JavaScripthttps://doi.org/10.1007/978-1-4842-5070-9_3

3. Networking

Peter Hoddie1  and Lizzie Prader1
(1)
Menlo Park, CA, USA
 

There are so many different kinds of IoT devices—from thermostats to door locks, from smart watches to smart light bulbs, from washing machines to security cameras—that it’s easy to forget they all have something in common: the network. What separates an IoT device from an ordinary everyday device is its connection to the network. This chapter is all about that connection, starting with different ways to connect to the network.

Once your device is connected to the network, it can communicate in many different ways. This chapter shows you how to communicate using the same HTTP networking protocol used by the web browser on your computer and phone. It also shows how to use the WebSocket protocol for interactive two-way communication and the MQTT protocol used for publish-and-subscribe.

Securing communication is essential for many products, so you’ll also learn how to make secure connections using TLS (Transport Layer Security) in combination with protocols like HTTP, WebSocket, and MQTT.

The chapter closes with two advanced topics. The first is how to turn your device into a Wi-Fi base station, a technique used by many commercial IoT products for easy configuration. You can connect your computer, phone, and other devices to this private Wi-Fi base station without installing any special software. The second advanced topic is how to use JavaScript promises with networking APIs.

About Networking

This book focuses on hardware that connects to the network using Wi-Fi. Your Wi-Fi access point, also called a base station or router, connects your Wi-Fi network to the internet. The access point also creates a local network which allows devices connected to it to communicate with each other. The HTTP, MQTT, and WebSocket protocols are used to communicate with servers on the internet, but they can also be used to communicate between devices on your local Wi-Fi network. Communicating directly between devices is faster and can be more private because your data never leaves your Wi-Fi network. It eliminates the cost of a cloud service. Using the mDNS network protocol makes it easy for devices on your local network to communicate directly with each other.

All the networking examples in this chapter are non-blocking (or asynchronous ). This means, for example, that when you request data from the network using the HTTP protocol, your application continues running while the request is made. This is the same way networking works when you use JavaScript on the web but different from much of the networking implementations in embedded environments. For various reasons, many embedded development environments use blocking networking instead; this leaves the device unresponsive to user input during the network operation unless a more complex and memory-intensive technique, such as threads, is also used.

The classes in the Moddable SDK that implement networking capabilities use callback functions to provide status and deliver network data. Callbacks are simple to implement, and they operate efficiently even on hardware with relatively little processing power and memory. On the web, developers have long used callbacks for network operations. More recently, a feature of JavaScript called promises has become a popular alternative to callbacks for some situations. Because promises require more resources, they’re used sparingly here. Promises are supported in the XS engine that powers the Moddable SDK. The networking capabilities introduced in this chapter may be adapted to use promises; an example is included in the section on promises at the end of this chapter.

Connecting to Wi-Fi

You already know how to connect your computer and phone (and probably even your television!) to the internet, and that experience will help you when writing the code to connect your device. You’ll need to learn a few new things too, because IoT devices don’t always have a screen, and without a screen the user can’t simply tap the name of the Wi-Fi network to connect to.

This section describes three different ways to connect to Wi-Fi:
  • From the command line

  • With simple code to connect to a known Wi-Fi access point

  • By scanning for an open Wi-Fi access point

Each of these is useful for different situations; you’ll choose the best one for your projects. Using the command line is great for development, but the other two approaches are needed when you move beyond experimenting to building sophisticated prototypes and real products.

Note

This section uses a different installation pattern from the one you learned in Chapter 1: rather than installing the host with mcconfig and then installing examples using mcrun, you install the examples using mcconfig.

Connecting from the Command Line

In Chapter 1, you learned to use the mcconfig command line tool to build and install the host. The mcconfig command can define variables. As shown in the following command, you can connect to a Wi-Fi access point by defining the variable ssid with the value of the name of the Wi-Fi access point. SSID stands for service set identifier and is the technical term for the human-readable name of a Wi-Fi network provided by a Wi-Fi base station.
> mcconfig -d -m -p esp ssid="my wi-fi"

Defining ssid in this way causes a configuration variable to be added to your application which is used by the device’s base networking firmware to connect automatically to Wi-Fi when the device powers up. After the Wi-Fi connection is established, your application is run. This is convenient because it means your application can assume that the network is always available.

If your Wi-Fi access point requires a password, include that on the command line as the value of the password variable:
> mcconfig -d -m -p esp ssid="my wi-fi" password="secret"
During the Wi-Fi connection process, diagnostic trace messages are displayed in the debug console. Watch the messages to help diagnose connection troubles. Here’s an example of a successful connection:
Wi-Fi connected to "Moddable"
IP address 10.0.1.79
If the Wi-Fi password is rejected by the Wi-Fi access point, the following message is displayed:
Wi-Fi password rejected
All other unsuccessful connection attempts display the following message:
Wi-Fi disconnected

Install the $EXAMPLES/ch3-network/wifi-command-line example on your device to test this connection method.

Connecting with Code

Using command line options to define your Wi-Fi credentials is convenient for development, but for projects you share with others you’ll often want to store the Wi-Fi credentials in a preference instead. This section looks at the code to connect to a Wi-Fi access point defined in your application. (Managing preferences is described in Chapter 5.)

The wifi module contains the JavaScript class used to manage Wi-Fi network connections. To use the wifi module in your code, first import the WiFi class from it:
import WiFi from "wifi";
Use the static connect method of the WiFi class to connect to a Wi-Fi network. In the $EXAMPLES/ch3-network/wifi-code example, the SSID and password are passed to the constructor as properties in a dictionary (Listing 3-1).
WiFi.connect({
        ssid: "my wi-fi",
        password: "secret"
    }
);

Listing 3-1.

This call begins the process of establishing a connection. The call is asynchronous, which means that the actual work of connecting takes place in the background; the application keeps running while the connection is being established. This is just like on your phone, where you can continue using apps while a Wi-Fi connection is being established. In an IoT device, you often want to know when the network connection is available so that you’ll know when your application can make connections to other devices and the internet.

To monitor the connection status, create an instance of the WiFi class and provide a monitoring callback function (Listing 3-2) to be called whenever the connection status changes.
let wifiMonitor = new WiFi({
        ssid: "my wi-fi",
        password: "secret"
    },
    function(msg) {
        switch (msg) {
            case WiFi.gotIP:
                trace("network ready ");
                break;
            case WiFi.connected:
                trace("connected ");
                break;
            case WiFi.disconnected:
                trace("connection lost ");
                break;
        }
    }
);

Listing 3-2.

The callback function is called with one of these three messages, depending on the connection status:
  • connected – Your device has connected to the Wi-Fi access point. It’s not yet ready to use, however, because it hasn’t yet received its IP address. When you see this message, you know that the SSID and password are valid.

  • gotIP – Your device has received its IP address and is now ready to communicate with other devices on the local network and the internet.

  • disconnected – Your device has lost its network connection. On some devices, you receive this message before receiving a connect message.

Some projects keep the WiFi object active all the time to monitor for network disconnections. If you don’t need to monitor for a dropped network connection, you should close the WiFi object to free the memory it’s using.
wifiMonitor.close();

Closing the WiFi object does not disconnect from the Wi-Fi network. It simply means your callback function will no longer be called with notifications about the callback status.

To disconnect from the Wi-Fi network, call the WiFi class’s static disconnect method:
WiFi.disconnect();
To test this connection method, take these steps:
  1. 1.

    Open $EXAMPLES/ch3-network/wifi-code/main.js in your text editor.

     
  2. 2.

    Change lines 4 and 5 so that ssid and password match your network credentials.

     
  3. 3.

    Install the $EXAMPLES/ch3-network/wifi-code example on your device from the command line using mcconfig.

     
If the connection is successful, you’ll see the following messages traced to the debug console:
connected
network ready

If the connection is unsuccessful, you’ll instead see connection lost displayed repeatedly.

Connecting to Any Open Access Point

Sometimes you’ll want your IoT device to connect to any available open Wi-Fi access point (for example, one that doesn’t require a password). Connecting to an unknown network isn’t a good idea from a security perspective, but in some situations the convenience is more important.

To connect to an open access point, the first step is to find one. The WiFi class provides the static scan method to look for access points. The code in Listing 3-3 performs a single scan for access points, logging the results to the debug console. It gets the signal strength from the rssi property of accessPoint. RSSI stands for received signal strength indication and is a measure of the strength of the signal received from the Wi-Fi access point. Its values are negative numbers, and stronger signals have an RSSI value closer to 0.
WiFi.scan({}, accessPoint => {
    if (!accessPoint) {
        trace("scan complete ");
        return;
    }
    let name = accessPoint.ssid;
    let open = "none" === accessPoint.authentication;
    let signal = accessPoint.rssi;
    trace(`${name}: open=${open}, signal=${signal} `);
});

Listing 3-3.

Here’s an example of this code’s output:
ESP_E5C7AF: open=true, signal=-62
Large Conf.: open=false, signal=-85
Expo 2.4: open=false, signal=-74
PAB: open=true, signal=-77
Kanpai: open=false, signal=-66
Moddable: open=false, signal=-70
scan complete

The duration of a scan is typically less than 5 seconds, varying somewhat by device. During the scan, the example traces the name of the access point, whether it’s open, and its signal strength. When the scan is complete, the scan callback function is called with the accessPoint argument set to undefined and the message scan complete is traced.

If you’re in a location with many access points, a single scan may not discover every available access point. To build a complete list, your application can merge the results of several scans. See wifiscancontinuous in the Moddable SDK for an example.

A user choosing a Wi-Fi access point to connect to usually selects the one with the strongest strength. The $EXAMPLES/ch3-network/wifi-open-ap example performs the same selection process using the code in Listing 3-4.
let best;
WiFi.scan({}, accessPoint => {
    if (!accessPoint) {
        if (!best) {
            trace("no open access points found ");
            return;
        }
        trace(`connecting to ${best.ssid} `);
        WiFi.connect({ssid: best.ssid});
        return;
    }
    if ("none" !== accessPoint.authentication)
        return; // not open
    if (!best) {
        best = accessPoint; // first open access point found
        return;
    }
    if (best.rssi < accessPoint.rssi)
        best = accessPoint; // new best
});

Listing 3-4.

This code uses the variable best to keep track of the open access point with the strongest signal strength during scanning. After the scan completes, the code connects to that access point.

To test this method, install the wifi-open-ap example on your device.

Installing the Network Host

The host is in the $EXAMPLES/ch3-network/host directory. Navigate to this directory from the command line and install it with mcconfig.

Installing Examples

The examples in this chapter only work properly if the device is connected to a Wi-Fi access point. Earlier in this chapter, you learned how to specify the SSID and password of an access point by defining variables in the mcconfig command. You can use these same variables in the mcrun command to connect your device to Wi-Fi before the example runs.
> mcrun -d -m -p esp ssid="my wi-fi"
> mcrun -d -m -p esp ssid="my wi-fi" password="secret"

Getting Network Information

When working with the network, you may need information about the network interface or network connection, for debugging purposes or to implement features. This information is available from the net module .
import Net from "net";
Information is retrieved from the Net object using its static get method. This example retrieves the name of the Wi-Fi access point that the device is connected to:
let ssid = Net.get("SSID");
Here are some other pieces of information you can retrieve:
  • IP – the IP address of the network connection; for example, 10.0.1.4

  • MAC – the MAC address of the network interface; for example, A4:D1:8C:DB:C0:20

  • SSID – the name of the Wi-Fi access point

  • BSSID – the MAC address of the Wi-Fi access point; for example, 18:64:72:47:d4:32

  • RSSI – the Wi-Fi signal strength

Making HTTP Requests

The most commonly used protocol on the internet is HTTP, and there are many good reasons for its popularity: it’s relatively simple, it’s widely supported, it works well for small and large amounts of data, it has proven to be extremely flexible, and it can be supported on a wide range of devices, including the relatively inexpensive ones found in many IoT products. This section shows how to make different kinds of HTTP requests to an HTTP server. (The next section will show how to secure those connections.)

Fundamentals

The http module contains support for making HTTP requests and creating an HTTP server. To make an HTTP request, first import the Request class from the module:
import {Request} from "http";
The Request class uses a dictionary to configure the request. There are just two required properties in the dictionary:
  • Either a host property or an address property to define the server to connect with, where host specifies the server by name (for example, www.example.com) and address defines the server by IP address (for example, 10.0.1.23)

  • A path property to specify the path to the HTTP resource to access (for example, /index.html or /data/lights.json)

All other properties are optional; the kind of HTTP request you’re making determines whether they’re present and what their values are. Many of the optional properties are introduced in the following sections.

In addition to the configuration dictionary, each HTTP request has a callback function that’s invoked throughout the various stages of the request. The callback receives a message corresponding to the current stage. Here’s the complete list of the stages of an HTTP request:
  • requestFragment – The callback is being asked to provide the next part of the request body.

  • status – The status line of the HTTP response has been received. The HTTP status code (for example, 200, 404, or 301) is available. The status code indicates the success or failure of the request.

  • header – An HTTP response header has been received. This message is repeated for each HTTP header received.

  • headersComplete – This message is received between receipt of the final HTTP response header and receipt of the response body.

  • responseFragment – This message provides a fragment of the HTTP response and may be received multiple times.

  • responseComplete – This message is received after all HTTP response fragments.

  • error – A failure occurred while processing the HTTP request.

If this looks overwhelming, don’t worry; many HTTP requests use only one or two of these messages. Two of the messages, requestFragment and responseFragment, are only used to work with HTTP data that’s too big to fit in the memory of the device. The sections that follow show how to use many of the available messages.

GET

The most common HTTP request is GET , which retrieves a piece of data. The code in Listing 3-5 from the $EXAMPLES/ch3-network/http-get example performs an HTTP GET to get the home page from the web server www.example.com.
let request = new Request({
    host: "www.example.com",
    path: "/",
    response: String
});
request.callback = function(msg, value) {
    if (Request.responseComplete === msg)
        trace(value, " ");
}

Listing 3-5.

The response property in the call to the Request constructor specifies how you would like the body of the response to be returned. In this case, you’re specifying that it should be returned as a JavaScript string. The callback receives the responseComplete message when the response—the entire web page—is received. The web page is stored in the value parameter. The call to trace displays the source HTML in the debug console.

You can use this approach in your projects to retrieve text data. If you want to retrieve binary data, you can do that by passing a value of ArrayBuffer instead of String for the response property , as in Listing 3-6.
let request = new Request({
    host: "httpbin.org",
    path: "/bytes/1024",
    response: ArrayBuffer
});

Listing 3-6.

Getting the entire HTTP response at once works perfectly well as long as there’s enough memory on the device to hold it. If there’s not enough memory, the request fails with an error message. The next section explains how to retrieve sources that are bigger than available memory.

Streaming GET

In situations where the response to an HTTP request may not fit into available memory, you can make a streaming HTTP GET request instead. This is just a little more complicated, as shown in Listing 3-7 from the $EXAMPLES/ch3-network/http-streaming-get example.
let request = new Request({
    host: "www.bing.com",
    path: "/"
});
request.callback = function(msg, value, etc) {
    if (Request.responseFragment === msg)
        trace(this.read(String), " ");
    else if (Request.responseComplete === msg)
        trace(` Transfer complete. `);
}

Listing 3-7.

Notice that in the call to the constructor, the response property is not present. The absence of that property tells the HTTP Request class to deliver each fragment of the response body to the callback as it’s received, with the responseFragment message. In this example, the callback then reads the data as a string to trace to the debug console, but it could also read the data as an ArrayBuffer. Instead of tracing to the debug console, the callback might write the data to a file; you’ll learn how to do this in Chapter 5.

When you stream an HTTP request, the body of the response is not provided in the value argument with the responseComplete message.

The Request class supports the chunked transfer encoding feature of the HTTP protocol. This feature is often used to deliver large responses. The HTTP Request class decodes the chunks before invoking the callback function. Therefore, your callback function doesn’t need to parse the chunk headers, simplifying your code.

GET JSON

IoT products don’t usually request web pages unless they’re scraping a page to extract data; instead, they use REST APIs, which very often respond with JSON. Since JSON is a very small data-only subset of JavaScript, it’s extremely convenient to use in JavaScript code. Listing 3-8 is an example of a request to a REST weather service. The application ID used in $EXAMPLES/ch3-network/http-get-json is only an example; you should sign up for your own application ID (APPID) at openweathermap.org and use it instead.
const APPID = "94de4cda19a2ba07d3fa6450eb80f091";
const zip = "94303";
const country = "us";
let request = new Request({
    host: "api.openweathermap.org",
    path: `/data/2.5/weather?appid=${APPID}&` +
          `zip=${zip},${country}&units=imperial`
    response: String
});
request.callback = function(msg, value) {
    if (Request.responseComplete === msg) {
        value = JSON.parse(value);
        trace(`Location: ${value.name} `);
        trace(`Temperature: ${value.main.temp} F `);
        trace(`Weather: ${value.weather[0].main}. `);
    }
}

Listing 3-8.

Notice that in the dictionary passed to the Request constructor, response is set to String, just as in the GET example earlier. The response is requested as String because JSON is a text format. Once the response is available, the callback receives the responseComplete message, and then it uses JSON.parse to convert the string it received to a JavaScript object. Finally, it traces three values from the response to the debug console.

If you want to know all the available values returned by the weather service, you can either read their documentation or look at the response directly in the debug console. To look in the debugger, set a breakpoint on the first trace call; when stopped at the breakpoint, expand the value property to see the values, as shown in Figure 3-1.
../images/474664_1_En_3_Chapter/474664_1_En_3_Fig1_HTML.jpg
Figure 3-1

Expanded JSON weather response shown in xsbug

As you can see in this figure, the JSON returned by the server contains many properties that the JavaScript code doesn’t use, such as clouds and visibility. In some situations there’s enough memory on the device to hold the entire JSON text but not enough memory to hold the JavaScript object created by calling JSON.parse. The object may use more memory than the text because of the way JavaScript objects are stored in memory. To help solve this problem, the XS JavaScript engine supports an optional second parameter to the JSON.parse call. If the second parameter is an array, only the property names in the array are parsed from the JSON. This can significantly reduce the memory used, and the parsing runs faster too. Here’s how to change the call to JSON.parse in the preceding example to decode only the properties the example uses:
value = JSON.parse(value, ["main", "name", "temp", "weather"]);

Subclassing an HTTP Request

The HTTP Request class is a low-level class that provides a great deal of functionality with a high degree of efficiency, giving it the power and flexibility necessary for a broad range of IoT scenarios. Still, for any given situation, the functional purpose of the code can be obscured by details related to the HTTP protocol. Consider the code in Listing 3-8 from the preceding section: the inputs are the ZIP code and country, and the outputs are the current weather conditions, but everything else is an implementation detail.

A good way to simplify the code is to create a subclass. A well-designed subclass provides a focused, easy-to-use API that takes only the relevant inputs (for example, the ZIP code) and provides only the desired outputs (for example, the weather conditions). The $EXAMPLES/ch3-network/http-get-subclass example (Listing 3-9) shows a subclass design for the weather request in the preceding section.
const APPID = "94de4cda19a2ba07d3fa6450eb80f091";
class WeatherRequest extends Request {
    constructor(zip, country) {
        super({
            host: "api.openweathermap.org",
            path: `/data/2.5/weather?appid=${APPID}&` +
                  `zip=${zip},${country}&units=imperial`,
            response: String
        });
    }
    callback(msg, value) {
        if (Request.responseComplete === msg) {
            value = JSON.parse(value,
                          ["main", "name", "temp", "weather"]);
            this.onReceived({
                temperature: value.main.temp,
                condition: value.weather[0].main}
            );
        }
    }
}

Listing 3-9.

Using this WeatherRequest subclass is easy (Listing 3-10), as all the details of the HTTP protocol, the openweathermap.org API, and JSON parsing are hidden in the implementation of the subclass.
let weather = new WeatherRequest(94025, "us");
weather.onReceived = function(result) {
    trace(`Temperature is ${result.temperature} `);
    trace(`Condition is ${result.condition} `);
}

Listing 3-10.

Setting Request Headers

The HTTP protocol uses headers to communicate additional information about the request to the server. For example, it’s common to include the name and version of the product making the HTTP request in the User-Agent header, one of the standard HTTP headers. You may also include nonstandard HTTP headers with the request to communicate information to a particular cloud service.

Listing 3-11 shows how to add headers to an HTTP request. It adds the standard User-Agent header and a custom X-Custom header. The headers are provided in an array, with the name of each header followed by its value.
let request = new Request({
    host: "api.example.com",
    path: "/api/status",
    response: String,
    headers: [
        "User-Agent", "my_iot_device/0.1 example/1.0",
        "X-Custom", "my value"
    ]
});

Listing 3-11.

Specifying the headers in an array rather than in a dictionary or a Map object is somewhat unusual. It’s done here because it’s more efficient and reduces the resources needed on the IoT device.

Getting Response Headers

The HTTP protocol uses headers to communicate additional information about the response to the client. A common header is Content-Type, which indicates the data type of the response (such as text/plain, application/json, or image/png). The response headers are delivered to the callback function with the header message. One header is delivered at a time, to reduce memory use by avoiding the need to store all received headers in memory at once. When all response headers have been received, the callback is invoked with the headersComplete message.

Listing 3-12 checks all headers received for a Content-Type header. If one is found, its value is stored in the variable contentType. After all headers are received, the code checks to see that a Content-Type header was received (that is, contentType is not undefined) and that the content type is text/plain.
let contentType;
request.callback = function(msg, value, etc) {
    if (Request.header === msg) {
        if ("content-type" === value)
            contentType = etc;
    }
    else if (Request.headersComplete === msg) {
        trace("all headers received ");
        if ((undefined === contentType) ||
            !contentType.toLowerCase().startsWith("text/plain"))
            this.close();
    }
}

Listing 3-12.

The names of HTTP headers are case-insensitive by definition, so Content-Type, content-type, and CONTENT-TYPE all refer to the same header. The HTTP Request class converts the name of the header to lowercase, so the callback can always use lowercase letters in header name comparisons.

POST

All the examples of HTTP requests so far have used the default HTTP request method of GET and have had an empty request body. The HTTP Request class supports setting the request method to any value, such as POST , and providing a request body.

The $EXAMPLES/ch3-network/http-post example (Listing 3-13) makes a POST call to a web server with a JSON request body. The method property of the dictionary defines the HTTP request method, and the body property defines the contents of the request body. The request body may be either a string or an ArrayBuffer. The request is posted to a server that echoes back the JSON response. The callback function traces the echoed JSON values to the debug console.
let request = new Request({
    host: "httpbin.org",
    path: "/post",
    method: "POST",
    body: JSON.stringify({string: "test", number: 123}),
    response: String
});
request.callback = function(msg, value) {
    if (Request.responseComplete === msg) {
        value = JSON.parse(value);
        trace(`string: ${value.json.string} `);
        trace(`number: ${value.json.number} `);
    }
}

Listing 3-13.

This example stores the entire request body in memory. In some situations, there’s not enough free memory available to store the request body, such as when uploading a large file. The HTTP Request class supports streaming of the request body; for an example of this, see the examples/network/http/httppoststreaming example in the Moddable SDK.

Handling Errors

Sometimes an HTTP request fails, possibly due to a network failure or a problem with the request. In all cases, the failure is nonrecoverable. Therefore, you need to decide how to handle the error in a way that’s appropriate for your IoT product, such as reporting it to the user, retrying immediately, retrying later, or just ignoring the error. If you’re not yet ready to add error handling to your project, adding a diagnostic trace on error is a good start, as it helps you see failures during development.

When the failure is due to a network error—network failure, DNS failure, or server fault—your callback is invoked with the error message. The following example shows a callback that traces the failure to the debug console:
request.callback = function(msg, value) {
    if (Request.error === msg)
        trace(`http request failed: ${value} `);
}
If the failure is due to a problem with the request—it was badly formed, the path is invalid, or you’re not properly authorized—the server responds with an error in the HTTP status code. The HTTP Request class provides the status code to the callback in the status message. For many web services, a status code from 200 to 299 means the request succeeded, while others indicate a failure. Listing 3-14 demonstrates handling HTTP status codes.
request.callback = function(msg, value) {
    if (Request.status === msg) {
        if ((value < 200) || (value > 299))
            trace(`http status error: ${value} `);
    }
}

Listing 3-14.

Securing Connections with TLS

Secure communication is an important part of most IoT products. It helps maintain the privacy of the data generated by the product and prevents tampering with the data while it’s moving from the device to the server. On the web, most communication is secured using Transport Layer Security, or TLS , which replaces Secure Sockets Layer (SSL). TLS is a low-level tool for securing communication that works with many different protocols. This section explains how to use TLS with the HTTP protocol. The same approach applies to the WebSocket and MQTT protocols, described later.

Working with TLS on an embedded device is a bit more challenging than on a computer, server, or mobile device because of the reduced memory, processing power, and storage. In fact, establishing a secure TLS connection is the most computationally demanding task many IoT products perform.

Using TLS with the SecureSocket Class

The SecureSocket class implements TLS in a way that can be used with various network protocols. To use SecureSocket, you must first import it:
import SecureSocket from "securesocket";
To make a secure HTTP request (HTTPS), add a Socket property with the value of SecureSocket, which tells the HTTP Request class to use the secure socket instead of the default standard socket. Listing 3-15 is an excerpt from the $EXAMPLES/ch3-network/https-get example that shows the dictionary from the earlier HTTP GET example (Listing 3-5) modified to make an HTTPS request.
let request = new Request({
    host: "www.example.com",
    path: "/",
    response: String,
    Socket: SecureSocket
});

Listing 3-15.

The callback does not change from the original example.

Public Certificates

Certificates are an important part of how TLS provides security: they enable the client to verify the identity of the server. Certificates are built into the software of the IoT product just as they’re built into a web browser, with one difference: whereas a web browser can store hundreds of certificates—enough to verify the identity of all publicly available servers on the internet—an IoT product doesn’t have enough storage to hold so many certificates. Fortunately, an IoT product typically communicates with only a few servers, so you can include only the certificates you need.

Certificates are data, so they’re stored in resources that applications can access rather than in code. The manifest for the HTTPS GET example includes the certificate needed to verify the identity of www.example.com (Listing 3-16).
"resources": {
    "*": [
        "$(MODULES)/crypt/data/ca107"
    ]
}

Listing 3-16.

If you try to access a website and the certificate’s resource is not available, the TLS implementation throws an error like the one shown in Figure 3-2.
../images/474664_1_En_3_Chapter/474664_1_En_3_Fig2_HTML.jpg
Figure 3-2

TLS certificate error message in xsbug

The error shows the number of the missing resource, so you can modify the manifest to include that resource (Listing 3-17).
"resources": {
    "*": [
        "$(MODULES)/crypt/data/ca106"
    ]
}

Listing 3-17.

This works because the Moddable SDK includes the certificates for most public websites. The next section describes how to connect to a server that uses a private certificate.

Private Certificates

Private certificates provide additional security by ensuring that only IoT products that have the private certificate are able to connect to the server. The private certificate is usually provided in a file with a .der extension. To use a private certificate in your project, first put the certificate in the same directory as your manifest and modify the manifest to include it (Listing 3-18). Note that the manifest does not include the .der file name extension.
"resources": {
    "*": [
        "./private_certificate"
    ]
}

Listing 3-18.

Next, as shown in Listing 3-19, your application loads the certificate from the resource and passes it to the HTTP request in the secure property of the constructor’s dictionary.
import Resource from "resource";
let cert = new Resource("private_certificate.der");
let request = new Request({
    host: "iot.privateserver.net",
    path: "/",
    response: String,
    Socket: SecureSocket,
    secure: {
        certificate: cert
    }
});

Listing 3-19.

Creating an HTTP Server

Including an HTTP server in your IoT product opens up many possibilities, such as enabling your product to do the following:
  • Provide web pages to users on the same network, which is a great way to provide a user interface for products without a display

  • Provide a REST API for applications and other devices to communicate with

Fundamentals

To create an HTTP server, first import the Server class from the http module:
import {Server} from "http";
Like the HTTP Request class, the HTTP Server class is configured with a dictionary object. There are no required properties in the dictionary. Also like HTTP Request, HTTP Server uses a callback function to deliver messages at the various stages of responding to an HTTP request. Here’s the complete list of the stages of an HTTP request:
  • connection – The server has accepted a new connection.

  • status – The status line of the HTTP request has been received. The request path and request method are available.

  • header – An HTTP request header has been received. This message is repeated for each HTTP header received.

  • headersComplete – This message is received between receipt of the final HTTP request header and receipt of the request body.

  • requestFragment – (For streaming request bodies only) A fragment of the request body is available.

  • requestComplete – The entire request body has been received.

  • prepareResponse – The server is ready to begin delivering the response. The callback returns a dictionary describing the response.

  • responseFragment – (For streaming responses only) The callback responds to this message by providing the next fragment of the response.

  • responseComplete – The entire response has been delivered successfully.

  • error – A failure occurred before the HTTP response was completely delivered.

The examples that follow show how to use many of these messages. Most applications working with the HTTP Server class use only a few of them.

Responding to a Request

An HTTP server responds to all kinds of different requests. Listing 3-20 is an excerpt from the $EXAMPLES/ch3-network/http-server-get example that responds to each request with plain text that indicates the HTTP method used for the response (usually GET) and the path of the HTTP resource requested. Both the method and the path are provided to the callback with the status message. The callback stores these values to return them in the text when it receives the prepareResponse message .
let server = new Server({port: 80});
server.callback = function(msg, value, etc) {
    if (Server.status === msg) {
        this.path = value;
        this.method = etc;
    }
    else if (Server.prepareResponse === msg) {
        return {
            headers: ["Content-Type", "text/plain"],
            body: `hello. path "${this.path}".
                   method "${this.method}".`
        };
    }
}

Listing 3-20.

When you run this example, the IP address of the device is displayed in the debug console as follows:
Wi-Fi connected to "Moddable"
IP address 10.0.1.5
After the IP address is displayed, you can use a web browser on the same network to connect to the web server. When you enter http://10.0.1.5/test.html in the address bar of the browser, you receive the following response:
hello. path "/test.html". method "GET".

Notice that the callback does not set the Content-Length field. When you use the body property, the server implementation adds the Content-Length header automatically.

The body property in this example is a string, but it may also be an ArrayBuffer to respond with binary data.

Responding to JSON PUT

Often a REST API receives its input as JSON in the request body and provides its output as JSON in the response body. The $EXAMPLES/ch3-network/http-server-put example is a JSON echo server which replies to every message it receives by sending back that message. The example expects the client to use the PUT method to send a JSON object. The response embeds that JSON object into a larger JSON object that also includes an error property.

When the status message is received, the server verifies that it’s a PUT method; otherwise, the server closes the connection to reject the request. The callback returns String when it receives the status message, to indicate that it wants the entire request body at one time as a string. To receive the request body as binary data instead, it may return ArrayBuffer.

In response to the requestComplete message, the server parses the JSON input and embeds it into the object used to generate the response. When the prepareResponse message is received, the server in Listing 3-21 returns the response body JSON as a string and sets the Content-Type header to application/json.
let server = new Server;
server.callback = function(msg, value, etc) {
    switch (msg) {
        case Server.status:
            if ("PUT" !== etc)
                this.close();
            return String;
        case Server.requestComplete:
            this.json = {
                error: "none",
                request: JSON.parse(value)
            };
            break;
        case Server.prepareResponse:
            return {
                headers: ["Content-Type", "application/json"],
                body: JSON.stringify(this.json)
            };
    }
}

Listing 3-21.

Since this example doesn’t pass a dictionary to the Server constructor, the default of port 80 is used.

You can use the following command to try the http-server-put example using the curl command line tool. You’ll need to change <IP_address> to match the IP address of your development board (for example, 192.168.1.45). The command posts the simple JSON message in the --data argument to the server and displays the result to the debug console.
> curl http://<IP_address>/json
    --request PUT
    --header "Content-Type: application/json"
    --data '{"example": "data", "value": 101}'

Receiving a Streaming Request

When a large request body is sent to the HTTP server, it may be too large to fit in memory. This can happen, for example, when you upload data to store in a file. The solution is to receive the request body in fragments rather than all at once. Listing 3-22 from the $EXAMPLES/ch3-network/http-server-streaming-put example logs an arbitrarily large text request to the debug console. To ask the HTTP Server class to deliver the request body in fragments, the callback returns true to the prepareRequest message. The fragments are delivered with the requestFragment message and traced to the debug console. The requestComplete message indicates that all request body fragments have been delivered.
let server = new Server;
server.callback = function(msg, value) {
    switch (msg) {
        case Server.status:
            trace(" ** begin upload to ${value} ** ");
            break;
        case Server.prepareRequest:
            return true;
        case Server.requestFragment:
            trace(this.read(String));
            break;
        case Server.requestComplete:
            trace(" ** end of file ** ");
            break;
    }
}

Listing 3-22.

You can adapt this example to write the received data where your application needs it rather than to the debug console. For example, in Chapter 5 you’ll learn the APIs to write the data to a file.

To try this example, use the curl command line tool as shown in the following. You’ll need to change <directory_path> and <IP_address> for your configuration.
> curl --data-binary "@/users/<directory_path>/test.txt"
    http://<IP_address>/test.txt -v

Sending a Streaming Response

If the response to an HTTP request is too large to fit into memory, the response can be streamed instead. This approach is appropriate for file downloads. As shown in Listing 3-23, the $EXAMPLES/ch3-network/http-server-streaming-get example generates a response of random length containing random integers from 1 to 100. To indicate that the response body is to be streamed, the callback sets the body property to true in the dictionary returned from the prepareResponse message. The server invokes the callback repeatedly with the responseFragment message to get the next part of the response. The callback returns undefined to indicate the end of the response.
let server = new Server;
server.callback = function(msg, value) {
    if (Server.prepareResponse === msg) {
        return {
            headers: ["Content-Type", "text/plain"],
            body: true
        };
    }
    else if (Server.responseFragment === msg) {
        let i = Math.round(Math.random() * 100);
        if (0 === i)
            return;
        return i + " ";
    }
}

Listing 3-23.

This example returns string values for the response body, but it may also return ArrayBuffer values to provide binary data. When the responseFragment message is received, the value argument to the callback indicates the maximum number of bytes that the server is prepared to accept for this fragment. When you stream a file, this can be used as the number of bytes to read from the file for the fragment.

The HTTP Server class sends streaming response bodies using chunked transfer encoding. For response bodies where the length is known, the server uses the default identity encoding to send the body without a transfer encoding header and includes a Content-Length header.

mDNS

Multicast DNS, or mDNS , is a collection of capabilities that make it easier for devices to work together on a local network. You probably know the DNS (Domain Name System) protocol because it’s how your web browser finds the network address for the website you enter in the address bar (for example, it’s how the browser converts www.example.com to 93.184.216.34). DNS is designed to be used by the entire internet. In contrast, mDNS is designed to work only on your local network—for example, for all the devices connected to your Wi-Fi access point. DNS is a centralized design that depends on authoritative servers to map names to IP addresses, whereas mDNS is entirely decentralized, with each individual device answering requests to map its name to an IP address.

In this section, you’ll learn how to use mDNS to give your IoT device a name, like porch-light.local, so that other devices can find it by name rather than have to know its IP address. You’ll also learn to use another part of mDNS, DNS-SD (DNS Service Discovery), to find services provided by devices (such as finding all printers or all web servers) and to advertise your device’s services on the local network.

The mdns module contains the JavaScript classes you use to work with mDNS and DNS-SD from your application. To use the mdns module in your code, first import it as follows:
import MDNS from "mdns";
Note

mDNS is well supported on macOS, Android, iOS, and Linux. Windows 10 does not fully support mDNS yet, so you may need to install additional software to use it there.

Claiming a Name

mDNS is commonly used to assign a name to a device for use on the local network. mDNS names are always in the .local domain, as in thermostat.local. You can pick any name you like for a device. The device must check to see whether the name is already in use, because it won’t work to have multiple devices responding to the same name. The process of checking is called claiming . The claiming process lasts a few seconds. If a conflict is found, mDNS defines a negotiation process. At the end of the negotiation, only one device has the requested name and the other selects an unused name. For example, if you try to claim iotdevice unsuccessfully, you may end up with iotdevice-2.

The $EXAMPLES/ch3-network/mdns-claim-name example shows the process of claiming a name (see Listing 3-24). The MDNS constructor is invoked with a dictionary that contains the hostName property with the value of the desired name. There’s a callback function that receives progress messages during the claiming process. When the name message is received with a non-null value, the claimed name is traced to the debug console.
let mdns = new MDNS({
        hostName: "iotdevice"
    },
    function(msg, value) {
        if ((MDNS.hostName === msg) && value)
            trace(`Claimed name ${value}. `);
    }
);

Listing 3-24.

Once a device has claimed a name, you can use the name to access the device. For example, you can use the ping command line tool to confirm that the device is online.
> ping iotdevice.local

Finding a Service

By claiming a name, your device becomes easier to communicate with, but at best the name gives only a small hint about what the device does. It would be helpful to know that the device is a light, thermostat, speaker, or web server so that you could write code that works with it without any configuration. That’s the problem that DNS-SD solves: it’s a way to advertise the capabilities of your IoT product on the local network.

Each kind of DNS-SD service has a unique name. For example, a web server service has the name http and a network file system has the name nfs. The $EXAMPLES/ch3-network/mdns-discover example shows how to search for all the web servers advertising on your local network. There may be web servers on your network that you aren’t aware of, because many printers have a built-in web server for configuration and management.

As shown in Listing 3-25, the mdns-discover example creates an MDNS instance without claiming a name. It installs a monitoring callback function to be notified when an http service is found. For each service found, it makes an HTTP request for the home page of the device and traces its HTTP headers to the debug console.
let mdns = new MDNS;
mdns.monitor("_http._tcp", function(service, instance) {
    trace(`Found ${service}: "${instance.name}" @ ` +
          `${instance.target} ` +
          `(${instance.address}:${instance.port}) `);
    let request = new Request({
        host: instance.address,
        port: instance.port,
        path: "/"
    });
    request.callback = function(msg, value, etc) {
        if (Request.header === msg)
            trace(`  ${value}: ${etc} `);
        else if (Request.responseComplete === msg)
            trace(" ");
        else if (Request.error === msg)
            trace("error ");
    };
});

Listing 3-25.

The instance argument to the callback function has several properties for working with the device:
  • name – the human-readable name of the device

  • target – the mDNS name of the device (for example, lightbulb.local)

  • address – the IP address of the device

  • port – the port used to connect to the service

Here’s the output from the example when it finds an HP printer with an http service:
Found _http._tcp: "HP ENVY 7640 series"
                @hpprinter.local (192.168.1.223:80)
    server: HP HTTP Server; HP ENVY 7640 series - E4W44A;
    content-type: text/html
    last-modified: Mon, 23 Jul 2018 10:53:51 GMT
    content-language: en
    content-length: 658

Advertising a Service

Your device can use DNS-SD to advertise the services it provides, which enables other devices on the same network to find and use those services.

The $EXAMPLES/ch3-network/mdns-advertise example defines the service it provides in a JavaScript object stored in the variable httpService. The service description says that the example supports the http service and makes it available on port 80. Listing 3-26 defines the HTTP service for DNS-SD.
let httpService = {
    name: "http",
    protocol: "tcp",
    port: 80
};

Listing 3-26.

The example then creates an MDNS instance to claim the name server. Once the name has been claimed, the script in Listing 3-27 adds the http service. The service cannot be added before the name is claimed because DNS-SD requires each service to be associated with an mDNS name.
let mdns = new MDNS({
        hostName: "server"
    },
    function(msg, value) {
        if ((MDNS.hostName === msg) && value)
            mdns.add(httpService);
    }
);

Listing 3-27.

After the service is added, other devices may find it, as shown earlier in the section “Finding a Service.”

The full mdns-advertise example also contains a simple web server that listens on port 80. When you run the example, you can type server.local into your web browser to view the response from the web server.

WebSocket

The WebSocket protocol is a good alternative to HTTP when you need frequent two-way communication between devices. When two devices communicate using WebSocket, a network connection is kept open between them, enabling efficient communication of brief messages such as sending a sensor reading or a command to turn on a light. In HTTP, one device is the client and the other is the server; only the client can make a request, and the server always responds. WebSocket, on the other hand, is a peer-to-peer protocol, enabling both devices to send and receive messages. It’s often a good choice for IoT products that need to send many small messages. However, because it keeps a connection open at all times between two devices, it usually requires more memory than HTTP.

The WebSocket protocol is implemented by the websocket module, which contains both WebSocket client and WebSocket server support. Your project can import one or both, as needed.
import {Client} from "websocket";
import {Server} from "websocket";
import {Client, Server} from "websocket";

Because WebSocket is a peer-to-peer protocol, the code for a client and a server is very similar. The primary difference is in the initial setup.

Connecting to a WebSocket Server

The $EXAMPLES/ch3-network/websocket-client example uses a WebSocket echo server, which replies to every message it receives by sending back that message. The WebSocket Client class constructor takes a configuration dictionary. The only required property is host, the name of the server. If no port property is specified, the WebSocket default of 80 is assumed.
let ws = new Client({
    host: "echo.websocket.org"
});

You can establish a secure connection using TLS by passing SecureSocket for the Socket property, as explained earlier in the section “Using TLS with the SecureSocket Class.”

You provide a callback function to receive messages from the WebSocket Client class. The WebSocket protocol is simpler than HTTP, so the callback is also simpler. In the websocket-client example, the connect and close messages just trace a message. The WebSocket protocol’s connection process consists of two steps: the connect message is received when the network connection is established between the client and server, and the handshake message is received when the client and server agree to communicate using WebSocket, indicating that the connection is ready for use.

When the example receives the handshake message, it sends the first message, a JSON string with count and toggle properties. When the echo server sends that JSON back, the callback in Listing 3-28 is invoked with the receive message. It parses the string back to JSON, modifies the count and toggle values, and sends the modified JSON back to the echo server. This process repeats indefinitely, with count increasing each time.
ws.callback = function(msg, value) {
    switch (msg) {
        case Client.connect:
            trace("connected ");
            break;
        case Client.handshake:
            trace("handshake success ");
            this.write(JSON.stringify({
                count: 1,
                toggle: true
            }));
            break;
        case Client.receive:
            trace(`received: ${value} `);
            value = JSON.parse(value);
            value.count += 1;
            value.toggle = !value.toggle;
            this.write(JSON.stringify(value));
            break;
        case Client.disconnect:
            trace("disconnected ");
            break;
    }
}

Listing 3-28.

Here’s the output of this code:
connected
handshake success
received: {"count":1,"toggle":true}
received: {"count":2,"toggle":false}
received: {"count":3,"toggle":true}
received: {"count":4,"toggle":false}
...
Each call to write sends one WebSocket message. You can send a message at any time after receiving the handshake message, not just from inside the callback:
ws.write("hello");
ws.write(Uint8Array.of(1, 2, 3).buffer);
Messages are either a string or an ArrayBuffer. When you receive a WebSocket message, it’s either a string or an ArrayBuffer depending on what was sent. Listing 3-29 shows how to check the type of value, the received message.
if (typeof value === "string")
    ...;    // a string
if (value instanceof ArrayBuffer)
    ...;    // an ArrayBuffer, binary data

Listing 3-29.

Creating a WebSocket Server

The $EXAMPLES/ch3-network/websocket-server example implements a WebSocket echo server (again, meaning that whenever the server receives a message, it sends back the same message). The WebSocket Server class is configured with a dictionary that has no required properties. The optional port property indicates the port to listen on for new connections; it defaults to 80.
let server = new Server;
The server callback function in Listing 3-30 receives the same messages as the client. In this example, all messages just trace status to the debug console, except for receive, which echoes back the received message.
server.callback = function(msg, value) {
    switch (msg) {
        case Server.connect:
            trace("connected ");
            break;
        case Server.handshake:
            trace("handshake success ");
            break;
        case Server.receive:
            trace(`received: ${value} `);
            this.write(value);
            break;
        case Server.disconnect:
            trace("closed ");
            break;
    }
}

Listing 3-30.

This server supports multiple simultaneous connections, each of which has a unique this value when the callback is invoked. If your application needs to maintain state across a connection, it can add properties to this. When a new connection is established, the connect message is received; when the connection ends, the disconnect message is received.

MQTT

The Message Queuing Telemetry Transport protocol, or MQTT, is a publish-and-subscribe protocol designed for use by lightweight IoT client devices. The server (sometimes called the “broker” in MQTT) is more complex, and consequently isn’t typically implemented on resource-constrained devices. Messages to and from an MQTT server are organized into topics. A particular server may support many topics, but a client receives only the messages for the topics it subscribes to.

The client for the MQTT protocol is implemented by the mqtt module :
import MQTT from "mqtt";

Connecting to an MQTT Server

The MQTT constructor is configured by a dictionary with three required parameters: the host property indicates the MQTT server to connect to, port is the port number to connect to, and id is a unique ID for this device. It’s an error for two devices with the same ID to connect to an MQTT server, so take care to ensure that these are truly unique. The $EXAMPLES/ch3-network/mqtt example excerpt in Listing 3-31 uses the device’s MAC address for the unique ID.
let mqtt = new MQTT({
    host: "test.mosquitto.org",
    port: 1883,
    id: "iot_" + Net.get("MAC")
});

Listing 3-31.

If the MQTT server requires authentication, the user and password properties are added to the configuration dictionary. The password is always binary data, so Listing 3-32 uses the ArrayBuffer.fromString static method to convert a string to an ArrayBuffer.
let mqtt = new MQTT({
    host: "test.mosquitto.org",
    port: 1883,
    id: "iot_" + Net.get("MAC"),
    user: "user name",
    password: ArrayBuffer.fromString("secret")
});

Listing 3-32.

To use an encrypted MQTT connection, use TLS as described earlier in the section “Securing Connections with TLS,” by adding a Socket property and optional secure property to the dictionary.

Some servers use the WebSocket protocol to transport MQTT data. If you’re using a server that does this, you need to specify the path property to tell the MQTT class the endpoint to connect to, as shown in Listing 3-33. Transporting MQTT over a WebSocket connection has no benefit and uses more memory and network bandwidth, so it should be used only if the remote server requires it.
let mqtt = new MQTT({
    host: "test.mosquitto.org",
    port: 8080,
    id: "iot_" + Net.get("MAC"),
    path: "/"
});

Listing 3-33.

The MQTT client has three callback functions (Listing 3-34). The onReady callback is invoked when a connection is successfully established to the server, onMessage when a message is received, and onClose when the connection is lost.
mqtt.onReady = function() {
    trace("connection established ");
}
mqtt.onMessage = function(topic, data) {
    trace("message received ");
}
mqtt.onClose = function() {
    trace("connection lost ");
}

Listing 3-34.

Once the onReady callback has been invoked, your MQTT client is ready to subscribe to message topics and publish messages.

Subscribing to a Topic

To subscribe to a topic, send the server the name of the topic to subscribe to. Your client can subscribe to multiple clients by calling subscribe more than once.
mqtt.subscribe("test/string");
mqtt.subscribe("test/binary");
mqtt.subscribe("test/json");
Messages are delivered to the onMessage callback function for all topics that your client has subscribed to. The topic argument is the name of the topic and the data argument is the complete message.
mqtt.onMessage = function(topic, data) {
    trace(`received message on topic "${topic}" `);
}
The data argument is always provided in binary form, as an ArrayBuffer. If you know the message is a string, you can convert it to a string; if you know the string is JSON, you can convert it to a JavaScript object.
data = String.fromArrayBuffer(data);
data = JSON.parse(data);

String.fromArrayBuffer is a feature of XS to make it easier for applications to work with binary data. There is a parallel ArrayBuffer.fromString function. These are not part of the JavaScript language standard.

Publishing to a Topic

To send a message to a topic, call publish with either a string or an ArrayBuffer:
mqtt.publish("test/string", "hello");
mqtt.publish("test/binary", Uint8Array.of(1, 2, 3).buffer);
To publish JSON, first convert it to a string:
mqtt.publish("test/json", JSON.stringify({
    message: "hello",
    version: 1
}));

SNTP

Simple Network Time Protocol, or SNTP , is a lightweight way to retrieve the current time. Your computer probably uses SNTP (or its parent, NTP) to set its time behind the scenes. Unlike your IoT device, your computer also has a real-time clock backed up by a battery, so it always knows the current time. If you need the current time on an IoT device, you need to retrieve it. If you’re using the command line method of connecting to Wi-Fi, the current time is retrieved once the Wi-Fi connection is established if you specify a time server on the command line.
> mcconfig -d -m -p esp ssid="my wi-fi" sntp="pool.ntp.org"
When connecting to Wi-Fi with code, you also need to write some code to set your IoT device’s clock. You get the current time with the SNTP protocol, which is implemented in the sntp module, and you set the device’s time using the time module.
import SNTP from "sntp";
import Time from "time";
Listing 3-35 shows the $EXAMPLES/ch3-network/sntp example requesting the current time from the time server at pool.ntp.org. When the time is received, the device’s time is set and displayed in UTC (Coordinated Universal Time) in the debug console. The SNTP instance closes itself to free the resources it’s using, since it’s no longer needed.
new SNTP({
        host: "pool.ntp.org"
    },
    function(msg, value) {
        if (SNTP.time !== msg)
            return;
        Time.set(value);
        trace("UTC time now: ",
              (new Date).toUTCString(), " ");
    }
);

Listing 3-35.

Most IoT products keep a list of several SNTP servers for situations where one is unavailable. The SNTP class supports this scenario without needing to create additional instances of the SNTP class. See the examples/network/sntp example in the Moddable SDK to learn how to use this fail-over feature.

Advanced Topics

This section introduces two advanced topics: how to turn your device into a private Wi-Fi base station and how to use JavaScript promises with networking APIs.

Creating a Wi-Fi Access Point

Sometimes you don’t want to connect your IoT product to the entire internet but you do want to let people connect to your device to configure it or check its status. At other times, you do want to connect your device to the internet but you don’t have the name and password for the Wi-Fi access point yet. In both these situations, creating a private Wi-Fi access point may be a solution. In addition to being a Wi-Fi client that connects to other access points, many IoT microcontrollers (including the ESP32 and ESP8266) can be an access point.

You can turn your IoT device into an access point with a call to the static accessPoint method of the WiFi class:
WiFi.accessPoint({
    ssid: "South Village"
});
The ssid property defines the name of the access point and is the only required property. As shown in Listing 3-36, optional properties enable you to set a password, select the Wi-Fi channel to use, and hide the access point from appearing in Wi-Fi scans.
WiFi.accessPoint({
    ssid: "South Village",
    password: "12345678",
    channel: 8,
    hidden: false
});

Listing 3-36.

A device is either an access point or the client of an access point. It cannot be both at the same time, so once you’ve entered access point mode, you cannot access the internet.

You can provide a web server on your access point, as shown earlier in the section “Responding to a Request.” In Listing 3-37, from the $EXAMPLES/ch3-network/accesspoint example, the import of the HTTP Server class is a little different because it renames, or aliases, the class to HTTPServer to avoid a name collision with the DNS server (introduced following this example).
import {Server as HTTPServer} from "http";
(new HTTPServer).callback = function(msg, value) {
    if (HTTPServer.prepareResponse === msg) {
        return {
            headers: ["Content-Type", "text/plain"],
            body: "hello"
        };
    }
}

Listing 3-37.

How will other devices know the address of your web server so that they can connect to it? You could claim a local name with mDNS. But since your IoT product is the access point, it’s also now the router for the network, so it can resolve DNS requests. This means that whenever a device on the network looks up a name, such as www.example.com, your application can direct the request to your HTTP server. Listing 3-38 is a simple DNS server that does exactly that.
import {Server as DNSServer} from "dns/server";
new DNSServer(function(msg, value) {
    if (DNSServer.resolve === msg)
        return Net.get("IP");
});

Listing 3-38.

The DNS Server class constructor takes a callback function as its sole parameter. The callback function is invoked with the resolve message whenever any device connected to the access point tries to resolve a DNS name. In response, the callback provides its own IP address. When most computers or phones connect to a new Wi-Fi point, they perform a check to see if they’re connected to the internet or if a login is required. When this check is performed on your access point, it will cause your web server’s access point to be called to get the web page to show. In this example, it will simply show hello, but you can change this to show device status, configure Wi-Fi, or anything else you like.

Promises and Asynchronous Functions

Promises are a feature of JavaScript to simplify programming with callback functions. Callback functions are simple and efficient, which is why they’re used in so many places. Promises can improve the readability of code that performs a sequence of steps using callback functions.

This section is not intended as a complete introduction to promises and asynchronous functions . If you aren’t familiar with these JavaScript features, read through this section to see if they look useful to your projects; if they do, many excellent resources are available on the Web to help you learn more.

The $EXAMPLES/ch3-network/http-get-with-promise example excerpt in Listing 3-39 builds on the HTTP Request class to implement a fetch function that returns a complete HTTP request as a string.
function fetch(host, path = "/") {
    return new Promise((resolve, reject) => {
        let request = new Request({host, path, response: String});
        request.callback = function(msg, value) {
            if (Request.responseComplete === msg)
                resolve(value);
            else if (Request.error === msg)
                reject(-1);
        }
    });
}

Listing 3-39.

The implementation of the fetch function is tricky, requiring an in-depth understanding of how promises work in JavaScript. But using the fetch function is easy (Listing 3-40).
function httpTrace(host, path) {
    fetch(host, path)
        .then(body => trace(body, " "))
        .catch(error => trace("http get failed "));
}

Listing 3-40.

Reading the code for httpTrace, you might imagine that the HTTP request happens synchronously, but that’s not the case, as all network operations are non-blocking. The arrow functions passed to the .then and .catch calls are executed when the request completes—.then if the call succeeds or .catch if it fails.

The recent versions of JavaScript provide another way to write this code: as an asynchronous function. Listing 3-41 shows the call to fetch rewritten in an asynchronous function. The code looks like ordinary JavaScript apart from the keywords async and await.
async function httpTrace(host, path) {
    try {
        let body = await fetch(host, path);
        trace(body, " ");
    }
    catch {
        trace("http get failed ");
    }
}

Listing 3-41.

The httpTrace function is asynchronous, so it returns immediately when called. The keyword await before the call to fetch tells the JavaScript language that when fetch returns a promise, execution of httpTrace should be suspended until the promise is ready (resolved or rejected).

Promises and asynchronous functions are powerful tools, and they’re used in JavaScript code for much more powerful systems, including web servers and computers. They’re available for your IoT projects, even on resource-constrained devices, because you’re using the XS JavaScript engine. Still, callback functions are preferred in most situations, because they require less code, execute faster, and use less memory. When building your project, you’ll need to decide whether the convenience of using them outweighs the additional resources used.

Conclusion

In this chapter you’ve learned various ways for your IoT device to communicate over a network. The different protocols described in this chapter all follow the same basic API pattern:
  • The protocol’s class provides a constructor that accepts a dictionary to configure the connection.

  • Callback functions deliver information from the network to your application.

  • Communication is always asynchronous to avoid blocking, an important consideration on IoT products that don’t always have the luxury of multiple threads of execution.

  • Callbacks can be turned into promises using small helper functions so that applications can use asynchronous functions in modern JavaScript.

You, as the developer of an IoT product, need to decide the communication methods it supports. There are many factors to consider. If you want your device to communicate with the cloud, HTTP, WebSocket, and MQTT are all possible choices, and they all support secure communication using TLS. For direct device-to-device communication, mDNS is a good starting point to enable devices to advertise their services, and HTTP is a lightweight way to exchange messages between devices.

Of course, your product doesn’t have to choose just one network protocol for communication. Starting from the examples in this chapter, you’re ready to try different protools to find what works best for the needs of your device.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.170.206