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.
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.
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
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.
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.)
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.
Listing 3-2.
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.
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.
- 1.
Open $EXAMPLES/ch3-network/wifi-code/main.js in your text editor.
- 2.
Change lines 4 and 5 so that ssid and password match your network credentials.
- 3.
Install the $EXAMPLES/ch3-network/wifi-code example on your device from the command line using mcconfig.
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.
Listing 3-3.
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.
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
Getting Network Information
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
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.
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
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.
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
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
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.
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.
Listing 3-9.
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.
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.
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.
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.
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
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.
Listing 3-16.
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
Listing 3-18.
Listing 3-19.
Creating an HTTP Server
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
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
Listing 3-20.
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.
Listing 3-21.
Since this example doesn’t pass a dictionary to the Server constructor, the default of port 80 is used.
Receiving a Streaming Request
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.
Sending a Streaming Response
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.
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.
Listing 3-24.
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.
Listing 3-25.
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
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.
Listing 3-26.
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.
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
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.
Listing 3-28.
Listing 3-29.
Creating a WebSocket Server
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.
Connecting to an MQTT Server
Listing 3-31.
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.
Listing 3-33.
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
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
SNTP
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.
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.
Listing 3-37.
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.
Listing 3-39.
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.
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
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.