- see,
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
clogged. Using some cleaning filament made it work great again.
After some digging, the problem was due to the nozzle beingAssembling
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
- sudo apt install arduino
- arduino
- files -> preferences -> add the board configuration file http://arduino.esp8266.com/stable/package_esp8266com_index.json
- tools -> boards -> board manager -> search esp8266 and install
- tools -> boards -> lolin(wemos d1) r2 & mini
- C-S-i -> install neopixel
- C-S-i -> install arduino json (see later my custom program)
-
plug the wemos d1
$ dmesg [ 6336.833294] usb 1-2.1.4.3: new full-speed USB device number 18 using xhci_hcd [ 6336.934752] usb 1-2.1.4.3: New USB device found, idVendor=1a86, idProduct=7523, bcdDevice= 2.54 [ 6336.934768] usb 1-2.1.4.3: New USB device strings: Mfr=0, Product=2, SerialNumber=0 [ 6336.934775] usb 1-2.1.4.3: Product: USB2.0-Ser! [ 6336.943668] ch341 1-2.1.4.3:1.0: ch341-uart converter detected [ 6336.944489] ch341-uart ttyUSB0: break control not supported, using simulated break [ 6336.944684] usb 1-2.1.4.3: ch341-uart converter now attached to ttyUSB0
ls -la /dev/ttyUSB0
crw-rw----+ 1 root dialout 188, 0 Oct 21 09:15 /dev/ttyUSB0
sudo adduser sam dialout
-
tools -> port -> /dev/ttyUSB0
-
C-u when the program is ready sometimes, it does not succeed in uploading the program, try again or use another usb port
Build options changed, rebuilding all Archiving built core (caching) in: /tmp/arduino_cache_952883/core/core_esp8266_esp8266_d1_mini_xtal_80,vt_flash,exception_disabled,stacksmash_disabled,ssl_all,mmu_3232,non32xfer_fast,eesz_4M2M,ip_lm2f,dbg_Disabled,lvl_None____,wipe_none,baud_921600_97c7015af2bc8fe6734cc71dd43401e2.a Executable segment sizes: ICACHE : 32768 - flash instruction cache IROM : 269584 - code in flash (default or ICACHE_FLASH_ATTR) IRAM : 27501 / 32768 - code in IRAM (IRAM_ATTR, ISRs...) DATA : 1508 ) - initialized variables (global, static) in RAM/HEAP RODATA : 1192 ) / 81920 - constants (global, static) in RAM/HEAP BSS : 25944 ) - zeroed variables (global, static) in RAM/HEAP Sketch uses 299785 bytes (28%) of program storage space. Maximum is 1044464 bytes. Global variables use 28644 bytes (34%) of dynamic memory, leaving 53276 bytes for local variables. Maximum is 81920 bytes. esptool.py v3.0 Serial port /dev/ttyUSB0 Connecting.... Chip is ESP8266EX Features: WiFi Crystal is 26MHz MAC: c4:5b:be:62:c2:db Uploading stub... Running stub... Stub running... Changing baud rate to 460800 Changed. Configuring flash size... Auto-detected Flash size: 4MB Compressed 303936 bytes to 221951... Writing at 0x00000000... (7 %) Writing at 0x00004000... (14 %) Writing at 0x00008000... (21 %) Writing at 0x0000c000... (28 %) Writing at 0x00010000... (35 %) Writing at 0x00014000... (42 %) Writing at 0x00018000... (50 %) Writing at 0x0001c000... (57 %) Writing at 0x00020000... (64 %) Traceback (most recent call last): File "/home/sam/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/tools/upload.py", line 66, in <module> esptool.main(cmdline) File "/home/sam/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/tools/esptool/esptool.py", line 3604, in main operation_func(esp, args) File "/home/sam/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/tools/esptool/esptool.py", line 2987, in write_flash esp.flash_defl_block(block, seq, timeout=DEFAULT_TIMEOUT * ratio * 2) File "/home/sam/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/tools/esptool/esptool.py", line 113, in inner return func(*args, **kwargs) File "/home/sam/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/tools/esptool/esptool.py", line 761, in flash_defl_block self.check_command("write compressed data to flash after seq %d" % seq, File "/home/sam/.arduino15/packages/esp8266/hardware/esp8266/3.0.2/tools/esptool/esptool.py", line 423, in check_command raise FatalError.WithResult('Failed to %s' % op_description, status_bytes) esptool.FatalError: Failed to write compressed data to flash after seq 8 (result was C100) esptool.FatalError: Failed to write compressed data to flash after seq 8 (result was C100)
-
C-S-M to debug the program
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
- 3D print maybe list (braindump)
- IOT heart again, with micropython (braindump)
- more clever night light (braindump)
- why does my model shift (braindump)