Showing posts with label ESP8266. Show all posts
Showing posts with label ESP8266. Show all posts

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 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.



Wednesday, March 13, 2019

An Input Node

In the last post I mentioned that I would build an input node. This node will be capable of monitoring 2 doors, and the temperature. I have this node out in the garage. There are magnetic reed switches near the two doors and a Dallas DS18B20 temperature sensor near the door.

I built the node with the ESP8266, using the Arduino IDE. There is a "Garage" Flow in my Node-Red environment for monitoring this node. The node code is very similar to the tone generator node, but only sends output. It has the capability to turn on the LED, but there isn't anything in the Flow that actually uses it.

Here is the code:


/**
 * Simple action node. Read sensors and send out status
 * 
 * garage/bigDoor to get status of garage door
 * garage/littleDoor to get status of garage door.
 * garage/outsideTemp to read ds18b20 temperature sensor. 
 * 
 * They are separate devices, but using Node Red can be connected.
 * 
 * 
 * TGB 9/30/18
 */

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

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

// Data wire is plugged into port D3 on the ESP8266
#define ONE_WIRE_BUS D3

// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);

//DS18B20
#define ONE_WIRE_BUS D3 //Pin to which is attached a temperature sensor
#define ONE_WIRE_MAX_DEV 15 //The maximum number of devices

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

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

long lastMsg = 0;
int value = 0;
String s = "";

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

  pinMode(BUILTIN_LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output

  pinMode(D1, INPUT_PULLUP);      // big door
  pinMode(D2, INPUT_PULLUP);      // little door
  
  // Connect to WiFi
  Serial.print(s+"Connecting to WiFi: "+ssid+" ");
  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("GarageControl")) {
    Serial.println("connected");
    digitalWrite(BUILTIN_LED, HIGH); // turn LED off

    mqtt.subscribe("garage/light", topic1_subscriber);
  } else {
    Serial.println(s+"failed, rc="+client.state());
  }

  // Start up the library
  sensors.begin();
}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect("GarageControl")) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic","garageControl Reconnect");
      mqtt.subscribe("garage/light", topic1_subscriber);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" Resetting");
      // Wait 5 seconds before retrying
    }
  }
}

void loop() {
  if (!client.connected()) {
    reconnect();
  }
  
  client.loop();
  publisher();
}

void publisher() {
  long now = millis();
  if (now - lastMsg > 5000)  // 10 second loop.
  {
    lastMsg = now;
    ++value;
    Serial.println("Publish messages: ");


    int bd = digitalRead(D1);
    int ld = digitalRead(D2);

    String bigDoor="closed";
    String littleDoor="closed";

    if (bd) {
      bigDoor = "open";
    }
    if (ld) {
      littleDoor = "open";
    }
    Serial.print(" bigDoor: ");
    Serial.println(bigDoor);
    Serial.print(" littleDoor: ");
    Serial.println(littleDoor);
            
    mqtt.publish("garage/bigDoor", bigDoor);
    mqtt.publish("garage/littleDoor", littleDoor);

    sensors.requestTemperatures(); // Send the command to get temperatures
    int temp = sensors.getTempCByIndex(0); 
    Serial.print(" temperature: ");
    Serial.println(temp);
    String tempS = String(temp);
    mqtt.publish("garage/outsideTemperature", tempS);
  }
}

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);   // Turn the LED on (Note that LOW is the voltage level
  }
  else
  {
    digitalWrite(BUILTIN_LED, HIGH);
  }
}



The Dallas DS18B20 is a simple to use device that uses the Dallas 1-wire protocol for reading data. Several 1-wire devices can be added to the single pin needed, and only ground and power are needed (actually for some devices power is optional, since the devices can be powered by the serial link). The 1-wire library is all that is needed to used these devices with almost any Arduino system. 

The code starts out initializing the 1-Wire device:


#include <OneWire.h>
#include <DallasTemperature.h>

// Data wire is plugged into port D3 on the ESP8266
#define ONE_WIRE_BUS D3

// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(ONE_WIRE_BUS);

// Pass our oneWire reference to Dallas Temperature. 
DallasTemperature sensors(&oneWire);

We need to include the various libraries. I've assigned the serial data to come in on the D5 ESP8266 pin. Again, I am using the 8266's library definition to avoid having to map to the Arduino GPIO pin names. I find it easier to use the library names, since that is what is silk-screen printed on the device. 

The setup function is mostly identical to the tone generator. The only difference is setting the input mode for the two reed switches and starting the sensor monitoring that checks the ds1820. The "INPUT_PULLUP" setting for the D1 and D2 allow me to have the reed switch connected to the D1 or D2 input pins and the other side connected to ground. 

  pinMode(BUILTIN_LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output

  pinMode(D1, INPUT_PULLUP);      // big door
  pinMode(D2, INPUT_PULLUP);      // little door
    // Start up the library
  sensors.begin();



Once everything is started, the main loop() just checks the WiFi connection state, and then calls publish(), a new function. The publish function checks the state of the devices and publishes the results. This is the new part that is different from the tone generator node.

void publisher() {
  long now = millis();
  if (now - lastMsg > 5000)  // 10 second loop.
  {
    lastMsg = now;
    ++value;
    Serial.println("Publish messages: ");


    int bd = digitalRead(D1);
    int ld = digitalRead(D2);

    String bigDoor="closed";
    String littleDoor="closed";

    if (bd) {
      bigDoor = "open";
    }
    if (ld) {
      littleDoor = "open";
    }
    Serial.print(" bigDoor: ");
    Serial.println(bigDoor);
    Serial.print(" littleDoor: ");
    Serial.println(littleDoor);
            
    mqtt.publish("garage/bigDoor", bigDoor);
    mqtt.publish("garage/littleDoor", littleDoor);

    sensors.requestTemperatures(); // Send the command to get temperatures
    int temp = sensors.getTempCByIndex(0); 
    Serial.print(" temperature: ");
    Serial.println(temp);
    String tempS = String(temp);
    mqtt.publish("garage/outsideTemperature", tempS);
  }
}

There is a global variable called "lastMsg" that contains the last time the sensors were checked. If the last time was more than 5000 milliseconds (5 seconds) ago, then the sensors will be checked. If less than 5 seconds ago, this function will not do anything. The two door switches are checked. If the switch is closed, the bd/ld values will be 1, otherwise those values will be 1. The MQTT payload is initialized to be "closed". If the big door switch is closed, the "bd" variable will be 1, and the bigDoor payload will be updated to "open". Once both doors have been checked, the mqtt.publish will send out the payload for the two topics, "garage/bigDoor" and "garage/littleDoor".

After the doors have had the MQTT message published, the DS1820 will be checked. The return value from the sensor.getTempCByIndex() will be the temperature in degrees C.  The temperature value will be published on the "garage/outsideTemperature" topic.


A couple thoughts about MQTT


MQTT brokers can maintain state for various topics. There are good things and bad things about maintaining state. If the state is maintained, the last payload will be kept. If a node dies, there may not be a way to determine when the last time the node reported the state. For temperature, a maintained state may not good, since it may say the value is comfortably warm, when the real temperature is very cold or hot.

Node-Red is generally stateless. That is, unless someone publishes an MQTT message, node-red will not check for state of an item. If node-red is restarted, the node states will not be known, until the next time they publish their state.

I generally have each node publish state every few seconds. Even if the state hasn't changed, I assume that node-red doesn't know. It does mean there will be messages going on the network, that may be unnecessary. MQTT messages are small, and even 802.11n WiFi is fast enough for thousands of messages a second.


Garage Flow


The garage flow shows some new concepts. There will be some programming, but should be easy to understand.




Starting on the left top, there are 2 MQTT input nodes. Both of these will use the "localhost" MQTT server. Each will be subscribed to unique MQTT topics. The littleDoor node will be subscribed to the "garage/littleDoor" topic defined above, and the bigDoor node will be subscribed to the bigDoor MQTT topic. Both MQTT nodes are connected to debug nodes, allowing me to see if the message is being received when something comes in. A third MQTT input node is subscribed to the "garage/outsideTemperature"

The temperature node is connected to another debug node, as well as the cDegToF node. The cDegToF node converts an MQTT payload containing a temperature in Celsius and converts that to a payload in degrees Fahrenheit. The output of cDegToF node is connected to a debug node, and a MQTT output node. The MQTT output node publishes the Fahrenheit temperature on the topic "outside/farenheight". Other nodes can subscribe to "garage/outsideTemperature" or "outside/farenheight" depending on their needs.

The cDegToF node is a function node (note the F on the left). This means it is a small program. Nodes can be programmed in JavaScript. The code looks like:



The msg.payload that comes in is converted using the F=9/5C+32 calculation. To edit a function, simply double click on the node after dragging it to the work area. The above dialog will appear.

The mode advanced function node in this flow is the "Door Open Alarm" node. As you can see from the cDegToF node above, functions are easy with one message coming in. For this node, there are two messages that can cause the alarm to sound. Either big door or little door messages will cause the alarm to come on. If one door is open, and the other closed, the function may not know if anything is open, if the node only looks at all msg.payloads.

I need to introduce context. State of nodes in Node-Red have several contexts. The node has a context for the instant it is running, there is a flow based context, that all nodes within a flow can know about, and there is the global node-red context that all flows share.

Code for the Door Open Alarm:

// if either door is open, after 11pm and before 6am 
// sound an alarm.

context.hour = context.global.hour || 12;
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;
} else if (msg.topic === 'time/ISO-8601') {
    context.hour = msg.payload.hour;
}

// check the status, to know if alarm should be on
if (((context.bigDoor === 'open') || (context.littleDoor === 'open')) &&
    (context.hour > 22) || (context.hour < 6)) {
    // msg.payload = "on " + context.therm + " " + context.temp;
    msg.payload = 'on';
} else {
    msg.payload = 'off';
}
return msg;


For the "Door Open Alarm" node, I maintain state in a the node's context. The context is initialized to the current state or if unknown, to the 'closed' state. The time is global context that I have in another node (I'll talk about that in another post).

If a message comes in on a topic that the node knows about (garage big/little door), then the context for that door is updated to the current state. If either door is open during the alarm time, a new message is published containing the payload "on". This node doesn't know the recipient of the payload, only the payload.

That last node in this flow is the MQTT output node that will send the alarm payload to the tone generator. It will publish the payload passed here on the "home/alarm" topic.

This may be lots to digest. Hopefully it will give some ideas of the capabilities of a RaspberryPi and some 8266's.

More to come.