Monday, May 13, 2019

Time on MQTT

I've been promising for a while to write about the time keeping on the network. Well, now is a good time.

Time can be important for many applications. Automatically enabling an alarm system is one use, setback thermostats are another use. One thing that everyone wants to know is what time is it now. The answer to what time is it now, ought to be the same for all systems on the network.

The Raspberry PI has an on-board clock that keeps pretty good time. It adjusts local time for daylight savings and other similar events.

Node red can query the server time, and use that for various things. For my setup, I have a global context to hold the current hours and minutes, and I have an mqtt message sent out every second with the full local time details in a JSON payload.


I mostly only worry about local time, so that the nodes don't have to convert to local time. The individual elements allow the various nodes to act when the hour is 3 or when the minute is 33. They can use any combinations of the current time.

The flow is very simple:


The convert time function converts the system time to local (central) time. Toward the top, where the "new Date()" call is made, there is a language format (en-US, english United States), ant the timezone is "America/Chicago" which is US central time.

// Create a new JavaScript Date object based on the timestamp
// multiplied by 1000 so that the argument is in milliseconds, not seconds.

var currentUtcTime = new Date(); // This is in UTC
var date = new Date(currentUtcTime.toLocaleString('en-US', { timeZone: 'America/Chicago' }));

// Hours part from the timestamp
var hours = "0" + date.getHours();
// Minutes part from the timestamp
var minutes = "0" + date.getMinutes();
// Seconds part from the timestamp
var seconds = "0" + date.getSeconds();

var year = date.getYear()+1900;
var month = "0"+(date.getMonth()+1);
var day = "0"+date.getDate();
// Will display time in 21:30:23 format

var timeStr = year+"-"+month.substr(-2)+"-"+day.substr(-2)+"T"+
              hours.substr(-2) + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);

context.global.hour = date.getHours();
context.global.minute = date.getMinutes();

msg.payload = {"CDTstring" : timeStr,
               "hour": date.getHours(),
               "minute": date.getMinutes(),
               "second": date.getSeconds(),
               "year": date.getYear()+1900,
               "month": date.getMonth()+1,
               "day" : date.getDate()};


return msg;

The timestamp is the inject node firing once a second.



I use the topic "time/ISO-8601" because I was outputting that only for the time. The UTC conversion was done on the nodes, but I quickly found that to be tedious to do for each node that needed the time. Now I do the conversion in node-red and send out the converted time.

Now maybe the other nodes that use time will be more complete.

Should I write about GPIO next time, or USB connected Arduino's? Both are supported by node-red, and can have use in an automated system. Let me know.


Monday, May 6, 2019

Autonomouns Cars; Edge Computing or Cloud?

If you want to demonstrate cloud computing vs edge computing, why not build something to demonstrate the ideas. I have an MQTT controlled robot!



The robot has the usual suspects, with the ESP8266 for the SOC, an HC-SR04 sonar on the front, two continuous revolution servos attached to the wheel on a hobby robot chassis. The schematic is actually from this page: https://www.instructables.com/id/HackerBoxes-0013-Autosport/ that is where I got the kit.

This is my own code, that is running on the 8266. It looks the same as most of the other MQTT nodes in the house:
// 
// 2WD NodeMCU controlled over MQTT (node-red)
//   Sonar distance measured at regular intervals sent over MQTT as well.
// 

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <PubSubClientTools.h>

#include <Thread.h>             // https://github.com/ivanseidel/ArduinoThread
#include <ThreadController.h>

// Update these with values suitable for your network.


const char* ssid = "raspi-webgui";
const char* password = "********";
const char* MQTT_SERVER = "10.3.141.1";

#define echoPin 12 // Echo Pin
#define trigPin 13 // Trigger Pin
#define LEDPin  16 // Onboard LED

#define RightMotorSpeed 5
#define RightMotorDir   0
#define LeftMotorSpeed  4
#define LeftMotorDir    2

WiFiClient espClient;
PubSubClient client(MQTT_SERVER, 1883, espClient);
PubSubClientTools mqtt(client);

ThreadController threadControl = ThreadController();
Thread thread = Thread();

long lastMsg = 0;
char msg[50];
int value = 0;
String s = "";

int maximumRange = 200; // Maximum range needed
int minimumRange = 0; // Minimum range needed
long duration, distance; // Duration used to calculate distance

void setup() 
{
  // put your setup code here, to run once:
  pinMode(BUILTIN_LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output
  Serial.begin(115200);
  setup_wifi();


  // Connect to MQTT
  Serial.print(s+"Connecting to MQTT: "+MQTT_SERVER+" ... ");
  if (client.connect("ESP8266Client")) {
    Serial.println("connected");

    mqtt.subscribe("wheel/Left", topic1_subscriber);
    mqtt.subscribe("wheel/Right", topic2_subscriber);
  } else {
    Serial.println(s+"failed, rc="+client.state());
  }
  
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);

  pinMode(RightMotorSpeed, OUTPUT);
  pinMode(RightMotorDir, OUTPUT);
  pinMode(LeftMotorSpeed, OUTPUT);
  pinMode(LeftMotorDir, OUTPUT);
}

void setup_wifi() 
{
  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}


///////////////////////////////////////////////////////////////////////
////////////  Motor Controls //////////////////////////////////////////
void rightStop()
{
  digitalWrite(RightMotorSpeed, LOW);
}

void leftStop()
{
  digitalWrite(LeftMotorSpeed, LOW);
}

void rightForward()
{
  digitalWrite(RightMotorDir, HIGH);
  digitalWrite(RightMotorSpeed, HIGH);
}

void leftForward()
{
  digitalWrite(LeftMotorDir, HIGH);
  digitalWrite(LeftMotorSpeed, HIGH);
}

void rightReverse()
{
  digitalWrite(RightMotorDir, LOW);
  digitalWrite(RightMotorSpeed, HIGH);
}
void leftReverse()
{
  digitalWrite(LeftMotorDir, LOW);
  digitalWrite(LeftMotorSpeed, HIGH);
}

//////////////////////////////////////////////////////////////////////////
////////// Sonar Sample //////////////////////////////////////////////////
long sample() 
{
/* The following trigPin/echoPin cycle is used to determine the
 distance of the nearest object by bouncing soundwaves off of it. */ 
 digitalWrite(trigPin, LOW); 
 delayMicroseconds(2); 

 digitalWrite(trigPin, HIGH);
 delayMicroseconds(10); 
 
 digitalWrite(trigPin, LOW);
 duration = pulseIn(echoPin, HIGH);
 
 //Calculate the distance (in cm) based on the speed of sound.
 distance = duration/58.2;
 
 if (distance >= maximumRange || distance <= minimumRange)
 {
   /* Send a negative number to computer and Turn LED ON 
   to indicate "out of range" */
   Serial.println("-1");
   //digitalWrite(BUILTIN_LED, LOW);   // Turn the LED on (Note that LOW is the voltage level
 }
 else
 {
   /* Send the distance to the computer using Serial protocol, and
   turn LED OFF to indicate successful reading. */
   Serial.println(distance);
   //digitalWrite(BUILTIN_LED, HIGH);  // Turn the LED off by making the voltage HIGH
 }
 return distance;
}

//////////////////////////////////////////////////////////////////////////
//////////  MQTT Stuff ///////////////////////////////////////////////////

void reconnect()
{
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("ESP8266Client")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "car connected");
      // ... and resubscribe
      //client.subscribe("inTopic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void publisher() {
  long now = millis();
  if (now - lastMsg > 500) {
    lastMsg = now;
    long distance=sample();
    ++value;
    //String payload = "{\"dist\" : ";
    // payload += distance;
    //payload += "}";
    snprintf (msg, 75, "%ld", distance);
    Serial.print("Publish message: ");
    Serial.println(msg);
    mqtt.publish("sonar/distance", msg);
  }
}


void topic1_subscriber(String topic, String payload) {
  Serial.println(s+"Message arrived in function 1 ["+topic+"] "+payload);
  if ((char)payload[0] == 'F') 
  {
    leftForward();
  } 
  else if ((char)payload[0] == 'S') 
  {
    leftStop();
  } 
  else
  {
    leftReverse();
  }
}
void topic2_subscriber(String topic, String payload) {
  Serial.println(s+"Message arrived in function 2 ["+topic+"] "+payload);
  if ((char)payload[0] == 'F') 
  {
    rightForward();
  } 
  else if ((char)payload[0] == 'S') 
  {
    rightStop();
  } 
  else  
  {
    rightReverse();
  }
}

void loop() 
{
  client.loop();
  threadControl.run();
  publisher();
}

The publisher reads the sonar distance, send send on the topic "sonar/distance". The inputs are "wheel/Left" and "wheel/Right", values are "F", "S" and "R" for Forward, Stop and Reverse. The robot has no smarts, it just listens for commands, and sends out distance to nearest object in front.

All this information is processed in Node-red. The flow looks like:


Lots of manual controls, mostly individual control of the direction of the wheels. The middle left/right will get both wheels spinning in the same direction or stop. Then the "Bounce Around" function will get the left wheel to turn the car when it gets too close to something. The function looks like:


var d1 = parseInt(msg.payload);var dx = msg.payload;
if (dx < 17) {    msg.payload = "R";} else {    msg.payload = "F";}//msg.payload.dist = msg.payload.dist;
return msg;

If the distance is less than 17 CM, the left wheel goes in reverse. When the distance is greater than 17CM then the wheel will continue going forward. 

It demonstrates when a reliable communications mechanism is there, cloud calculations can be used to do motion controlled systems. 

Obviously for a real autonomous car more sensors would be needed to ensure safe travel. 


Monday, April 22, 2019

Knowing The Garage Status

I've finally got it working the way I want. If you have been following my garage work, for the last 10 years, I have wanted a way to know if I closed the garage door after I left. I tried several ways, using email and other schemes to know. I finally have a way to know, and it seems quite reliable.

I have Google assistant acting as the middle agent, and with the rest of the infrastructure it seems to be very reliable. Node red knows the state, and has all winter. The voice assistant that I brought up last post is connected to node red, and I have MQTT messages going to the Google assistant. I can ask Google assistant on my phone or tabled to know the current state.

The two categories for doors in gBridge are:

gBridge/uNNN/d4149/openclose is for querying the current state. gBridge/uNNN/d4149/openclose/set is for setting the current state. If I can get the garage device to talk to node red (I am trying, stay tuned), then I will have the node-red control the garage. For now, I am still using the garage android app.



I've broken up the garage flow, and added two new functions. I created a combiner, that will mix the state of the two doors into one message. I have the combiner return a payload of open if either door is open, otherwise will retrun a payload of closed. That function looks like:

// if either door is open, return open, otherwise if both // doors are closed return closed.
context.bigDoor = context.bigDoor || 'closed';context.littleDoor = context.littleDoor || 'closed';
if (msg.topic === 'garage/bigDoor') {  context.bigDoor = msg.payload;} else if (msg.topic === 'garage/littleDoor') {  context.littleDoor = msg.payload;
if ((context.littleDoor === 'open') || (context.bigDoor === 'open')) {        msg.payload = 'open';} else {    msg.payload = 'closed';}msg.topic = 'garage/door';
return msg;


The alarm function node was simplified, to only check the state of the combined nodes and the time.

// if either door is open, after 11pm and before 6am // sound an alarm.
context.hour = context.global.hour || 12;context.door = context.door || 'closed';
if (msg.topic === 'garage/door') {  context.door = msg.payload;} else if (msg.topic === 'time/ISO-8601') {    context.hour = msg.payload.hour;}
// check the status, to know if alarm should be onif ((context.door === 'open')  &&    (context.hour > 22) || (context.hour < 6)) {    // msg.payload = "on " + context.therm + " " + context.temp;    msg.payload = 'on';} else {    msg.payload = 'off';}return msg;

This should make the alarm more reliable as well.

The new function node converts the payload "open" or "closed" to "100" or "0" that the gBridge door nodes expect. The value can be something between 0 and 100 to indicate partially open or closed. I haven't played with it, so I don't know if there is a threshold, but the voice will say "open" if the value is set to 100.

// Convert a msg payload that says "open" or "closed"// to a msg.payload "100" for open, and "0" flr closed
context.flow.value = context.flow.value || "0";context.flow.newVal = context.flow.newVal || "0";
if (msg.payload === 'open'){    context.flow.newVal = "100";} else {    context.flow.newVal = "0"}
if (context.flow.newVal === context.flow.value) {    //msg.payload = {newVal: context.flow.newVal,    //               value: context.flow.value};    //return msg;} else {    msg.payload = context.flow.newVal;    context.flow.value = context.flow.newVal;    return msg;}

I have the ConvOpenCloseTo100or0 function will only send a message on a change. If the door is closed, the MQTT message will only be sent once if either door is opened. Again, if a door is open, it will only send the MQTT message when both doors are closed.

This is something that may allow Google to invade my privacy a little. It should still keep all the logic inside my house (on the edge).


Wednesday, April 17, 2019

Voice Control

Almost everything can be voice controlled these days. Ask your phone, and it'll tell you stuff you didn't know you needed to know. Apple, Google, Amazon and others all have voice assistants that can be used for control of appliances, buying stuff or just entertainment. Node Red has nodes that support most of these assistants.

I have been mostly using the Google infrastructure, since it is typically more open. It isn't perfect, but it does allow me to develop apps and write blogs. I have had some success using their tools for automating my home.

I am using the gbridge node. The setup is very simple, using a standard MQTT interface to their service. It does take some registration on their web site, and in the google home app. They support multiple levels of usage, and allow self hosting. The self hosting can be done on the same RaspberryPi that Node-Red/Mosquitto is running on. The free service allows 4 devices, and more than that will require a subscription, and you choose how much to pay.

Setting up something simple

In the account on the gbridge site, you can set up various devices, what traits (open, close, on, off, etc) those devices will exhibit and what the topic and payload is allowed. This must be done before adding the node to node red.

I connected this to my most complex flow, and it worked right out of the box! After following the setup steps properly. The flow is very simple:



Just an MQTT input node, a debug node and a connector. The connector goes to the heater flow in the previous post. The single "google voice" node could go right in the heater flow without adding much complexity to that flow.

The MQTT node has some configuration:

The server is the gbridge server. This is another MQTT agent that the Node-Red will talk to . It should be set up as any other MQTT agent is added to node-red (like localhost for any of the connections to mosquitto running on the Raspberry Pi).

The URL will have your user number in it. This is your number, and shouldn't be shared with anyone, unless you want others to control your home. The uNNN is my number obfuscated, so I can prevent you from controlling my house.

Once all that is set, using a google assistant (voice or typing), I enter the command "set sunroom thermostat to 80" and the thermostat setting will change to 80 degrees. The payload from the MQTT node is the temperature.

The voice assistant understands other commands like "make it cooler", "make it warmer", but my flow doesn't know what to do with those commands. The payload from the Google Voice MQTT node will send "3" for make it warmer, and "-3" for make it cooler. This could be used in a function node to adjust the current temperature, but for now, I won't use them. What happens now, the thermostat will be set to 3 degrees if the "make it warmer" command is asked.

There are other topics that can be used by node-red to allow querying the status of the node as well. The "gBridge/uNNN/d1000/tempset-ambient/set topic allows me to ask the google assistant what the current temperature is in the room.

I'd like to ask the current state of the garage as well. I've driven off many times, and been unsure if I shut the garage. MAybe in the next post, I'll show how to do that. I also want to show the library that I have made to simplify the nodeMCU development, and other things. There is lots to come.

Ask any questions.




Monday, April 8, 2019

Heater Flow

As promised, I am going to present the flow used for the thermostat node. This flow has a nice dashboard, so it can be controlled from my phone, or from the thermostat, or any other flow input.

The flow:



We can start from the center, the "Check Thermostat" function node. This function take inputs from the heater/state, sunroom/thermostat and sunroom/temperature MQTT topics. The heater/state is published by the nodeMCU attached to the heater, and the payload is either "on" or "off" depending on the state of the heater. The sunroom/thermostat and sunroom/temperature topics are from the thermostat nodeMCU from the last post. The sunroom/temperature payload is a number indicating the temperature measured by the DHT-22 sensor converted to degrees Fahrenheit. The sunroom/thermostat payload is a number representing the setting the thermostat was last set to.

 // if temp is less than thermostat turn on heater.
flow.temp = flow.temp || 0.0;
flow.therm = flow.therm || 70.0;
 if (msg.topic === 'sunroom/thermostat') {
  flow.therm = parseFloat(msg.payload);
} else if (msg.topic === 'sunroom/temperature') {
  flow.temp = parseFloat(msg.payload);
} else if (msg.topic === 'heater/state') {
    flow.state = msg.payload;
}
// add a dead band, if on turn off 1 degree late
if (flow.temp < flow.therm) {
    // msg.payload = "on " + flow.therm + " " + flow.temp;
    msg.payload = "on"
} else if ((flow.temp > flow.therm) &&
           (flow.state === 'on')) {
    //msg.payload = "off " + flow.therm + " " + flow.temp;
    msg.payload = "off";
}
return msg;

There is only one thing output from this, and it indicates if this wants the heater "on" or "off". The output of this is determined if the temperature is less than the thermostat (flow.therm) the payload is sent as "on". If the temperature is greater than the the thermostat setting, and the heater is currently on (flow.state from the heater/state topic) then the payload is set to off. If the temperature matches, it does nothing.

Sometimes the sensor is right on the edge 72degrees/73degrees and it jumps back and forth, so the heater turns on and off frequently. Leaving the sensor to not do anything if the temperature matches the thermostat setting allows the heater to have some hysteresis, and take maybe a minute to change states.

The output of the check thermostat goes to the heater/gas topic, listened to by the nodeMCU connected to the heater itself. There is also a debug output and it is handy to have while debugging things. The function code allows additional information in the payload to see what the current temperature and thermometer settings are when changing state, but that will only work with the debug turned on.

The blue boxes are for the dashboard. The current temperature is a gauge_ui element, and is at the top.


The slider under the gauge is the current thermostat. This is a pass through UI element. It will adjust based on messages heard on the "sunroom/thermostat" MQTT topic, and will send values through to the heater thermostat topic.

The settings look like:



There is also a text output that will show the current thermostat setting. The UI is a relative bar, and no other indicators. I typically add the text items so I know what the slider is pointing at.

I have two switches, both pass through. The current heater state is indicated by values coming in on the heater/state MQTT topic. Clicking this button on or off will output the state to the heater/gas MQTT topic turning the heater on or off (manually). The light on/off is similar sending on the heater/light topic and indicating the debug on/off buttons.

There is a filler UI element. Without the filler the UI sometimes randomly wrapped funny.
That is something to play with to get the widgets. to align properly.



A keen eye might notice a connector widget in the middle of the flow that I didn't mention. This is for the voice input. Yes, from my phone, a Google assistant or other device I can control my house by voice. That is for a future post.



Thursday, April 4, 2019

Thermostat and Node Red

I don't know if I will cover all of this in one post or two. This is the biggest and most complex node I have done. Since it is built on built on public documents and similar ideas, it may go quick.

The reason I built this, we have a nice gas furnace in our sun-room. It is a good sun-room, with lots of windows. Windows are not known for holding in heat. I think this was an add on to our house (looking at the rest of the house and how it was framed underneath it seemed to be maybe a 3 season porch that someone insulted). Imagine this room with the sun shining and leaves on the trees!



The heater had a remote control that was mounted on the wall, and could be used as a thermostat. The bad thing is, the heater gets hot, and sometimes the remote didn't get put back into the little hook on the wall, and well when the remote is sitting on the heater, the case deforms.



I was eventually able to get the cover off the battery compartment, but it never quite worked after that. On eBay these controls sell for over $100, which seems extreme.  For about two years, we just manually controlled the heater with the manual override switch. Sometimes the switch would be left on for days, where other times it was really cold in the room, and it took an hour or more to get up to a comfortable temperature.

We needed a thermostat for that room. Keeping the temperature at some minimum would make the room livable all the time. Preventing the heater from staying on for days would save money. Initially I was going to build a standalone thermostat that would be connected via wires to the heater. Since the thermostat should be away from the heater, wires meant drilling in the walls, and fishing, and lotsa work. I thought about two nodeMCUs that would communicate over WiFi, and that lead me into node-red.

This was actually my first set of node-red nodes. I have a nodeMCU that controls a relay behind the gas stove in a 3D printed box. The Thermostat is controlled by another nodeMCU connected to a touch screen, and a temp/humidity sensor (DHT-22). It has a USB powersupply powering it. The case I am using now is something I found on thingiverse.

I used point to point wiring and followed some basic instructions I found. The touchscreen input and the display are separate devices, but both talk SPI so can be wired to the same SPI pins on the nodeMCU.



Adafruit has a graphics library for the ILI9341 display that includes several fonts. The touchscreen uses the XP2046 chip, so you will need a library for that as well. I think in the Arduino library manager, entering XP2046 will allow installing the library directly without any compiles or other problems. There are displays available in 2.4" and 2.8" (and probably other sizes) that all use the same libraries. The case will need to change to make it all work.

Once the libraries are all installed, then the fun starts. Doing any GUI, we need to provide proper information, as well as simple understandable controls. It is like a joke, if you have to explain it, it isn't that good. In the above display, the top box contains the current Room information. The bottom is the current heater settings with two arrows, one up to raise the temperature, and one down to lower the temperature.

The top of the display is the current date and time. I promise soon there will be an explanation of the Date/Time flow, but this is already getting long, so I probably won't explain it today.

The display can be as fancy as you want it to be. It could have 3d animations for the buttons and fading between temp and humidity, an analog clock or whatever you want to program. I started out pretty basic, and find it meets the needs of the family.

The code, well, it is a little messy, because it is doing a lot!

/**
 * Using the DHT 22 Sensor, get the current temprature and humidity. 
 * Send these values out on the MQTT topics:
 *    sunroom/temperature
 *    sunroom/humidity
 *    
 * Using the touchscreen, display the current time, temprature, humidity, 
 * and thermostat settings. The touch screen allows changing the thermostat
 * setting and sending the value on the MQTT topic:
 *     sunroom/thermostat
 * The thermostat can be changed by listening to the MQTT topic
 *     heater/thermostat
 * The on board LED is controlled by the MQTT topic
 *     heater/light
 * The time is received on the MQTT topic
 *     time/ISO-8601
 *     
 * The code is copyright 2018, Tom Brusehaver 
 * It can be copied using the OSF v3 license. Do what you want with this, but share.
 * 
 * The touchscreen connections are on:   https://nailbuster.com/?page_id=341
 * 
 * Note the input (touch) is the XPT2046, the graphics are the ILI9341
 * Many of the adafruit modules use a different touch input (most of the 
 * ILI9341 touch screen input examples don't work). 
 *
 * The board selected in the IDE is WeMos D1 R2 & Mini. Standard everything 
 * 
 */

#include <SimpleDHT.h>

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <PubSubClientTools.h>
#include <ArduinoJson.h>

#include <XPT2046_Touchscreen.h>
#include <SPI.h>
#include "Adafruit_GFX.h"
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans18pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include "Adafruit_ILI9341.h"

const char* ssid = "raspi-webgui";
const char* password = "XXXXXXXXi";
const char* MQTT_SERVER = "10.3.141.1";

String state="off";

// For the Adafruit shield, these are the default.
#define TFT_DC 2
#define TFT_CS 15

#define TCS_PIN  4
// MOSI=11, MISO=12, SCK=13

#define DHTTYPE DHT12   // DHT 22

// DHT Sensor
const int DHTpin = 16;
SimpleDHT22 dht22(DHTpin);

//XPT2046_Touchscreen ts(CS_PIN);
#define TIRQ_PIN  5
//XPT2046_Touchscreen ts(CS_PIN);  // Param 2 - NULL - No interrupts
//XPT2046_Touchscreen ts(CS_PIN, 255);  // Param 2 - 255 - No interrupts
XPT2046_Touchscreen ts(TCS_PIN, TIRQ_PIN);  // Param 2 - Touch IRQ Pin - interrupt enabled polling

// Use hardware SPI 
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);


WiFiClient espClient;
PubSubClient client(MQTT_SERVER, 1883, espClient);
PubSubClientTools mqtt(client);

/**
ThreadController threadControl = ThreadController();
Thread thread = Thread();
**/

/** Global Values **/
long lastMsg = 0;
String s = "";
int    thermostatSetting=70;
String currTime="2018-01-01T00:00";
String eraseTime="2018-01-01T00:00";
String currTemp="0";
String eraseTemp="0";
String currHumid="0";
String eraseHumid="0";
String currThermo="70";
String eraseThermo="70";
String publishedThermo="70";
int outsideTemperature=70;
String currOutsideTemp="70";

const int width=240;
const int height=320;

void setup() {
  Serial.begin(115200);

  // graphics start
  tft.begin();
  // touch start
  ts.begin();

  // read diagnostics (optional but can help debug problems)
  uint8_t x = tft.readcommand8(ILI9341_RDMODE);
  Serial.print("Display Power Mode: 0x"); Serial.println(x, HEX);
  x = tft.readcommand8(ILI9341_RDMADCTL);
  Serial.print("MADCTL Mode: 0x"); Serial.println(x, HEX);
  x = tft.readcommand8(ILI9341_RDPIXFMT);
  Serial.print("Pixel Format: 0x"); Serial.println(x, HEX);
  x = tft.readcommand8(ILI9341_RDIMGFMT);
  Serial.print("Image Format: 0x"); Serial.println(x, HEX);
  x = tft.readcommand8(ILI9341_RDSELFDIAG);
  Serial.print("Self Diagnostic: 0x"); Serial.println(x, HEX); 

  tft.fillScreen(ILI9341_BLUE);
  displayThermoButtons();

  // Connect to WiFi
  Serial.print(s+"Connecting to WiFi: "+ssid+" ");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(s+" connected with IP: "+WiFi.localIP());

  // Connect to MQTT
  Serial.print(s+"Connecting to MQTT: "+MQTT_SERVER+" ... ");
  if (client.connect("SRThermostat")) {
    Serial.println("connected");

    mqtt.subscribe("heater/light", topic1_subscriber);
    mqtt.subscribe("heater/thermostat", topic2_subscriber);
    mqtt.subscribe("time/ISO-8601", topic3_subscriber);
    mqtt.subscribe("outside/farenheight", topic4_subscriber);
  } else {
    Serial.println(s+"failed, rc="+client.state());
  }
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("SRThermostat")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic","thermostat Restart");
      mqtt.publish("sunroom/temperature", currTemp);
      mqtt.publish("sunroom/thermostat", currThermo);
      mqtt.subscribe("heater/light", topic1_subscriber);
      mqtt.subscribe("heater/thermostat", topic2_subscriber);
      mqtt.subscribe("time/ISO-8601", topic3_subscriber);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      // Wait 5 seconds before retrying
    }
  }
}

void loop() {
  if (ts.touched()) {
    TS_Point p = ts.getPoint();
    Serial.print("Pressure = ");
    Serial.print(p.z);
    Serial.print(", x = ");
    Serial.print(p.x);
    Serial.print(", y = ");
    Serial.print(p.y);
    Serial.println();

    if ((p.y > 3000) && (p.y < 3800))
    {
      Serial.println("** Y zone **");
      if ((p.x > 1100) && (p.x < 1600)) // UP button
      {
        currThermo = String(++thermostatSetting);
      }
      if ((p.x > 500) && (p.x < 1000)) // Down button
      {
        currThermo = String(--thermostatSetting);
      }
    }
  }

  if (!client.connected()) {
    reconnect();
  }

  client.loop();
  
  publisher();           // send out current values.
  tft.setFont();
  displayTime(2);
  tft.setFont(&FreeSans18pt7b);
  displayTemp(2);
  tft.setFont(&FreeSans9pt7b);
  displayHumid(1);
  tft.setFont(&FreeSans9pt7b);
  displayThermostat(2);

  tft.drawRect(25, 40, 200, 125, ILI9341_LIGHTGREY);
  tft.setTextColor(ILI9341_LIGHTGREY);
  tft.setTextSize(1);
  tft.setCursor(28, 55);
  tft.println("Room");

  tft.drawRect(25, 170, 200, 125, ILI9341_LIGHTGREY);
  tft.setTextColor(ILI9341_LIGHTGREY);
  tft.setTextSize(1);
  tft.setCursor(28, 195);
  tft.println("Thermostat");
}

void displayTime(int fontSize)
{
  if (!currTime.equals(eraseTime))
  {
    tft.setCursor(1, 1);
    tft.fillRect(1, 1, width, fontSize*10, ILI9341_BLACK);
    tft.setTextColor(ILI9341_BLACK);
    tft.setTextSize(fontSize);
    tft.println(eraseTime);
  }
  tft.setCursor(1, 1);
  tft.setTextColor(ILI9341_WHITE);
  tft.setTextSize(fontSize);
  tft.println(currTime);
  eraseTime = currTime;
}

void displayTemp(int fontSize)
{
  int X=60; 
  int Y=150;
  int intTemp = currTemp.toInt();
  if (!currTemp.equals(eraseTemp))
  {
    tft.fillRect(X, Y-90, 150, 100, ILI9341_BLUE);
  }
  tft.setCursor(X, Y);
  tft.setTextColor(ILI9341_GREEN);
  tft.setTextSize(fontSize);
  tft.println(intTemp);
  eraseTemp = currTemp;
}

void displayHumid(int fontSize)
{
  int X=50; 
  int Y=80;
  if (!currHumid.equals(eraseHumid))
  {
    tft.fillRect(X, Y-22, 120, 25, ILI9341_BLUE);
  }
  tft.setCursor(X, Y);
  tft.setTextColor(ILI9341_GREEN);
  tft.setTextSize(fontSize);
  tft.print("Humid: ");
  tft.print(currHumid);
  tft.println("%");
  eraseHumid = currHumid;
}

void displayThermostat(int fontSize)
{
  int X=35; 
  int Y=240;
  if (!currThermo.equals(eraseThermo))
  {
    tft.fillRect(0, Y-30, width, 35, ILI9341_BLUE);
    displayThermoButtons();
  }
  tft.setCursor(X, Y);
  tft.setTextColor(ILI9341_GREEN);
  tft.setTextSize(1);
  tft.print("Heat at: ");
  tft.setTextSize(fontSize);
  tft.print(currThermo);
  eraseThermo = currThermo;
}

void displayThermoButtons()
{
   int X = width-75;
   int Y = 180;

   tft.fillRect(X,Y, 50, 50, ILI9341_BLACK);
   tft.fillTriangle(X+5,Y+40, X+25,Y+5, X+45,Y+40, ILI9341_BLUE); 
   Y+=60;
   tft.fillRect(X, Y, 50, 50, ILI9341_BLACK);
   tft.fillTriangle(X+5,Y+10, X+25,Y+45, X+45,Y+10, ILI9341_BLUE);
}

float degCtoF(float deg)
{
  return (deg * 9.0 / 5.0) + 32.0;
}

void publisher() {
  long now = millis();
  if (now - lastMsg > 20000) 
  {
    lastMsg = now;
    Serial.print("Publish message: ");
    float humid = 0;
    float tempC = 0;
    if (dht22.read2(&tempC, &humid, NULL) == 0)
    {
      float tempFl = degCtoF(tempC);
      String val = String(tempFl);
      mqtt.publish("sunroom/temperature", val);
      currTemp=String(tempFl);
      Serial.print("Temp: ");  Serial.println(val);
      val = String(humid);
      mqtt.publish("sunroom/humidity", val);
      currHumid = val;
    }
    else
    {
      Serial.println("Error Reading DHT device");
    }
  }
  if (!currThermo.equals(publishedThermo))
  {
    mqtt.publish("sunroom/thermostat", currThermo);
    publishedThermo = currThermo;
  }
}

void topic1_subscriber(String topic, String message) {
  Serial.println(s+"Message arrived in function 1 ["+topic+"] "+message);
  if (message.equals("on"))
  {
    digitalWrite(BUILTIN_LED, LOW);   // LED on 
    state = "on";
  }
  else
  {
    digitalWrite(BUILTIN_LED, HIGH);
    state="off";
  }
}

/**
 * Read number value of thermostat setting.
 */
void topic2_subscriber(String topic, String message) {
  Serial.println(s+"Message arrived in function 2 ["+topic+"] "+message);
  int thermSetting = message.toInt();
  if ((thermSetting > 55) && (thermSetting < 99))
  {
    thermostatSetting = thermSetting;
    currThermo = String(thermostatSetting);
  }
}

/**
 * Read current time.  (ISO-8601 format: 2016-06-01T21:34:12.629Z)
 */
void topic3_subscriber(String topic, String message) {
  StaticJsonDocument<300> doc;

  DeserializationError error = deserializeJson(doc, message);
  if (error) {
    Serial.println("Parsing failed");
    return;
  }
   // Get the root object in the document
  JsonObject root = doc.as<JsonObject>();

  currTime = root["CDTstring"].as<String>();
  Serial.println(s+"Message arrived in function 3 ["+topic+"] "+currTime);
}

/**
 * Read outside temperature.
 */
void topic4_subscriber(String topic, String message) {
  int temp = message.toInt();
  outsideTemperature = temp;
  currOutsideTemp = String(outsideTemperature);
  
  Serial.println(s+"Message arrived in function 4 ["+topic+"] "+outsideTemperature);
}

The code starts as most of other NodeMCU nodes I have shown. The WiFi SSID and password are there, along with the MQTT broker address. Several globals are defined next. Since the screen tries to update as fast as possible, I have "erase" values. These are the previous settings that the screen will draw in the background color to speed up changes to that section of the screen. I full refresh of the screen over SPI is visible, and should be avoided if possible.

In the setup function, I initialize the ILI9341 display, and read out the current settings. Then I fill the whole screen with BLUE. This is a very saturated blue, and I have thought about changing it to something more muted. In a previous post, I talk about GUIs and muted colors. I then draw the thermostat buttons. This is in another function, and allows me to play with different looking buttons. I've found for this, the basic buttons (triangles inside of squares) are nice enough.

The WiFi and MQTT are started next. I subscribe to 4 topics:
  • heater/light   - The basic on board LED state (on/off) for testing.
  • heater/thermostat - The setting that node-red wants the thermostat at
  • time/ISO-8601 - The time to display
  • outside/farenheight  - The outside temperature (not displayed).
The reconnect publishes 2 messages, if the system ever disconnects from the MQTT broker, current temperature (sunroom/temperature topic) and the current thermostat setting (sunroom/thermostat topic). The thermostat can not only set the temperature to keep the room at, it also listens to Node-Red to know what it wants the thermostat to be set at. This adds complexity to Node-Red, this node is pretty simple, just displaying the last input to the thermostat it got. 

The first thing the loop function checks is if anything is touching the screen. These touch screens are resistive, so they take some pressure to register a touch. For this app, the screens are fine, since the touched area will be quite large. The touch location is something you have to work out depending on the orientation of the screen. The screen touch is roughly positions 0 to 4000 right to left (y direction) and 0-1800 top to bottom (x direction). For my display with the up/down arrows in the lower left, the arrows are 3000 on the left, and 3800 on the right. The up arrow touch area starts at 1100 bottom and goes to 1600 top. The down arrow starts at 500 bottom and goes to 1000 on top. This I derived trial and error with the help of the serial output. 

The loop then calls the publisher to read the current temperature and humidity, and publish these values. Unlike other nodes, I convert the temperature C to temperature F before publishing. Someday I may switch this, but for now, it is done here. The screen is updated after that. All the sections of the screen have their own function to update the display. Using the separate functions should allow a more modular display. I can move the sections by changing the X and Y values in the individual functions. The loop function also has some grey rectangles seperating the sections making the screen a little more finished looking.
When messages come in on the subscribed topics, the values are updated only. The display will update using these values in the next loop() function. There are some safety checks (thermometer cannot be set below 55 or greater than 99) where it is a good idea. 

The time comes in a full json message that needs to be parsed out. For now I have it read only the CDTstring that is encoded in the message. The message is separated out into hours, minutes, seconds and month, day, year. I only send the Central time version, since I can think in local time. 

This node does not turn on the heater anywhere, nor does it compare the thermostat setting to the current temperature. This node only maintains the current settings. Node-Red will turn on the heater if needed, and check the current thermostat value comparing to current temperature. The flow will be handled next time. 

Come back next time.