NodeMCU and light detector – part 2: failing is an option

We need something to see the state. I think the best way is to display icon in the taskbar. It must work under Windows 10 and Ubuntu/Mint.
To build GUI I will try and use wxPython. Sadly their bindings are for Python 2.7 not 3.x. I found wxPython Phoenix Project but for now lets stick with official release.
Spolier: we gonna fail – see running section. This is why code is dirty and nasty – no reason for refactor.

Planing

When app starts it must ask sensor for current state and display a proper icon. No big deal.
It must listen to events and change icon accordingly.
I split app into 3 files. First is message.py where we start a server, ask for start state and have special function start_server where all nasty things happen (dual binding). Later about it.
Another file is view.py where GUI is kept. Last is main.py where we start modules and endless loop.
I mention dual binding, this is a nasty way of joining the server with the view. Proper way should base on queue or some event handler and not tight coupling. But for this case it is ok.

View

View should attach an icon to the taskbar, add ‘Exit’ to menu and expose function to change the icon.

import wx

__author__ = 'kosci'


class TaskBarIcon(wx.TaskBarIcon):
    def __init__(self, frame):
        self.icons = {
            'unknown': wx.Bitmap('img/unknown.png'),
            'detect.light': wx.Bitmap('img/busy.png'),
            'detect.dark': wx.Bitmap('img/empty.png')
        }
        self.frame = frame
        super(TaskBarIcon, self).__init__()
        self.set_icon('unknown')

    def CreatePopupMenu(self):
        menu = wx.Menu()
        item = wx.MenuItem(menu, -1, 'Exit')
        menu.Bind(wx.EVT_MENU, self.on_exit)
        menu.AppendItem(item)

        return menu

    def set_icon(self, icon):
        icon = wx.IconFromBitmap(self.icons[icon])
        self.SetIcon(icon, 'WC bulb')

    def on_exit(self, event):
        self.socket.shutdown()
        wx.CallAfter(self.Destroy)
        self.frame.Close()

It create three resources, image for unknown, busy and empty state.
Next we create a context menu with Exit and attach handler. Handler also closes the socket for server.
Finally we have a function to change current icon.

class App(wx.App):
    def OnInit(self):
        frame=wx.Frame(None)
        self.SetTopWindow(frame)
        self.task_bar_icon = TaskBarIcon(frame)

        return True

    def changeIcon(self, event):
        print (event)
        if event == "detect.light":
            self.task_bar_icon.set_icon('detect.light')
        elif event == "detect.dark":
            self.task_bar_icon.set_icon('detect.dark')
        elif event == "unknown":
            self.task_bar_icon.set_icon('unknown')

    def set_socket(self, socket):
        self.task_bar_icon.socket = socket

Class App is a main GUI entry point. See that we are not using any frame, we only instance TaskBarIcon. We have a changeIcon function that calls change function from taskbar. Last function is set_socket. We gonna need it to pass socket from server.

Message

import SocketServer
import threading
import json
import socket

__author__ = 'kosci'


class MessageHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        data = self.request[0].strip()
        event = None
        try:
            message = json.loads(data)
            if 'protocol' in message \
                    and message['protocol'] == "iot:1" \
                    and 'node' in message \
                    and message['node'] == self.server.node_name:
                event = message['event']
        except ValueError:
            pass

        if event:
            self.server.app.changeIcon(event)


class ThreadedUDPServer(SocketServer.ThreadingMixIn, SocketServer.UDPServer):
        pass

Standard ServerSocker handler and instance. Now embrace very bad code:

def start_server(host, port, node_name, app):
    udpserver = ThreadedUDPServer((host, port), MessageHandler)
    udpserver.app = app
    udpserver.node_name = node_name
    app.set_socket(udpserver)
    udp_thread = threading.Thread(target=udpserver.serve_forever)
    udp_thread.start()

Uff sorry for this. Lets see what and why. We are instancing a server, attaching App (GUI) to it. Next we add socket to App and starting server thread. Nasty, dirty and do not do it at home. This is strong/tight coupling. It is good idea to avoid this. But this app is a prototype so it may be like this.

def current_wc_state():
    address = ('192.168.1.255', 5053)
    packet = {
        "protocol": "iot:1",
        "node": "computer",
        "event": "state",
        "targets": [
            "light-wc"
        ]
    }

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    msg = json.dumps(packet)
    s.sendto(msg.encode(), address)
    (data, ip) = s.recvfrom(1024)
    state = "unknown"
    try:
        message = json.loads(data)
        if message['protocol'] == "iot:1":
            state = message['response']
    except:
        pass

    return state

This one checks for current sensor state. It send packet and awaits for a response.

Main

#!/usr/bin/python
# -*- coding: utf-8 -*-

import view
import message

HOST = ''
PORT = 5053
NODE = "light-wc"


def main():
    state = message.current_wc_state()
    app = view.App(False)
    message.start_server(HOST, PORT, NODE, app)

    app.changeIcon(state)
    app.MainLoop()


if __name__ == '__main__':
    main()

Smallest and cleanest part of code. Initialize wx app, call start_server, change icon and starts wx loop.

Running

I’m working on Windows 10 but I have to make sure that this app works on Linux Mint/Ubuntu.
On my machine it is working perfectly:
light_detector_win

But on Linux it is not! I used virtual machine to check against Mint 18. App starts, displays an icon and even can change it once. Sadly after that an error appears in console:

GLib-CRITICAL **: Source ID 55 was not found when attempting to remove it

And changing icon do not work any more.
After some googling seems that it is case where GLib 2.40.0 introduced the following change:

   - g_source_remove() will now throw a critical in the case that you
     try to remove a non-existent source.  We expect that there is some
     code in the wild that will fall afoul of this new critical but
     considering that we now reuse source IDs, this code is already
     broken and should probably be fixed.

This problem is with wxPython. We cannot do anything about this 😦 I will try and look some more but I strongly consider dropping wx and using PyQt.

Summary

Well this time we were close to success but sadly we failed. Time to see what PyQt can offer. Still code works on Windows 🙂

Advertisements

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