Konubinix' site

IOT Heart

tag: learning 3D printing

For our birthday, I wanted to create the connected hearts from Heliox.

Printing

To be able to print it on my labists x1 mini, I need to shrink the model of the heart. Hopefully I will still have room to put enough LED and the holes will be big enough for the wires.

The heart is indeed too big.

By rotating it, I can make go as far as 74% of the original size.

Or, by removing the brim, I can go up to 77%. Actually, since the heart is mostly flat. I should be ok without brim.

Due to the fact it is mostly empty, I need to put some infill.

I don’t know why, but the basement lap was kinda hard to print, due to the fact it kept breaking. May be I printed it too cold (I used 210°C for a 200-230°C filament) or the wall was too thin (see how to solve cracking or breaking of layers during 3d printing). I added infill anyway because I wanted the lap to be strong after all.

I also learned that my printer is definitely not able to print very fast.

The support itself did print quite easily

[2021-08-05 Thu] After some digging, the problem was due to the nozzle being clogged. Using some cleaning filament made it work great again.

Assembling

After printing and soldering, I tried uploading a basic blinking program.

Great! It works, now its time for programming.

Setting up the arduino development environment

Programming

I disliked the initial code. It was too low level. Also, because of all the trouble I had printing one heart, I did not have time for printing a second one.

So I decided that there would be only one heart, on the living room with each side telling the emotion of one of us. This is great, since we always teach our kids to tell, name and accept their emotions. It is for us a good opportunity to practice what we try to teach them.

I decided that the weemos memory was enough to write a more elegant C++ code, with classes. I still did not have much time, so here is the end result.

Basically, it connects to the Wifi and polls every few seconds an endpoint that gives the color and intensity information for both sides of the heart.

#include <Arduino.h>
#include <Arduino_JSON.h>

#include <math.h>
#include <Adafruit_NeoPixel.h>

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>

#include <ESP8266HTTPClient.h>

#include <WiFiClient.h>

ESP8266WiFiMulti WiFiMulti;


class Command {
 public:

    int r1;
    int g1;
    int b1;
    int v1;
    int r2;
    int g2;
    int b2;
    int v2;
    boolean connected;

    void connect() {
        WiFi.mode(WIFI_STA);
        WiFiMulti.addAP("yourwifissid", "yourwifipassword");
    }

    bool refresh() {
        bool result = false;
        if ((WiFiMulti.run() == WL_CONNECTED))
            {
                this->connected = true;
                WiFiClient client;
                HTTPClient http;
                if (http.begin(client, "http://192.168.1.46:9696/get")) {
                    int httpCode = http.GET();
                    if (httpCode > 0) {
                        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
                            String payload = http.getString();
                            JSONVar myArray = JSON.parse(payload);
                            this->r1 = myArray[0];
                            this->g1 = myArray[1];
                            this->b1 = myArray[2];
                            this->v1 = myArray[3];
                            this->r2 = myArray[4];
                            this->g2 = myArray[5];
                            this->b2 = myArray[6];
                            this->v2 = myArray[7];
                            result = true;
                        } else {
                            Serial.printf("Bad http code %d\n", httpCode);
                        }
                    } else {
                        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
                    }
                    http.end();
                } else {
                    Serial.printf("[HTTP} Unable to connect\n");
                }
            }
        else
            {
                Serial.printf("Waiting for connection\n");
                this->connected = false;
            }
        return result;
    }

    void print() {
        if(this->connected)
            {
                Serial.printf("r1: %d\n", this->r1);
                Serial.printf("g1: %d\n", this->g1);
                Serial.printf("b1: %d\n", this->b1);
                Serial.printf("v1: %d\n", this->v1);
                Serial.printf("r2: %d\n", this->r2);
                Serial.printf("g2: %d\n", this->g2);
                Serial.printf("b2: %d\n", this->b2);
                Serial.printf("v2: %d\n", this->v2);
            }
        else
            {
                Serial.printf("Not connected\n");
            }
    }
};

class LEDStrip {
 public:
    int pin;
    Adafruit_NeoPixel strip;
    uint8_t r;
    uint8_t g;
    uint8_t b;
    uint8_t v;
    bool isShowing;
    uint32_t npixels;

    LEDStrip(int pin, int npixels) {
        this->pin = pin;
        this->npixels = npixels;
        this->strip = Adafruit_NeoPixel(npixels, pin, NEO_GRB + NEO_KHZ800);
        this->isShowing = false;
    }

    void init() {
        this->strip.begin();
        this->strip.clear();
    }

    void set(uint8_t r, uint8_t g, uint8_t b, uint8_t v) {
        this->r = r;
        this->g = g;
        this->b = b;
        this->v = v;
        this->isShowing = true;
        for (int n=0; n<this->npixels; n++) {
            this->strip.setPixelColor(n, this->strip.Color(this->r, this->g, this->b));
        }
        this->strip.setBrightness(this->v);
        this->strip.show();
    }
};

LEDStrip right(5, 5);
LEDStrip left(4, 5);
Command command;

void setup() {
  Serial.begin(9600);
    command.connect();
    right.init();
    left.init();
}

void loop() {
    if(command.refresh())
        {
            right.set(command.r2, command.g2, command.b2, command.v2);
            left.set(command.r1, command.g1, command.b1, command.v1);
        }
    else
        {
            right.set(255, 255, 255, 10);
            left.set(255, 255, 255, 10);
        }
    delay(1000);
}

The server code

As you could see, the arduino code polls an IP address to get the color and intensity information.

I did not want to depend on some external service like blink, so I host my own service. Fortunately, using flexx, it is a piece of cake.

Here is the final code.

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import json
import logging
import os
from pathlib import Path

import tornado.web
from flexx import config, flx
from redis import StrictRedis

logging.basicConfig(level=logging.DEBUG)

config.hostname = "0.0.0.0"
config.port = os.environ["IOTHEARTS_PORT"]
config.tornado_debug = True
config.log_level = "debug"
# trying to increase the timeout 20 -> 60 to be resilient to wifi disruption
config.ws_timeout = 60

__version__ = (Path(__file__).parent / 'VERSION').read_text()
db = StrictRedis(os.environ["SERVICEMESH_IP"], decode_responses=True)


def add_data(path):
    path = Path(__file__).parent / "assets" / path
    return flx.assets.add_shared_data(path.name, path.read_bytes())


MANIFEST = add_data("manifest.js")
ICON = add_data("icon.png")

PEACE = add_data("peace.png")
JOY = add_data("joy.png")
ANGER = add_data("anger.png")
LOVE = add_data("love.png")
SADNESS = add_data("sadness.png")
FEAR = add_data("fear.png")
LEFT = add_data("left.png")
RIGHT = add_data("right.png")


class GetColors(tornado.web.RequestHandler):

    async def get(self):
        self.write(
            json.dumps(
                json.loads(db.get("color_tuple_LEFT") or "[0, 0, 0]") +
                json.loads(db.get("color_tuple_RIGHT") or "[0, 0, 0]")))


class GetColors2(tornado.web.RequestHandler):

    async def get(self):
        self.write(
            json.dumps({
                "left":
                json.loads(db.get("color_tuple_LEFT") or "[0, 0, 0]"),
                "right":
                json.loads(db.get("color_tuple_RIGHT") or "[0, 0, 0]"),
            }))


def serve(app):
    flx.App(app, icon=ICON).serve()
    return app


class Manifest(flx.JsComponent):

    def init(self):
        global window
        manifest = window.document.createElement('link')
        manifest["rel"] = 'manifest'
        manifest["href"] = "/" + MANIFEST
        global document
        document.getElementsByTagName('head')[0].appendChild(manifest)


class Image(flx.ImageWidget):
    inner_color = flx.StringProp(settable=True)

    def _create_dom(self):
        global window
        outer = window.document.createElement('div')
        inner = window.document.createElement('img')
        inner.style.display = "block"
        inner.style.margin = "auto"
        inner.style.verticalAlign = "middle"
        outer.appendChild(inner)
        return outer, inner

    @flx.reaction("inner_color")
    def _inner_color(self, *ev):
        if self.inner_color:
            self.node.style.borderColor = self.inner_color
            self.node.style.borderStyle = "solid"
            self.node.style.borderWidth = "2px"


@serve
class IOTHearts(flx.PyWidget):
    CSS = """
.selected > img {
background-color: lightGrey;
}
"""
    side = flx.EnumProp(["LEFT", "RIGHT"], settable=True)

    def init(self):
        self.set_side(self.session.get_cookie("side") or "LEFT")
        _ = Manifest()
        with flx.VSplit():
            with flx.HFix(flex=0.1):
                self.left = Image(source=LEFT)
                self.right = Image(source=RIGHT)
            self.slider = flx.Slider(min=0, max=255, value=50, flex=0.1)
            with flx.HSplit(flex=0.9):
                with flx.VSplit(flex=0.3):
                    self.joy_color = "#FFFF00"
                    self.joy = Image(source=JOY)
                    self.peace_color = "#00FF00"
                    self.peace = Image(source=PEACE)
                    self.fear_color = "#222222"
                    self.fear = Image(source=FEAR)
                self.color = flx.ColorSelectWidget(
                    flex=0.9,
                    color=self.db_color,
                )
                with flx.VSplit(flex=0.3):
                    self.anger_color = "#FF0000"
                    self.anger = Image(source=ANGER)
                    self.sadness_color = "#0000FF"
                    self.sadness = Image(source=SADNESS)
                    self.love_color = "#ff00ff"
                    self.love = Image(source=LOVE)

    @flx.reaction("anger.pointer_click")
    def _anger(self, *event):
        self.color.set_color(self.anger_color)

    @flx.reaction("peace.pointer_click")
    def _peace(self, *event):
        self.color.set_color(self.peace_color)

    @flx.reaction("joy.pointer_click")
    def _joy(self, *event):
        self.color.set_color(self.joy_color)

    @flx.reaction("sadness.pointer_click")
    def _sadness(self, *event):
        self.color.set_color(self.sadness_color)

    @flx.reaction("love.pointer_click")
    def _love(self, *event):
        self.color.set_color(self.love_color)

    @flx.reaction("fear.pointer_click")
    def _fear(self, *event):
        self.color.set_color(self.fear_color)

    @property
    def db_color(self):
        return db.get(self.key_hex) or "#000"

    @property
    def db_slider(self):
        return db.get(self.key_slider) or "50"

    @property
    def key_hex(self):
        return f"color_hex_{self.side}"

    @property
    def key_slider(self):
        return f"color_slider_{self.side}"

    @property
    def key_tuple(self):
        return f"color_tuple_{self.side}"

    @flx.reaction("side")
    def side_updated(self, *event):
        self.left.set_css_class("selected" if self.side == 'LEFT' else "")
        self.right.set_css_class("selected" if self.side == 'RIGHT' else "")
        self.session.set_cookie("side", self.side)
        self.color.set_color(self.db_color)
        self.slider.set_value(self.db_slider)

    @flx.reaction("left.pointer_click")
    def _left(self, *event):
        self.set_side('LEFT')

    @flx.reaction("right.pointer_click")
    def _right(self, *event):
        self.set_side('RIGHT')

    @flx.reaction("color.color", "slider.value")
    def update_color(self, *events):
        if not isinstance(self.color.color, str):
            color_tuple = [
                int(self.color.color.t[0] * 255),
                int(self.color.color.t[1] * 255),
                int(self.color.color.t[2] * 255),
                int(self.slider.value),
            ]
            db.set(self.key_hex, self.color.color.hex)
            db.set(self.key_slider, self.slider.value)
            db.set(self.key_tuple, json.dumps(color_tuple))

    @flx.reaction("color.color", "slider.value")
    def _set_heart_colors(self, *ev):
        self.set_heart_colors()

    @flx.action
    def set_heart_colors(self):
        self.left.set_inner_color(db.get('color_hex_LEFT') or '#000')
        self.right.set_inner_color(db.get('color_hex_RIGHT') or '#000')


def main():
    tornado_app = flx.current_server().app
    tornado_app.add_handlers(r".*", [
        (r"/get", GetColors),
        (r"/get2", GetColors2),
    ])
    flx.start()

There is nothing fancy here, the interface looks like this

One can select per heart on the top (a cookie is used to remember the selection) and select the wanted color clicking on the big rectangle. The colored monsters are shortcut to select classical colors.

The idea of the colored monster to show ones emotion is from this great popup book.

The slider is used to control the intensity.

Profiting

It works great. Even the kids now ask us how we feel from time to time.

Notes linking here