Example Ruby on Rails Arduino Project

My first use of Ruby on Rails in an Arduino project is a simple temperature and humidity logger. Ideally I'll create another Arduino project to graph the data. Of course we need an Arduino with networking capabilities. You really can't go past the excellent, tiny, cheap HUZZAH from Adafruit.

Electronics

Components I used were:
The HUZZAH board needs more juice than can be supplied through the FTDI breakout, so the USB socket is used to provide a second 5V supply. It needs to be something that will happily put out 500mA.

Pic 1: the foreground positive rail is 3.3V, the background rail 5V.

If you haven't used a HUZZAH before I suggest you go through the tutorial. If you can upload a sketch and enumerate the nearby WiFi access point names then you're all set.

I made a few mistakes setting up this hardware. First, I assumed the FTDI board I had could power the HUZZAH. The Arduino serial monitor showed crash dumps of memory and the sketch restarting. So no, the FTDI wouldn't work on its own. Make sure you have a second power source, that it is connected correctly and can supply 500mA. By correctly I mean don't connect your second power source to the V+ pin on the HUZZAH, that one is shared with the FTDI pin header; something I didn't realise until I found weird voltages on all the supply rails. Use VBat for the second supply.

Lastly, when I hooked up the RHT03 sensor according to the data sheet I just ran it off the 5V rail from the USB-B socket. When my room was quiet I could hear the HUZZAH board screaming a nearly silent high-pitched squeak. So yes, it turns out I was pumping a 5V signal into the HUZZAH which takes only 3.3V inputs. Yikes! I'm glad I didn't blow it.

Circuit Description

The 5V supply from the USB-B socket is wired to GND and VBat on the HUZZAH. The 5V ground is also wired to pin 4 on the RHT03. The 3V pin (3.3V) on the HUZZAH supplies pin 1 on the RHT03 and pin 2 on the RHT03 via a 1kΩ resistor. Pin 2 on the RHT03 is wired to #5 on the HUZZAH. That's it.

Server

Of course you need a machine running Ruby on Rails (see previous blog posts) to act as the server for this project. Being Ruby on Rails there's not a lot to do to make this server work for us. I'm going to call the app 'climate' and record each value as a 'measure'. Given I've run through what it looks like to run these commands in previous blog posts, I'll leave out the response from Rails and just show the list of commands to run:

 ~$ rails new climate  
 ~$ cd climate  
 ~/climate$ rake db:create  
 ~/climate$ rails generate scaffold Measure temperature:float humidity:float  
 ~/climate$ rake db:migrate  
 ~/climate$ rails server

So now, on your Rails server you should be able to hit http://localhost:3000/measures and play with that to ensure you can CRUD Measures.

As explained previously, when we try to do this through JSON we'll get blocked by a built-in Rails security policy. At this stage we can read data using JSON (http://localhost:3000/measures.json) but we won't be able to post new data to the server until we fix the application_controller.rb file. Using a file manager or terminal window, edit ~/climate/app/controllers/application_controller.rb and add one line to make it read thus:
 class ApplicationController < ActionController::Base  
  protect_from_forgery with: :exception  
  protect_from_forgery unless: -> { request.format.json? }  
 end  

OK, so now we're able to post data to the server in JSON format without being blocked. We can test it at the command line using curl:
 ~/climate$ curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"temperature":0, "humidity":0}' http://localhost:3000/measures  

Arduino

So at this stage the server is done. We can move on to the Arduino code. Here's a dump of my Arduino code, I'll follow up with a walk-through:

 #include <ESP8266WiFi.h>  
 #include <ESP8266HTTPClient.h>  
 #include <DHT.h>  
 #include <DHT_U.h>  
   
 #define DHTPIN 5     // Pin that is connected to the DHT sensor.  
 #define DHTTYPE DHT22   // DHT 22 (AM2302) AKA: RHT03  
   
 const char* ssid   = "FifthBit";  
 const char* password = "ThisIsNotMyRealPassword";  
 const char* restURL = "http://192.168.1.100:3000/measures";  
 int maxWiFiConnectionWait = 20; // Seconds  
   
 bool connectWiFi();  
 bool sensorValues(float&, float&);  
 String toJSON(float, float);  
 int postValues(String json);  
   
 DHT_Unified dht(DHTPIN, DHTTYPE);  
   
 void setup() {  
  Serial.begin(115200);  
  dht.begin();  
 }  
   
 void loop() {  
   
  Serial.println("Wait 5.");  
  delay(5000);  
   
  if (!connectWiFi()) {  
   Serial.println("No WiFi");  
   return;  
  }  
  Serial.println("WiFi OK");  
   
  float temperature;  
  float humidity;  
  if (!sensorValues(temperature, humidity)) {  
   Serial.println("No sensor values");  
   return;  
  }  
   
  String json = toJSON(temperature, humidity);  
  int httpCode = postValues(json);  
    
  if (httpCode == 201) {  
   Serial.print("POSTED: ");  
   Serial.println(json);  
   delay(55000);  
  } else {  
   Serial.print("Fail. HTTP ");  
   Serial.println(httpCode);  
  }  
   
 }  
   
 int postValues(String json) {  
    
  HTTPClient http;   //Declare object of class HTTPClient    
  http.begin(restURL); //Specify request destination  
  http.addHeader("Content-Type", "application/json"); //Specify content-type header  
  http.addHeader("Accept", "application/json");  
  int httpCode = http.POST(json);  //Send the request  
  String payload = http.getString();         //Get the response payload  
   
  return httpCode;  
   
 // Serial.print("HTTP Response: ");  //Print HTTP return code  
 // Serial.print(httpCode);  
 // Serial.println(payload);  //Print request response payload  
 }  
   
 String toJSON(float temperature, float humidity) {  
   
  String temp = String(temperature);  
  String humi = String(humidity);  
  return String("{\"temperature\": ") + temp + ", \"humidity\": " + humi + "}";  
 }  
   
 bool sensorValues(float &temp, float &humid) {  
   
  sensors_event_t event;  
   
  dht.temperature().getEvent(&event);   
  if (isnan(event.temperature)) {  
   return false;  
  } else {  
   temp = event.temperature;  
  }  
   
  dht.humidity().getEvent(&event);  
  if (isnan(event.relative_humidity)) {  
   return false;  
  } else {  
   humid = event.relative_humidity;  
  }  
   
  return true;  
 }  
   
 void connectSensor() {  
   
  Serial.println("Starting sensor");  
 }  
   
 bool connectWiFi() {  
   
  if (WiFi.status() == WL_CONNECTED) return true;  
   
  int secondsWaited = 0;  
  Serial.print("Connecting to ");  
  Serial.println(ssid);  
   
  delay(1000);  
  WiFi.begin(ssid, password);  
    
  while (WiFi.status() != WL_CONNECTED) {  
   if (secondsWaited > maxWiFiConnectionWait) {  
    Serial.println("FAILED");  
    return false;  
   }  
   secondsWaited++;  
   delay(1000);  
   Serial.print(".");  
  }  
    
  Serial.println();  
  Serial.print("Connected: ");  
  Serial.println(WiFi.localIP());  
  return true;  
 }  

Code Walkthrough

My aim with this code is to make the system resilient to losing the WiFi network as well as the web server.

Top Level Code

The four #include directives are required to access libraries to operate the HUZZAH and the RHT03 sensor. (Yes, I'd rather write my own version of these!) The #define lines are required to set up the sensor. The const char* lines provide strings for configuration of the WiFi network and the URL for the REST server. Here you will need to use the IP address of the system where you're running Ruby on Rails. If you're running a Linux VM as I recommended you can do an ifconfig at the command line to find the IP address. Windows folks will think I meant "ipconfig", but on Linux it really is ifconfig. Network interfaces are interfaces on Linux, so commands like ifup, ifdown and ifconfig are used. I've then got a few function signatures because the functions themselves are implemented below where they're called in loop().

setup()

To my mind, void setup() {} must only contain code that will definitely never need to be called again. I'm not actually sure about DHT.begin(). How often might that sensor type lock up? I don't know. For now I'm assuming both Serial and dht are rock solid and will never fail. The reason I don't just throw these somewhere in void loop() {} is that they might cause a memory leak if they're called repeatedly.

loop()

We don't want this loop to get called at a high rate if both connectWiFi() and sensorValues() fall through, so the delay(5000) puts a limit on that. connectWiFi() will return true if either the WiFi is already connected or connecting to the WiFi network was successful. Likewise sensorValues(temperature, humidity) will return true if both values were successfully read from the sensor. When both are successful we have a go at posting the data to the server in a HTTP POST request carrying JSON text. If we get anything other than a HTTP 201 response, we've failed to post the data.

If we've posted data successfully we wait 55,000ms so the entire loop() delay is 60 seconds. Of course there are delays in connecting to WiFi when required as well as flight-time for the network packets to/from the web server, so the time between samples is actually "no less than 60 seconds". Actually measuring these values so rapidly is next to useless when measuring ambient conditions so delaying 10 minutes would be better.

connectWiFi()

This code is almost a straight steal from the HUZZAH sample code except that I've limited the wait time so I don't just get an unlimited number of decimals printed across the serial monitor.

connectSensor()

This is not currently called. But if it turns out the RHT03 flakes out from time to time I'll implement it.

sensorValues()

This code is from the DHT11 example code. It's implemented as a function because that's a logical chunk of work that can be called to return three values.

toJSON()

Again a logical bit of work to make a String from two float values.

postValues()

All we care about here is taking the JSON text and returning the HTTP response code from the server. We need to correctly configure the http request with the Content-Type and Accept headers so this works.

Results

When I look at the Serial monitor in Arduino, I see this:
 Wait 5.  
 WiFi OK  
 POSTED: {"temperature": 23.30, "humidity": 60.30}  
 Wait 5.  
 WiFi OK  
 POSTED: {"temperature": 23.30, "humidity": 60.30}  

So as you can see the conditions in my office right now are just dandy. Back on the Rails server, this is spitting out each time the Arduino has a conversation with the server. Awesome. Looks good.
 Started POST "/measures" for 192.168.1.24 at 2017-03-09 10:16:20 +0000  
 Processing by MeasuresController#create as JSON  
  Parameters: {"temperature"=>23.3, "humidity"=>60.4, "measure"=>{"temperature"=>23.3, "humidity"=>60.4}}  
   (17.0ms) begin transaction  
  SQL (15.8ms) INSERT INTO "measures" ("created_at", "humidity", "temperature", "updated_at") VALUES (?, ?, ?, ?) [["created_at", "2017-03-09 10:16:21.074032"], ["humidity", 60.4], ["temperature", 23.3], ["updated_at", "2017-03-09 10:16:21.074032"]]  
   (61.6ms) commit transaction  
  Rendered measures/show.json.jbuilder (20.5ms)  
 Completed 201 Created in 382ms (Views: 173.1ms | ActiveRecord: 94.5ms)  

So now we can look at the data being posted at http://localhost:3000/measures on the Rails server. Tada! As you can imagine, this page gets bigger and bigger as the data set increases. But it's simple to give Rails a command to limit the data size. Although this doesn't work out of the box, you can imagine a GET request something like this http://localhost:3000/measures?limit=100 could be coded at the server to get the latest 100 samples. But I'll save that kind of thing for the next post where I'll go into more Ruby code to get Rails to do more of what we want.

That is all.

Comments