Friday, February 2, 2018

Building a Software FM Receiver with an RTL-SDR and GNU Radio

A Short Practical Introduction to Software Defined Radio



Back in the 2012, the guys from the Osmocom team discovered how to hack a $20 DVB-TV dongle into a wide band radio receiver. Running on a Realtek RTL2832U chip and commonly called an RTL-SDR, this dongle is currently the cheapest Software Defined Radio receiver in the market. For a fraction of the cost of dedicated radio hardware or high-end SDR receivers, one can monitor law enforcement/emergency/air traffic communications, track aircraft and ship positions, download satellite data, analyze mobile phone traffic, reverse engineer data signals and explore other interesting areas of analog and digital wireless communications. 


RTL-SDR Dongle (Photo from rtl-sdr.com)

RTL-SDR is supported by a huge online community with hundreds of free software and tutorials for various radio applications. In this post, we take a few steps back and explore the fundamentals of Software Defined Radio and how it works by building an FM broadcast receiver with an RTL-SDR dongle and GNURadio.

GNU Radio is an open source toolkit for developing signal-processing and software-based radio applications. It works with various radio transmitter and receiver hardware, and also has a simulation mode. Widely used for wireless communication research and applications development, it has libraries for C++ and Python, as well as a visual programming environment, GNU radio companion, which we will use for the following examples.

Radio Theory in a Nutshell

Radio signals are electromagnetic waves that propagate at a frequency between 3 kilohertz to 300 gigahertz. They are naturally generated by atmospheric and astronomical conditions. Artificially generated radio signals are used for voice, video and data communications over a wireless medium.

Any radio system has a transmitter that sends a signal, and a receiver that receives it.



Radio Transmitters

A radio transmitter has an oscillator that rapidly changes the current in a circuit between two peak voltage points generating a continuous wave. This current is connected to an antenna and transmitted into the air as an electromagnetic wave called a carrier signal. A signal source, typically either sound, video or data, is encoded into the carrier signal through a modulator circuit.




How often the current changes over time is referred as the frequency and amplitude refers to the peak voltage values of a wave. A carrier signal oscillates at a consistent frequency and amplitude mathematically described as a sine wave.



A signal source also demonstrates wave behavior. However, unlike the carrier wave, it has a variable frequency and amplitude. Below is an actual sound wave as seen in Audacity.




Modulation is the process of encoding a signal source into a carrier signal using either of the two following methods:

  • Amplitude Modulation: The source signal is encoded by varying the amplitude of the carrier signal.
  • Frequency Modulation: The source signal is encoded by varying the frequency of the carrier signal.





The Radio Frequency Spectrum

We can describe a radio signal as a carrier signal encoded with a source signal, and transmitted via an antenna as an electromagnetic wave with a frequency between 3 khz and 300 ghz. Radio signals exhibit different behaviors depending on its frequency (e.g. lower frequencies propagate farther and higher frequencies can penetrate physical obstacles). A specific range of frequencies are chosen for a particular application based on these behaviors.  Below is a quick table of various applications in the radio frequency spectrum.




Radio Receivers

It is also useful to view frequency as a virtual location in the radio spectrum that separates one signal from another. Transmitters propagate at different frequencies from each other.  A common example are the various broadcast radio station frequencies available on on a car stereo.

A receiver antenna picks up several radio signals from broad range of frequencies depending on its design. The tuner circuit in a radio receiver is essentially a filter that amplifies a specific frequency from the antenna and ignores all the others. After a signal has been selected, a demodulation circuit separates the source from the carrier signal. The source signal is processed depending on the type: audio signals are amplified and sent to a speaker, videos are decoded and displayed, and data is processed into information.



Radio devices can either be unidirectional and can only transmit (e.g. broadcast radio or TV transmitters), or receive (e.g car radio, GPS receiver) signals, or bidirectional and can both transmit and receive signals (e.g. two-way radio, Wifi interface)

In traditional hardware-based radio systems, each step in the above processed is handled by a dedicated set of hardware components. Everything is hard-wired and designed to operate for a specific application. Each radio device can only operate within a limited range of the radio spectrum and process a specific type of source signal.  A car radio for example can only receive the broadcast FM band between 88 - 108 megahertz and process audio.

Software-Defined Radio and the RTL-SDR Dongle

The heart of Software Defined Radio (SDR) devices are Analog-to-Digital Converters that convert raw radio signals into digital data and tuner chips that operate on a wider range of frequencies.  Although it's necessary to use specific antennas and hardware-based amplifiers and filters to work with some signal types, most of the heavy-lifting is done using software, either by programming the hardware chips, or processing the resulting data on a computer. This allows a single device for various application making it a cheaper and more flexible alternative to pure hardware-based systems.

The RTL-SDR dongle was originally designed to convert analog radio signals to digital data  for Digital TV and FM Radio broadcast.  With a modified hardware driver, it can effectively receive signals between 25 megahertz to 1.6 gigahertz.  It has an 8-Bit sampling rate, which is relatively low compared to more expensive SDR hardware, but decent enough to be useful for a variety of applications.
  
Building A Software Radio

This post does not cover installation and configuration of the RTL-SDR driver and GNU Radio for your respective platform.  There are several helpful tutorials on the web so this assumes that the requisite driver have been installed and GNU Radio with GrOsmoSDR is up and running. To work on the examples in this post, I used a Noolec Nano3 dongle with GNU Radio Companion 3.7.9. Now that we have that out of the way, let's open GNU Radio companion and build our receiver.

Applications in GNU Radio Companion are built using blocks in a flowgraph. Each block represents a signal processing component. Blocks are chosen from a list on the right side of the interface and drag into the workspace and connected together.

Clicking on File->New brings a menu of options for the type of GUI to use for a new flowgraph. Pick QT GUI for our radio application.




By default, each flowgraph has an Options block where global parameters are set. Notice that our new flowgraph has an Options block with ID set as top_block and Generate Options set to QT GUI that we selected earlier. Also notice that there is a Variable block with a default samp_rate set to 32000. As the name suggests, Variable blocks define variables that are used to set parameters of other blocks in the flowgraph.

There are three general types of blocks:
  • Source blocks inputs data into the application. (e.g. File Source blocks which inputs data from a file)
  • Sink blocks outputs data or a signal from the the application. (e.g. Audio Sink block writes an audio output as a .wav file)
  • Processing blocks manipulate data between source and sink blocks
At a minimum, it requires one source block and one sink block for a flowgraph to run. With these two block types we can already start building our radio receiver by connecting to the RTL-SDR dongle, capturing a range of radio signals and converting it to digital data.

Capturing Raw Radio Signals

Let's begin by searching for the RTL-SDR Source block and dragging it to the workspace.




The RTL-SDR Source block connects our flowgraph to the RTL-SDR dongle, tunes it to a configured frequency, captures a portion of the radio spectrum and brings it to the flowgraph as digital data.  The captured portion is within a range of frequencies defined by a configured sample rate, The range of frequencies, or frequency band,  is centered around the frequency where the dongle is tuned, and we refer to this as the center frequency.  In this case we are capturing a portion of the spectrum with a 1 Mhz bandwidth centered at the frequency of 96 Mhz. 

  • First we change the value of the samp_rate variable by double-clicking on the Variable block. Python expressions are valid parameter values in GNU Radio, so we enter 1e6 as the samp_rate
  • Now we can configure the RTL-SDR Source block, setting the Sample Rate to samp_rate and the Frequency to 96e3. With these parameter values we are now capturing a portion of the radio spectrum between 95.5 Mhz to 96.5 Mhz with 96 Mhz as the center frequency.
  • We can examine our sampled signal by adding a QT GUI Sink block which gives us several instruments for visualizing signals. 
To run the flowgraph click on the play button on top of the interface:


The Frequency Display tab shows the strength of various signals (and noise) within the captured frequency band. Radio signals would typically have a peak strength on the display.



Noticed that the frequency range is centered at 0 and not on the 95 mhz center frequency. Contrary to what we are likely to expect, raw digital samples of radio signals don't contain frequency data. The digital sample of any given frequency band is represented as complex numbers with 0 Mhz as the center frequency. (There is some mathematics behind this that I am completely not qualified to explain). To visualize the frequencies, we need to set a relative reference to the center frequency of the captured frequency band.
  • Create a center_frequency Variable block since we need this value in several instances across the flowgraph. 
  • Edit the RTL-SDR Source and change the Frequency parameter to center_frequency
  • Set the reference point of the QT GUI block to reflect the captured frequency band by changing the center frequency parameter to center_frequency.
When we run the flowgraph, we get a visualization that reflects the actual frequency of the captured band. Keep in mind that the center frequency of visualization instruments should be consistent with the sampled frequency band from the hardware source. Unless you just want to observe the behavior of raw signals without frequency reference points. 

Tuning to a Signal

We are now capturing a portion of the radio spectrum as digital data. Since this is part of the FM Broadcast band, there should be at least one station transmitting within these range of frequencies. These signals are usually strong and appear as peaks in our Frequency Display.

The next step is to pick, or tune to, a specific broadcast station. This is done by shifting the frequency band and placing the station signal at the center and eliminating all the other signals.

  • Create a QT GUI Entry block for the frequency of target signal. This block creates a text entry form for setting variable values. Enter target_frequency as ID. To test the flowgraph, pick a local radio station broadcasting between 95.5 to 96.5 mhz and enter the frequency as the default value. In this case it's 96.3 Mhz

  • Create a Multiply block and connect the input to the output of the RTL-SDR Source. This block takes the sampled frequency band as input and combines it with a sine wave signal. The output is a frequency band with a center frequency shifted based on the frequency of the sine wave. 
  • Create a Signal Source block to generate the sine wave signal. Edit the block, choose Sine as the Waveform, and enter center_frequency  - target_frequency as the Frequency parameter. 
  • The difference between the center frequency and the target frequency is equal to the sine wave frequency required to shift the target frequency to the center as shown in the example below. 


  • Connect the Signal Source output to the Multiply block input.


  • Add another QT GUI Sink block. To view the shifted frequency band change the Frequency parameter to target_frequency.



  • Connect the GUI Sink block it to the output of the Multiply block. Run the flowgraph and observe how the frequency range has shifted.
Filtering

We isolate the target signal at the center by eliminating the rest of the frequency range with a Low Pass Filter block.  As the name suggests, this block filters out any signal above a cutoff frequency starting from the center commonly called the channel width. The filters transition width is the point along the channel width when filtering starts to 'roll-off'.

  • Create the following Variable blocks for the filter parameters: channel_width with a default value of 150e3, transition_width with a default value of 10e3 and a working_samp_rate of  400e3.
  • At this point in the flowgraph we are already dealing with our target signal which is only a segment of our captured frequency band. It's no longer necessary to have a high sample rate to work with this data, so we reduce it to lower rate defined in working_samp_rate. The process of reducing the sample rate is called decimation.
  • We create a Low Pass Filter block and set the Cutoff Frequency to channel_width, the Transition Width to transition_width and Decimation to int(samp_rate/working_sample_rate).
  • We connect the output of the Multiply block to the Low Pass Filter block to isolate the target frequency signal and reduce the sample rate. 
Connect another QT GUI Sink to the output of the Low Pass Filter. Run the flowgraph and scroll to the top Frequency Display to see the filtered signal.


Demodulation

The last step is to extract the audio source signal and send it to the computer's audio hardware. 
  • Create a WBFM Reciever block and connect the input to the output of the Low Pass Filter. This block separates the source signal from the carrier signal. 
  • Set the Quadrature Rate, which is the sample rate of the input signal, to working_samp_rate
We finally have an audio signal that we can send to the audio hardware. However, a typical sound card runs at a lower sample rate than the 400k output of the WBFM Reciever. The Rational Resampler block changes the sample rate of a signal. It has an Interpolation parameter that is divided with a Decimation parameter. The result of which is the factor by which a sample rate is changed. An Interpolation value that's lower than the Decimation value will result in a slower sample rate (e.g. decimation of 2 and interpolation of 1 will be 1/2x slower). An Interpolation value that's higher than the Decimation value will result in a slower sample rate (e.g. decimation of 1 and interpolation of 2 will be 2x slower).
  • Create a Rational Resampler block and set the type to Float. The WBFM Reciever outputs floating point values to represent the audio signal. Set the Interpolation to 48k and the Decimation to int(working_sample_rate) changing the data type of the variable to integer.
  • Connect the output of the WBFM Reciever to the input of the Rational Resampler.
  • Create an Audio Sink block and set the Sample Rate to 48k.
Our final flowgraph should look like this:


Run the flowgraph. You should hear a broadcast radio station if you are tuned to the correct frequency. To change the frequency, just enter a value in the entry form above the Frequency Display. There is no audio processing post-modulation so the volume is rather low. You might need to wear earphones to hear the audio.

We've hardly scratched the tip of the Software Defined Radio iceberg, but this exercise has gone through the basic steps common to all radio reciever applications. SDR provides an inexpensive platform for learning signal processing, radio theory and prototyping both hardware and software based radio systems. Hopefully, this will lead you to further explore the potential of this technology.


Again, comments, questions and corrections are always welcome.



Thursday, November 9, 2017

Connecting an Arduino to the Internet

Building Simple Network Devices



At a very basic level, the Internet of Things are simply sensors, actuators and computers connected through a network. In this eco-system, computers either gather real-word data through sensors, and/or alter the real-world by sending instructions to actuators. Regardless of complexity, IoT systems can often be reduced to these three common denominators.

Sensors and actuators are essentially network embedded devices--single-purpose computing systems connected to a network. These devices have hardware and operating systems optimized to run a single application. In this post we will look into the basic elements of a network embedded device by building a simple sensor and actuator device using an Arduino Uno and Ethernet Shield. Although things are becoming predominantly wireless, we are using the Ethernet to illustrate the simple basic building blocks. Well actually, I just found an Ethernet Shield that I can't recall ordering and decided to just use it for a blog post.

Connecting the Dots

A network embedded device can be broken down into the following parts:
  • Microcontroller: a small computer with a single integrated circuit that runs the device application software.
  • Network Interface: Connects the device to the network so it can communicate with computers or other embedded devices
  • Application Component: In an IoT context, this is either a sensor that provides real-word data or an actuator that executes real-world action.
In some cases, both the the microntroller and the network interface are integrated on a single board. In this post, we will use an Arduino as the microcontroller and the Ethernet shield as the network interface. We will go through two examples with different application components: a DTH11 Humidity and Temperature Sensor for a sensor server device and standard 16X2 LCD screen for the client actuator device.

Arduino Ethernet Shield

The Ethernet shield  connects to the Arduino Uno using the Serial Peripheral Interface (SPI), a synchronous serial communication protocol commonly used to connect microcontrollers with other peripheral components. It follows a Slave/Master model as shown below:


The Master component  (i.e. the microcontroller) sends a timing signal through a Clock (SCK) line to synchronize communication between two separate data lines. Depending on the clock cycle,  Slave components either receive data from the Master through a Master Out Slave In (MOSI) line, or send out data to the Master through a Master In Slave Out (MISO) line. The master can control several slave modules through multiple Slave Select (SSL) lines.  To activate a Slave component the SSL line connected to that device is set to high, and deactivated by setting it to low. Only one slave device is activated at any given point.

On the Arduino Uno, the SPI lines are as follows:

- SCK - Pin 13
- MISO - Pin 12
- MOSI - Pin 11

We will use the Arduino as a Master component and connect the Ethernet shield as a Slave. If set as the Master component, any digital pin set to output on the Arduino can be used as an SS line. However, Slave devices have dedicated SS lines. The Ethernet shield also uses  Pins 13, 12 and 11 as the SCK, MISO and MOSI lines respectively. It uses Pin 10 as the SS line to the Ethernet interface, and Pin 4 as the SS line to the SD card interface (which we won't be using in our examples.) The illustration below shows how the Arduino and the Ethernet shield are connected:


Check the, the Arduino website for information on the Ethernet. 

We will use this shield to build simple network devices that communicates via HTTP, the ubiquitous protocol that connects the World Wide Web. Attach the shield to the Arduino and connect it to a hub or router with an active connection to the Internet.

Network Sensor Device

Our first device is a web server that sends the temperature and humidity reading from a DTH11 sensor to web page. Data from the DHT11 is collected by the Arduino Uno that connects to the Internet via the Ethernet shield.

DTH11 Sensor Pins


The DHT11 sensor has 4 pins, the first is the power supply (VCC) pin that works between 3.5 to 5.5 volts, the second is the data pin, and the fourth and last is the Ground pin. The third pin is an empty pin and I don't really know why it's even there. If you are using a DHT11 on a breakout board it will only have three pins so you don't need to bother with the third pin.


Assuming the the Ethernet shield is attached to the Arduino, connect the DHT11 as follows:
  • 5v power pin on the Ethernet shield to the first pin on the DHT11.
  • Any Ground pin on the Ethernet shield to the last pin on the DHT11.
  • Pin 9 on the Ethernet shield to the second pin of the DHT11.
The data pin communicates using single-wire bi-directional serial protocol. Instead of a separate clock line, it uses one line for the synchronization signal and the data transmission as follows:
  • The Arduino sends a signal to the data pin and sets the sensor into a high-power state.
  • The DHT11 waits for the signal to end, and sends the humidity and temperature data.
  • It returns to a low-power state and waits for the next signal from the Arduino.

Upload the following code to the Arduino:

temp_humidity_sensor.ino
 [code]  
 /*  
 Basic network embedded device webserver that sends the output of a  
 DHT11 Temperature and Humidity Sensor on HTTP request.   
 */  
   
 #include <SimpleDHT.h>  
 #include <SPI.h>  
 #include <Ethernet.h>  
   
 // Enter a MAC address for your controller below.  
 // Newer Ethernet shields have a MAC address printed on a sticker on the shield  
 byte mac[] = {  
  0x00, 0xAA, 0xBB, 0xCC, 0xDE, 0x02  
 };  
   
 //DHT11 Input Pin  
 int pinDHT11 = 9;  
   
 //Initialize DHT11 Sensor  
 SimpleDHT11 dht11;  
   
 //initialize variable for DHT11 sensor  
 byte temperature = 0;  
 byte humidity = 0;  
 byte data[40] = {0};  
   
   
 // Initialize the a server that listens for a request  
 // on port 80  
 EthernetServer server(80);  
   
 void setup() {  
    
  // Open serial connection  
  Serial.begin(9600);  
     
   // Establish Ethernet connection:  
  if (Ethernet.begin(mac) == 0) {  
   // if can't connect print error msg and loop foever:  
   Serial.println("Failed to configure Ethernet using DHCP");  
   for (;;)  
   ;  
  }  
  // print your local IP address:  
  printIPAddress();  
   
  //start server  
  server.begin();  
      
 }  
   
 void loop() {  
  //reconnect when network is disconnected  
  networkReconnect();  
   
  //read DHT11 Data  
  if (dht11.read(pinDHT11, &temperature, &humidity, data)) {  
   Serial.println("Read DHT11 failed");  
   return;  
   }  
  else {  
   
    Serial.println("");  
    Serial.print("Sample OK: ");  
    Serial.print((int)temperature);   
    Serial.print(" *C, ");  
    Serial.print((int)humidity);   
    Serial.println(" %");    
   }  
   
  // listen for incoming clients  
  EthernetClient client = server.available();   
  if (client) {  
   
    // notify serial each server request  
    Serial.println("client connection");  
   
    // length of the incoming text line  
    int lineLength = 0;   
   
    //run while client is connected  
    while (client.connected()) {  
   
     //check if a request has been made  
     if (client.available()) {  
   
      // read one character from the request data stream    
      char thisChar = client.read();  
   
        
      // Check for end of header (blank line)  
      if (thisChar == '\n' && lineLength < 1) {  
        // send a standard http response header  
        makeResponse(client);  
        break;  
        }  
       
      // check for end of the line and reset linelength 0 ,  
      if (thisChar == '\n' || thisChar == '\r') {  
       lineLength = 0;  
      }  
        
      else {  
       // for any other character, increment the line length:  
       lineLength++;  
       }  
       Serial.write(thisChar);  
      }  
     }  
     
   // close the connection:  
   client.stop();  
   
  }  
   
  // DHT11 sampling rate is 1HZ.  
  delay(1000);  
 }  
   
 void networkReconnect() {  
   
   //reconnect if ethernet is disconnected  
  switch (Ethernet.maintain())  
  {  
   case 1:  
    //renewed fail  
    Serial.println("Error: renewed fail");  
    break;  
   
   case 2:  
    //renewed success  
    Serial.println("Renewed success");  
   
    //print your local IP address:  
    printIPAddress();  
    break;  
   
   case 3:  
    //rebind fail  
    Serial.println("Error: rebind fail");  
    break;  
   
   case 4:  
    //rebind success  
    Serial.println("Rebind success");  
   
    //print your local IP address:  
    printIPAddress();  
    break;  
   
   default:  
    //nothing happened  
    break;  
   
  }  
 }  
   
 // function for printing local IP address to serial  
 void printIPAddress()  
 {  
  Serial.print("My IP address: ");  
  for (byte thisByte = 0; thisByte < 4; thisByte++) {  
   // print the value of each byte of the IP address:  
   Serial.print(Ethernet.localIP()[thisByte], DEC);  
   Serial.print(".");  
  }  
   
  Serial.println();  
 }  
   
 // function for server response  
 void makeResponse(EthernetClient thisClient) {  
    
  thisClient.print("HTTP/1.1 200 OK\n");  
  thisClient.print("Content-Type: text/html\n\n");  
  thisClient.println("<html><head></head><body>\n");  
  thisClient.print("<h1>Hello! I'm an Arduino Web Server!</h1><br>");  
  thisClient.print("<b>Current Room Temperature:</b> ");  
  thisClient.print((int)temperature);  
  thisClient.print(" *C<br>");  
  thisClient.print("<b>Current Room Humidity:</b> ");  
  thisClient.print((int)humidity);  
  thisClient.print(" %<br>");   
  thisClient.println("</body></html>\n");  
  }  
   
 [/code]  

There are several Arduino libraries for the DHT11 and simpleDHT is used for this example. Follow this link for installation instructions.

We include the SPI and Ethernet libraries to access the networking functions of the Ethernet shield, and simpleDHT libary to access the sensor. We declare global variables starting with the MAC address of the shield. This address is typically printed on the shield, but an arbitrary address can also be used as shown in the code. We assign pin 9 as our data pin, and create additional variables for the humidity and sensor readings, as well as the raw 40-bit data sent by the sensor.

Managing network connection and client requests

  • We instantiate an EthernetServer object to handle client requests, setting the port number parameter to 80 (The port number actually defaults to 80 unless it's explicitly set).  We start the program by opening a serial connection for sending debug information to the Serial Monitor.
  • An Ethernet connection is initiated by passing the MAC address to the Ethernet.begin() method. It will establish a connection through the hub or router and request for an I.P address via DHCP.  It returns 0 if the if it cannot establish a connection. This is handled by sending an error message to the the Serial Monitor and suspending the program through an endless loop.  Otherwise, if a network connection is established it calls the printIPAddress() function and sends the device I.P. address to the Serial Monitor. This function obtains the local I.P address via Ethernet.localIP() method,
  • The assigned I.P address is leased to the device for a specified amount of time. It's necessary to consistently check, and renew, the address if it has expired. This is done at the start of the program loop by calling the networkReconnect() function. It uses Ethernet.maintain() method to renew the I.P address.  The method returns the following byte that the function handles accordingly with a switch statement:
    • 0: nothing happened
    • 1: renew failed
    • 2: renew success
    • 3: rebind fail
    • 4: rebind success
  • We start listening for client requests by calling server.begin() and checking for a client connection with server.available(), It returns an object representing the client that is connected to the server.  The client.connected() method checks if the client connection is still active. While the client is connected, we check for incoming data from the client, which can then be read per byte with the client.read() method.
  • Each time a web browser request is made, it sends an HTTP header that contains the request parameters and and ends with an empty line. The server response is based on the values of the request parameters. In this simple example, we just send the server response after the end of the header (blank line) has been read.
  • The makeResponse() function takes a Client object and sends an HTTP response using the print() and println(). It starts the response with a header that ends with a blank line, followed by the HTML content of the response, which contains the values read from the DTH11 sensor, which we will discuss next.

Reading the Sensor

The DHT11 library has a read() method that takes three parameters: the pin where the data line is connected, a variable for the temperature reading, a variable for the humidity reading, and a last variable for the 40-bit raw data. We won't be using the raw data but we still assign a variable just to be consistent when passing parameters to the read() method. The temperature and humidity values are stored in assigned variables with the same names, and are are sent with of the server response in the makeResponse() function.

Plug the Ethernet shield to a a router or hub once the the circuit has been assembled and the code uploaded to the Arduino. Open the Serial Monitor to check the debug messages.  If all went well, the Serial Monitor will display the local I.P. address of your device. Go to the address using a web browser and the current temperature and humidity will be displayed.  The code can be modified to respond with JSON or other data format instead of HTML.
Click to Zoom



Network Actuator Device


The next example is a client device that retrieves the current local time from an API provided by timzonedb.com, and displays it on 16x2 LCD screen.  You need to register an account in timezonedb.com to access the API. Take note of the API key of your registered account. 

We will use a simple monochrome alphanumeric display that runs on a Hitachi HD44780 controller. It has parallel data and control interface as follows:
  • 8 Data pins (D0 -D7)
  • A Register Select (RS) pin to switch between sending either data for display or instruction sets.
  • A Read/Write (R/W) pin to switch to either reading mode or writing mode
  • An Enable pin to enable/disable writing to the either the display or instruction registers
  • There is also +5V power supply (VCC) pin with a respective Ground pin, another pin to control the display contrast (VDD), and two pins to the turn on/off the LCD backlight.



Again, assuming that the Ethernet Shield is attached to the Arduino, connect the LCD display to the shield as follows:


  • LCD RS pin digital to Arduino pin 12
  • LCD Enable pin to Arduino digital pin 11
  • LCD 4 pin to Arduino digital pin 5
  • LCD 5 pin to Arduino digital pin 4
  • LCD 6 pin to Arduino digital pin 3
  • LCD 7 pin to Arduino digital pin 2
  • Connect the potentiometer to the breadboard power and ground rail and the middle wiper pin to the LCD VDD pin to control the contrast.
  • Turn on the backlight by connecting the LCD positive LED pin to the power rail with a 220 ohm pull-up resistor and the LCD negative LED pin to the ground rail.

Upload the following Sketches code on to Arduino:

displayLocalTime.ino
 // Simple Network Embedded Device  
 // Extracts date and time from timezonedb.com  
 // and displays on LCD screen  
   
 #include <ArduinoJson.h> //JSON Library  
 #include <Ethernet.h> //Ethernet Library  
 #include <SPI.h> //SPI Library  
 #include <LiquidCrystal.h> // Liquid Crystal Library  
   
 // initialize the LCD library with RS pin on 8, E pin on 9, and pin 5,4,3,2 as output  
 LiquidCrystal lcd(9, 8, 5, 4, 3, 2);  
   
 // initialize http client  
 EthernetClient client;  
   
 const char* server = "api.timezonedb.com"; // API server address  
 // API endpoint  
 const char* resource = "/v2/get-time-zone?key=G09GM1DXFAH7&format=json&by=zone&zone=Asia/Manila";   
 const unsigned long BAUD_RATE = 9600;   // serial connection speed  
 const unsigned long HTTP_TIMEOUT = 10000; // max respone time from server  
 const size_t MAX_CONTENT_SIZE = 512;    // max size of the HTTP response  
   
 // data type to extract from endpoint  
 struct LocalTime {  
  char country[32];  
  char time[32];  
 };  
   
 //Initialize program  
 void setup() {  
    
  //initialize serial library  
  initSerial();  
   
  //initialize Ethernet library  
  initEthernet();  
     
  // set up the LCD's number of columns and rows:  
  lcd.begin(16, 2);  
    
 }  
   
 void loop() {  
  //reconnect if Ethernet is disconnected  
  networkReconnect();  
     
  if (connect(server)) {  
   if (sendRequest(server,resource) && skipResponseHeaders()) {  
    LocalTime localTime;  
    if (readReponseContent(&localTime)) {  
     printUserData(&localTime);  
     printLCD(&localTime);  
    }  
   }  
  }  
   
  //disconnect from server  
  disconnect();  
   
  //wait before next reconnection  
  wait();  
 }  
   
 // Initialize Serial port  
 void initSerial() {  
  Serial.begin(BAUD_RATE);  
  while (!Serial) {  
   ; // wait for serial port to initialize  
  }  
  Serial.println("Serial ready");  
 }  
   
 // Initialize Ethernet library  
 void initEthernet() {  
  byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};  
  if (!Ethernet.begin(mac)) {  
   Serial.println("Failed to configure Ethernet");  
   return;  
  }  
  Serial.println("Ethernet ready");  
  printIPAddress();  
    
  delay(1000);  
 }  
   
 // Print local IP address to serial  
 void printIPAddress()  
 {  
  Serial.print("IP address: ");  
  for (byte thisByte = 0; thisByte < 4; thisByte++) {  
   // print the value of each byte of the IP address:  
   Serial.print(Ethernet.localIP()[thisByte], DEC);  
   Serial.print(".");  
  }  
   
  Serial.println();  
 }  
   
   
 //reconnect if ethernet is disconnected  
 void networkReconnect() {  
   
  switch (Ethernet.maintain())  
  {  
   case 1:  
    //renewed fail  
    Serial.println("Error: renewed fail");  
    break;  
   
   case 2:  
    //renewed success  
    Serial.println("Renewed success");  
   
    //print your local IP address:  
    printIPAddress();  
    break;  
   
   case 3:  
    //rebind fail  
    Serial.println("Error: rebind fail");  
    break;  
   
   case 4:  
    //rebind success  
    Serial.println("Rebind success");  
   
    //print your local IP address:  
    printIPAddress();  
    break;  
   
   default:  
    //nothing happened  
    break;  
   
  }  
 }  
   
 // Open connection to the HTTP server  
 bool connect(const char* hostName) {  
  Serial.print("Connect to ");  
  Serial.println(hostName);  
   
  bool ok = client.connect(hostName, 80);  
   
  Serial.println(ok ? "Connected" : "Connection Failed!");  
  return ok;  
 }  
   
 // Send the HTTP GET request to the server  
 bool sendRequest(const char* host, const char* resource) {  
  Serial.print("GET ");  
  Serial.println(resource);  
   
  client.print("GET ");  
  client.print(resource);  
  client.println(" HTTP/1.0");  
  client.print("Host: ");  
  client.println(host);  
  client.println("Connection: close");  
  client.println();  
   
  return true;  
 }  
   
 // Strip HTTP headers  
 bool skipResponseHeaders() {  
  // HTTP headers end with an empty line  
  char endOfHeaders[] = "\r\n\r\n";  
   
  client.setTimeout(HTTP_TIMEOUT);  
  bool ok = client.find(endOfHeaders);  
   
  if (!ok) {  
   Serial.println("No response or invalid response!");  
  }  
   
  return ok;  
 }  
   
 // Parse the JSON from the input string and extract the relevant values  
 // Here is the JSON we need to parse  
 bool readReponseContent(struct LocalTime* localTime) {  
  // Compute optimal size of the JSON buffer according to what we need to parse.  
  // See https://bblanchon.github.io/ArduinoJson/assistant/  
  const size_t BUFFER_SIZE = JSON_OBJECT_SIZE(13) + 215;  
   
  // Allocate a temporary memory pool  
  DynamicJsonBuffer jsonBuffer(BUFFER_SIZE);  
   
  JsonObject& root = jsonBuffer.parseObject(client);  
   
  if (!root.success()) {  
   Serial.println("JSON parsing failed!");  
   return false;  
  }  
   
  // Here were copy the strings we're interested in  
  strcpy(localTime->country, root["countryName"]);  
  strcpy(localTime->time, root["formatted"]);  
    
  return true;  
 }  
   
 // Print the data extracted from the JSON  
 void printUserData(const struct LocalTime* localTime) {  
  Serial.print("Country = ");  
  Serial.println(localTime->country);  
  Serial.print("Time = ");  
  Serial.println(localTime->time);  
 }  
   
 // Output to LCD  
 void printLCD(const struct LocalTime* localTime) {  
  lcd.print(localTime->country);  
  lcd.setCursor(0, 1);  
  lcd.print(localTime->time);  
 }  
   
   
 // Close the connection with the HTTP server  
 void disconnect() {  
  Serial.println("Disconnect");  
  client.stop();  
 }  
   
 // Pause   
 void wait() {  
  Serial.println("Wait 30 seconds");  
  delay(30000);  
 }  

Most of the code for managing the Ethernet and Serial interface is reused from the previous example. We also include the ArduinoJSON library by Bennot Blanchon for parsing the JSON response from API and the LiquidCrystal library which manages the RS, Enable and data pins of the the LCD display. The VDD and back light pins on the LCD pins are set via analog inputs directly from the breadboard.

We declare constants  for the API server address, the API endpoint (use your API key as value for 'key'), baud rate for the serial connection. 


Making an HTTP Request


  • We instantiate an EthernetClient object to handle HTTP requests.
  • We initiate Serial and Ethernet connections, using initSerial() and initEthernet() functions respectively. Both functions reused the code from the previous example. 
  • The main loop starts by by calling networkReconnect() to check the lease status of the DHCP assigned I.P. address. Then we attempt to establish a client connection by passing the server address to  the connect() function, which accepts a valid host name. It uses the connect() method of the EthernetClient class to make a connection. The method returns True if the connection was successfully established.
  • If a connection has been established, an API request is sent by passing the server and endpoint string to sendRequest(), and validating it with skipHeaderResponse(). sendRequest() sends a GET request using the print and println methods of the Ethernet Client class. skipHeaderResponse() validates the response by finding the two blank lines that indicates the end of a HTTP header.
  • If the server returns a valid HTTP response we parse the JSON content by calling readResponseContent() and passing it a pointer to LocalTime.

Parsing JSON

The ArduinoJSON library preallocates memory for the JSON object. It supports allocation of a fixed memory pool using the StaticJsonBuffer class, or dynamic memory allocation via the DynamicJsonBuffer class. ArduinoJSON Assistant is a browser-based utility for determining the amount of memory needed for a given JSON object tree.  It takes a JSON  input and returns the expression for computing the buffer size. It also gives a code snippet for a parsing program. Refer to the documentation page for a more details on the memory model.


Here's the JSON response from the API end-point: 


http://api.timezonedb.com/v2/get-time-zone?key=G09GM1DXFAH7&format=json&by=zone&zone=Asia/Manila



{
"status":"OK",
"message":"",
"countryCode":"PH",
"countryName":"Philippines",
"zoneName":"Asia\/Manila",
"abbreviation":"+08",
"gmtOffset":28800,
"dst":"0",
"dstStart":275151600,
"dstEnd":0,
"nextAbbreviation":"",
"timestamp":1510066974,
"formatted":"2017-11-07 15:02:54"

}

Which gives us the following expression in ArduinoJSON Assistant:





Parsing of the JSON content from the API response is handled by the readResponseContent() function:

  • The expression returned by ArduinoJSON Assistant is declared as the value of the BUFFER_SIZE constant,  A temporary memory is allocated by passing this value to an instance of DynamicJsonBuffer.
  • Once memory has been allocated, we pass the client object to the parseJsonObject() method of DynamicJsonBuffer to parse the content of the server response which returns an instance of the JsonObject class that represents the object tree.
  • The success() method of of the JsonObject class returns true if it contains a valid JSON object tree.
  • If a valid JSON object was returned, we copy the values of 'countryName' and 'formatted' from the JSON tree to  localTime->country' and localTime->time respectively.

Displaying Data on the LCD
  • The LCD is initialized by passing the RS, Enable and output pins to an instance of the LiquidCrystal class.
  • The number of rows and columns is set with begin() method which is called at setup().
  • In the main program loop, we display the relevant data to the LCD by passing a pointer to LocalTime  to the displayLCD() function, if readResponseContent() returns true.
  • displayLCD () displays the values using print() method. The cursor() method is used to move to the position where the line should start.
The if we connect the Ethernet Shield to a hub or router theLCD should display the local time from the API endpoint. The contrast is adjusted by using the potentiometer.


Microcontrollers have relatively small memory size and can't handle large or complex JSON objects. I originally intended to display a 'chance of rain' forecast from Weather Underdground, but the API returns a large set of forecast data as a multi-dimensional JSON tree which crashed the Arduino Uno. In cases like this, you need to build another web application hosted on an external cloud server to parse and extract a small sub-set of data from a large JSON object. This data subset is then exposed through a REST API as a simple JSON tree that can be handled by microcontrollers.

These examples are proof of concepts meant to illustrate the basic building blocks of network sensor and actuator devices. For actual projects, it would be more practical to use a microcontoller with an integrated esp8266 wifi interface such as the Adafruit Huzzah. If an Ethernet is required for performance or security, an ENC28J60 module would also be a good option.

Once again, if you have any comments, spot any errors, say hello, or simply rant, you are welcome to do that at the comments section below.