Showing posts with label arduino. Show all posts
Showing posts with label arduino. Show all posts

Sunday, March 8, 2020

Eggbot; filling in some missing information

I guess it is that time of year, people are building Eggbots. I built one, and needed to learn a bunch. Some of the information was vague, or not connected to the original design, or anyone elses design.



I started with a Russian frame that I liked on Thingiverse. This was mostly lacking any real documentation. Elliot from Hackaday has a page with lots of information, including how to make a CNC shield move a servo, and how to get G code out of inkscape.

The Russian frame had a bunch of options for the pen holder. I chose the simple one, and am having success using that with a sharpie fine point. Being an American, it was easier to get 1/4-20 5/16-18 threaded rod than 6mm, so that is what I used. I cut 3 1ft pieces. For the rest of the bolts and screws, I used metric (m4 and m3 mostly), since the motors assume metric.

One tool that I will call essential is the taper cutter. Cutting threaded rod is easy, but the threads are suddenly all mucked up. Either the taper goes away, and threading on a nut has to be aligned perfectly, or the threads are bunged up so badly nothing will thread on anymore. This tool will make the trouble disappear, in about 2 seconds!. Sure you could file a taper in using a file, or grinder, but for less than $5, why?

The Russian  frame also assumed some 16mm bearings, that I didn't order. Another similar frame used skateboard bearings in a holder with the same bolt pattern as the stepper motors. I just reprinted the left frame from the Russian frame and two of these bearing holders for the end.

I couldn't find O rings that would hang onto the egg good. One end or the other would slip, and the drawing wasn't real good. I found some #17 bath diverter washers that seemed to be a better shape, and fit the end caps well. They still slipped.


I've started playing with some table foot rubber pieces, like on the bottom of a crutch. They are more grippy. I'll update when I know more.

Some page, maybe another one on Thingiverse suggested I use a CNC V4 Shield. A great design, with the Arduino Nano right on the board with the stepper drivers. The trouble is, the common Chinese clone makers messed up the board. A couple others have figured out the changes needed, and published the wiring changes to make this board work.

What I have seen though, the inkscape G code output doesn't quite make the pen up/down command that the GRBL expects. The delays are about 100 times longer than they say (seconds instead of milliseconds) as well  (Update, in the save dialog, it lets you set delay to 0, that seems to work). Long delays with the pen over at an angle almost always messes up the continuation of the lines. I end up hand editing the G code output. I should probably build a python or perl script to fix the output, or just figure out what unicorn changes are needed.

Changing the GRBL source code, it is important to remember you moved the code to the Arduino directory, so change it there, not where you expanded the code. If the motors are going backwards, reverse the plugs on the CNC V4 board.

For test eggs, I have been using the 18 for a dollar plastic eggs that I can get at the Dollar Tree. I like hard boiled eggs, but I didn't think I'd eat 24-36 in the next couple of weeks. I should look into dry erase markers to see if I can erase the test eggs.




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.