Node One – a multi-purpose node

Today we combine the power of HLK-PM01, PIR HC SR-501, DHT11 and a light detector.
Together they create a nice looking multipurpose Node One. I wanted to do it for some time but the last piece was missing. I didn’t have an idea how to create a box for components.
Few days ago I found a perfect case named Z27. It has all I wanted, socket, plug and enough space for NodeMCU v3.
Python code @ GitHub
LUA code @ GitHub

Planing

 

Z27 is a very nice box with a nice amount of space. And what is also important it does not take a socket, it has in and out and between our node should fit:)
Let’s think how to put all sensors into it along with the power converter and NodeMCU v3. Lots of parts πŸ™‚ And lots of fun. We may try and fit them like this:

We need to cut small holes for DHT and foto-resistor and larger for PIR. But this is some plan. We will see how it goes and learn as much as possible, so the next version wouldn’t suffer from same mistakes.

We need to mount all devices on some kind of board. I have universal one and I’m gonna use it. Let’s mount it on the bottom.
I cut a board and fit it into the case. This is our base. Next, we need to add the power supply for our devices and connector for 230V.

Let’s think what GPIOs we need from NodeMCU. For sure we need to power it, so its VIN and GND. That is two pins but the smallest connector I have has a width of 4 so it must be πŸ™‚
Next, we have sensors and they require 3 GPIOs, D2, D4 and D5.Β  We will use 8 sockets connector, this will make it more stable and allows us to add later something to D4 and D6.
Final structure with connectors for NodeMCU v3 board:

What now? I think DHT11 is easiest to plug. I’m thinking about adding it at the bottom of the box. It must be outside. So let’s cut a hole and add a connector. As a cutter, we will use a hot nail

We have a hole but nothing to connect to. I have a nice 4 pin connector but it can be mounted only vertically. We need a horizontal mounting and we will use dirty trick to mount it the board. First, we solder the 4 pins to the board and connector vertically to it. Lastly, we need to trim pins.

The problem is with a lousy fitting sensor into a connector. It will come out easily. Maybe I will put some solder on pins? It would make them thicker and sensor gets a better hold.

Ok, time to wire it and see if DHT works. Let’s plan our connections:

and wire it:

There is plenty of space on the board, it looks promising.

Let’s connect to the power source and see what is going to happen…

Uploading the software

We do not have a hole for USB cable so we need to get node board from the box.
To be sure that our code works, I got it from git, copied parameters.dist.lua to parameters.lua and parameters-device.dist.lua to parameters-device.lua.Β 
Next in parameters-device I set a name and in parameters I set access point credentials.
With this setup completed and we can work on node’s capabilities. File main.lua is a place for our code. This file is launched automatically after whole init routine.
We have only DHT so just include modules, register handler and start the server.

print ("core ready")

network_message = require "network_message"
server_listener = require "server_listener"

send_socket = net.createConnection(net.UDP, 0)

dht11 = require "dht11"
dht11_handler = require "dht11_handler"

mydht = dht11(5, send_socket, 5000)
a = mydht:get_readings()
print(a['humi'], a['temp'])
dht_handler = dht11_handler(mydht)

server_listener.add("dht", dht_handler)
server_listener.start(PORT)

We start DHT with a broadcast option. Our interval is 5 seconds. So each 5s we will get readings. I left a print to see if this is working, later we will remove it.

We are almost ready for the first run. As we start node from USB, we need to get power to DHT, one wire additionally mounted takes care of it.
Code uploaded, NodeMCU hooked to a board, wired VU to +5V on DHT (via separate cable), USB in socket and … connection lost.
What the ..?

It took me a while but I found out. We are making a ‘power-loop’. Power from USB goes to VU pin, from there goes go DHT and from there goes to.. VIN pin!
It is my guess that this cause a failure. To confirm it let’s wire it as it should be, without USB.

We are ready for another try but without USB we cannot debug πŸ™‚ So Python to the rescue. The script that listens to UDP messages and prints content.

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
s.bind(('', 5053))

print("waiting for messages")
while 1:
    try:
        message, address = s.recvfrom(1024)
        print("Message from %s: %s" % (address, message.decode()))
    except:
        raise

Node plugged into power and what have we here…

Message from ('192.168.1.107', 4096): {"chip_id":425761,"protocol":"iot:1","node":"node-kitchen","parameters":{"humi":54,"temp":24},"targets":["ALL"],"event":"dht.status","response":""}

Hurray, it is working! But such way of looking at data is not efficient πŸ˜€ Let’s display it in a more readable way.
And the problem with USB is something we need to fix but probably in version 2.

Displaying data

As our main control node is still under development we will use an HD44780 based LCD and Raspberry Pi.

I still have aΒ  20×4 LCD connected to Raspberry Pi. We have one source for data so we will display it in a simple table.
To do this I will use CharLCD and LCD manager modules (see top right for more info). CharLCD is a module to handle HD44780 displays and LCD manager gives us simple widgets and more abstract way of displaying and positioning text.

I’m skipping how to connect the display to the RPi, if you want to read about it, click here.

To handle messages from sensor we will use handlers, similar to what we are doing in NodeMCU.

Ok. So let’s begin with libraries and a table to display. I created a module char_control_node in the Doton root and there the main file:

from charlcd.buffered import CharLCD
from charlcd.drivers.i2c import I2C
from lcdmanager import manager
from lcdmanager.widget.label import Label

i2c_20x4 = I2C(0x3b, 1)
i2c_20x4.pins = {
    'RS': 6,
    'E': 4,
    'E2': None,
    'DB4': 0,
    'DB5': 1,
    'DB6': 2,
    'DB7': 3
}

lcd = CharLCD(20, 4, i2c_20x4, 0, 0)
lcd.init()

We import buffered CharLCD, a driver for i2c, LCD Manager for CharLCDs and a label widget. Buffered version has, like a name says, a buffer, we write to it and calling flush sends content to the display.
Next, we set i2c connection, pass it to CharLCD together with width and height and we also hide a cursor.

lcd_manager = manager.Manager(lcd)

label_temp = Label(0, 1)
label_temp.label = "Temp:"
lcd_manager.add_widget(label_temp)

label_humi = Label(0, 2)
label_humi.label = "Humi:"
lcd_manager.add_widget(label_humi)

label_kitchen = Label(5, 0)
label_kitchen.label = "Kitchen"
lcd_manager.add_widget(label_kitchen)

Here, we create a new manager for defined LCD, next we set labels and add them to the manager at given coordinates.
Finally, we need to render and flush content:

lcd_manager.render()
lcd_manager.flush()

The script gives us something like this:

What next? Export labels to a separate view, create the handler for DHT and put it all together.

Create sub-module view and in it file readings.py, in this file we will create the main view:

from lcdmanager.widget.pane import Pane
from lcdmanager.widget.label import Label


class Readings(object):
    def __init__(self, lcdmanager):
        self.pane = Pane(0, 0, 'data_summary')
        self.pane.width = lcdmanager.width
        self.pane.height = lcdmanager.height

        label_temp = Label(0, 1)
        label_temp.label = "Temp:"
        self.pane.add_widget(label_temp)

        label_humi = Label(0, 2)
        label_humi.label = "Humi:"
        self.pane.add_widget(label_humi)

        label_kitchen = Label(5, 0)
        label_kitchen.label = "Kitchen"
        self.pane.add_widget(label_kitchen)

        self.pane.add_widget(Label(6, 1, 'kitchen_temp'))
        self.pane.get_widget('kitchen_temp').label = '--'
        self.pane.add_widget(Label(6, 2, 'kitchen_humi'))
        self.pane.get_widget('kitchen_humi').label = '--'

        lcdmanager.add_widget(self.pane)

We create a pane and on this we put all our required items. Our main file becomes smaller:

from charlcd.buffered import CharLCD
from charlcd.drivers.i2c import I2C
from lcdmanager import manager
from view.readings import Readings

i2c_20x4 = I2C(0x3b, 1)
i2c_20x4.pins = {
    'RS': 6, 'E': 4, 'E2': None, 'DB4': 0, 'DB5': 1, 'DB6': 2, 'DB7': 3
}

lcd = CharLCD(20, 4, i2c_20x4, 0, 0)
lcd.init()
lcd_manager = manager.Manager(lcd)

Readings(lcd_manager)

lcd_manager.render()
lcd_manager.flush()

We still have no data on display, so it is time to create a handler and display something. I’m thinking that handlers are universal classes, we will use them now and later with TFT display. So create a new module in the project root, I called it sensors. There create new class DHTHandler. The code is rather simple but powerful :D:

from message_listener.abstract.handler_interface import Handler as HandlerInterface


class DHTHandler(HandlerInterface):
    def handle(self, message):
        if message is not None and 'event' in message and message['event'] == 'dht.status':
                self.worker.set_dht_data(
                    message['node'],
                    str(message['parameters']['temp']),
                    str(message['parameters']['humi'])
                )

To put some light on it, a handler has a constructor in wich we pass a worker. But we can pass anything, like the main view.
Handler for event dht.status calls set_dht_data from the view.
Perfect but we have no server or any other functions πŸ™‚ Do not worry, we will create them.
First quick visit in Readings view and add missing function:

    def set_dht_data(self, node, temp, humi):
        if node in self.node_name:
            self.pane.get_widget(self.node_name[node] + '_temp').label = temp
            self.pane.get_widget(self.node_name[node] + '_humi').label = humi

and new lines to init:

    self.node_name = {
        'node-kitchen': 'kitchen'
    }

We have an array of available node’s names. In function set_dht_data we check if passed node name is in our node dictionary and if yes we update temperature and humidity.
Nice but still no information how to run it, in the main file we need new imports:

from message_listener.server import Server
from iot_message.message import Message
import sys, time
sys.path.append("../")
from sensors.DHTHandler import DHTHandler

One trick is used here because DHTHandler module and current module are on the same level, they do not see each other. But we can append a path and extend our range.

Next, remove all below

lcd_manager = manager.Manager(lcd)

and put new content:

msg = Message('secondary-node')

svr = Server(msg)
svr.add_handler('20x4', DHTHandler(Readings(lcd_manager)))
svr.start()

while True:
    lcd_manager.render()
    lcd_manager.flush()
    time.sleep(1)

svr.join()

This requires some explanation.
The Server class from the message_listener module is the same thing like the server_listener in NodeMCU boilerplate. It gets messages from the net, decodes them and passes to registered handlers.
To do this correctly it requires Message component. This one is responsible for handling messages. So Server and Message are bound by the task they do πŸ™‚
Because Server is a thread we need something to keep our code running, and this is a good job for while loop with the LCD refresh. We could use another component from LCD manager to start another thread with screen refreshing but this time it is not necessary.

To my surprise, everything is working… strange feeling πŸ™‚

Second node

As I have two nodes, one as a prototype on the desk and second the one we are working on, we can add them both to display. It is simple, open view, add new labels and name to the dictionary:

self.pane.add_widget(Label(14, 1, 'my-room_temp'))
self.pane.get_widget('my-room_temp').label = '--'
self.pane.add_widget(Label(14, 2, 'my-room_humi'))
self.pane.get_widget('my-room_humi').label = '--'

Our handlers will get readings from any node and pass data to view. If we have fields for the data it will be displayed.

Motion detector

Our fundaments are solid so let’s move on. We will add motion detector. It should be mounted on the front, do let’s cut a hole πŸ™‚ but how to keep sensor attached to the front wall? With rubber band!


It is fitting inside now we need to wire it somehow. We need the power from converter and data signal to D2 pin.
To make it work we need to upload a code for it, open main.lua and add:

pir = require "pir_hcs_sr501"
pir_handler = require "pir_hcs_sr501_handler"
motion = pir(send_socket, 2)
motion_handler = pir_handler(motion)

server_listener.add("motion", motion_handler)

Good, it is working in test mode πŸ™‚
What now?
We could add a light sensor or fold everything into the magic box and see how it fit.
If we have it dismounted we can add the last sensor, it would be a waste of time to not do so.

Light sensor

It is getting tight in the box, where to mount it? I thought about mounting it on the side wall but the front wall looks better.
To cut a hole we will once again use a hot nail.

As for the wire, we will swap pin D4 to D6 because with D4 I had problems when the state changed during boot.

With all wires soldered into the main board, it looks like this:

 

And code for using this sensor:

light_sensor = require "light_detector"
light_sensor_handler = require "light_detector_handle"
light = light_sensor(send_socket, 6, 2000)
light_handler = light_sensor_handler(light)

server_listener.add("light", light_handler)

There is almost no space left inside the case. We are wasting much of it anyway. But who cares πŸ™‚

Finishing

We need to shut the box but before we need wire upper socket with power from the plug.

Packed all together, plugged to power and.. dammit no temperature and humidity but light and movement work.
Dismounting…
Ground pin from DHT didn’t hit the socket, uff nothing big. Replugged and started Node again. This time full success! Everything works.. almost.. damn, light sensor requires a calibration :/

Movement and light on LCD

We need to display information when move or the light is detected. I think the best way is just to display L or M.
Back to Python and Readings view, we need to add fields for information:

self.pane.add_widget(Label(7, 3, 'kitchen_move'))
self.pane.get_widget('kitchen_move').label = '-'
self.pane.add_widget(Label(10, 3, 'kitchen_light'))
self.pane.get_widget('kitchen_light').label = '-'

self.pane.add_widget(Label(15, 3, 'my-room_move'))
self.pane.get_widget('my-room_move').label = '-'
self.pane.add_widget(Label(18, 3, 'my-room_light'))
self.pane.get_widget('my-room_light').label = '-'

and function to handle PIR and light changes:

    def set_pir_data(self, node, status):
        if node in self.node_name:
            if status:
                self.pane.get_widget(self.node_name[node] + '_move').label = 'M'
            else:
                self.pane.get_widget(self.node_name[node] + '_move').label = ' '

    def set_light_data(self, node, status):
        if node in self.node_name:
            if status:
                self.pane.get_widget(self.node_name[node] + '_light').label = 'L'
            else:
                self.pane.get_widget(self.node_name[node] + '_light').label = ' '

We need handlers, first, let’s focus on PIR.
Handler listens for two messages:

from message_listener.abstract.handler_interface import Handler as HandlerInterface


class PIRHandler(HandlerInterface):
    def handle(self, message):
        if message is not None and 'event' in message:
            if message['event'] == 'pir.movement':
                self.worker.set_pir_data(
                    message['node'],
                    True
                )

            if message['event'] == 'pir.nomovement':
                self.worker.set_pir_data(
                    message['node'],
                    False
                )

Some changes in the main file, instance and register the handler:

view = Readings(lcd_manager)

svr = Server(msg)
svr.add_handler('dht11', DHTHandler(view))
svr.add_handler('pir', PIRHandler(view))

And failure πŸ™‚ It turned out that I had a nasty bug in the mesage_listener package. Bug fixed and… it works!

We can add a handler for the light sensor:

from message_listener.abstract.handler_interface import Handler as HandlerInterface


class LightHandler(HandlerInterface):
    def handle(self, message):
        if message is not None and 'event' in message:
            if message['event'] == 'detect.light':
                self.worker.set_light_data(
                    message['node'],
                    True
                )

            if message['event'] == 'detect.dark':
                self.worker.set_light_data(
                    message['node'],
                    False
                )

And register it in the main file:

svr = Server(msg)
svr.add_handler('dht11', DHTHandler(view))
svr.add_handler('pir', PIRHandler(view))
svr.add_handler('light', LightHandler(view))
svr.start()

Summary

We have it! The Node One with sensors and software to display data from it. Current displaying is on HD44780 based LCD but our goal is to use TFT with touch support. We should be able to reuse handlers.
This module is added to the Doton project and named char_control_node.
It was a very good lesson, about fitting parts in boxes πŸ™‚ wiring it all in such way that it could be easily dismounted.
Some problems emerged like we cannot power unit from USB socket but we should be able to fix this in v2.
We should definitely work on packing devices it in more efficient way, I have some ideas and maybe another sensor will fit inside.
Our software for displaying is also quite poor. It should auto-detect nodes when the message arrives and when started it should ask environment about the current state. Now when started it knows nothing about light and movement until one of them changes. Also when a node dies we do not know about it.
But all in all, this is a success πŸ™‚

Advertisements

4 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s