Doton the Control Node – part 3, new GUI and window manager

We still have the weather widget to finish. But before that, I’m thinking about big changes in tile layout. Why?
Because tiles are rectangles and they cannot be used in vertical mode, square tiles would be much better.
This should give us a free space on top and bottom of the display for a nice functionality in near future (do not forget that out TFT is a touch screen) πŸ™‚
Let’s get to work!
Part 1
Part 2

Source @ GitHub


Let’s see what can we do with tile size. Seems not much, size 105×105 is the maximum we can have.
See the new layout, 6 tiles and 2 areas for future touch:

Bah, and now refactor all tiles…

There is a huge gap between columns but I have no idea what can be done with it.

There are few annoying things like we need to set tile position in pixels. It should be more friendly, so it is a time for:

Window manager

This service will keep information about pages and widgets on the page. Yes pages, our goal is to have few pages and up/down or left/right touch scroll.
What such manager must have? Ability to add a widget at (x, y), that for sure. It should take care of calling a draw function on widget so our main loop is free from this.

I just wanted do upgrade a weather widget…Do not cry, write! πŸ˜€

Ok Let’s begin, first the constructor:

class WindowManager(threading.Thread):
    """Window Manager"""
    def __init__(self):
        self.widgets = {} = True

When I wrote the add_widget function and position recalculation I had to refactor all widgets. We were taking pos_x and pos_y as a list but it is not a correct way. To stay with it I would have to use many ifs and it won’t be extendable. So all positions will be refactored to list of points.
Th constructor in Widget abstract is now:

class Widget(metaclass=abc.ABCMeta):
    """Widget abstract"""
    def __init__(self, coords, lcd):
        self.coords = coords
        self.lcd = lcd

And constructor for Node One is:

    def __init__(self, coords, lcd, font):
        super().__init__(coords, lcd)

and for Openweather:

    def __init__(self, coords, lcd, fonts):
        super().__init__(coords, lcd)

With all this we are able to add widgets to manager in such way:

window_manager = WindowManager()
window_manager.add_widget('node-kitchen', NodeOneWidget([(0, 0)], lcd_tft, FONTS['24x42']))
window_manager.add_widget('node-my-room', NodeOneWidget([(1, 0)], lcd_tft, FONTS['24x42']))
        [(0, 106), (134, 106)],

And we can write the first version of the add_widget function:

    def add_widget(self, name, widget, page=0):
        """add widget to grid, calculate (x,y)"""
        position = []
        for coords in widget.coords:
            position.append((coords[0]*134, coords[1]*106))
        widget.coords = position
        self.widgets[name] = widget


To see how it looks we need to add painting, as our class is a Thread we will do it in run function:

    def run(self):
        """main loop - drawing"""
        for widget in self.widgets:
            for widget in self.widgets:

Quick check with

in the main file and it is working.

What next? Colour changing.

    def set_widget_color(self, name, key, value):
        """change colour"""
        self.widgets[name].colours[key] = value

and call:

window_manager.set_widget_color('node-my-room', 'background', (0, 255, 255))

Almost done. Add get_widget to the manager, use it in OpenweatherWorker, in HandlerDispatcher change WIDGETS to window_manager.widgets, remove drawing from, remove lcd_tft from widget constructor, add it to manager constructor and pass it to the widget in draw functions. This will separate widget from the display.
Finally, start the WindowManager thread.
And fail πŸ™‚

There is a bug with value refreshing. If the weather is read during painting it won’t refresh. We need to postpone value assignment to after drawing. With this we are green πŸ™‚

There is one more aspect that breaks programming rules, we shouldn’t set widget position in its constructor. This is something that should be done in add_widget function and passed to the widget like lcd variable.
We want to separate components as much as possible.
So window manager needs a widget holder class. This class would know a position and what is on this position. With this, we can free a widget from unnecessary information.
Another large but necessary refactor done.

To add new widgets use:

window_manager.add_widget('node-kitchen', [(0, 0)], NodeOneWidget(FONTS['24x42']))
window_manager.add_widget('node-my-room', [(1, 0)], NodeOneWidget(FONTS['24x42']))
window_manager.add_widget('openweather', [(0, 1), (1, 1)], OpenweatherWidget(FONTS))

What we have next?
Ahh, yes, time to teach manager something about pages. Idea is to have many pages with widgets and scroll between them. But we will start from one page πŸ™‚

Let’s create a Page class with widgets dictionary, and in WindowManager add pages list:

class Page(object):
    """Page class"""
    def __init__(self):
        self.widgets = {}

And in manager’s init, replace self.widget with

    self.pages = [Page()]
    self.active_page = 0

Now we need to change all other functions πŸ™‚ We won’t create now a fully working page system just one page is enough. It is not a time to do this.

Finally, time for Jenkins!

Stop! Jenkins time

Hurray, a bug! Something more interesting then, run, fix, run routine. What bug?
A violation plugin has a problem with paths. Until now I have a module directory in root with files and now files are in the root folder.
In GfxLCD my tox.ini was like that:

commands= nosetests --with-xunit --xunit-file=junit-{envname}.xml gfxlcd/tests
    rm flake8-{envname}.log -f
    /bin/bash -c "flake8 --output-file=flake8-{envname}.log gfxlcd || :"
    /bin/bash -c "pylint gfxlcd > pylint-{envname}.log || :"

In this project, I replaced gfxlcd with .
Very bad idea…
The chart was shown but there was no access to files. In generated log files with paths that started with ./ and Jenkins went crazy. Changing calls in tox.ini to:

commands=rm flake8-{envname}.log -f
    /bin/bash -c "flake8 | sed 's|^./||' > flake8-{envname}.log || :"

Fixed a problem with flake8.

What about pylint? Few google results later and I know! It requires that everything is a package and has a file.
We do not have this in root and we shouldn’t but let’s try… It is not crashing but it is not checking…let’s ignore it for now.

Baka me!

Yeh, I deleted API key from posts but not from source code πŸ™‚ What a lame thing to do. Old key invalidated.
But this shows that we need a config file. The template for config is in config.ini.dist:


Do we need any fancy configuration parser? I do not think so. So let’s write a simple one:

from configparser import ConfigParser

class Config(object):
    def __init__(self, file="config.ini"):
        self.file = file
        self.config = ConfigParser()

    def get(self, key):
        return self.config.get('general', key)

And use it in

from service.config import Config
msg = Message(config.get('node_name'))
workerHandler.add('openweather', OpenweatherWorker(config.get('openweather_apikey'), window_manager.get_widget('openweather')), 5)

Uf, stupidity corrected πŸ™‚ No more private keys in source code πŸ™‚


This time without fancy images. Pure heavy code based post. We separated widgets from LCD and position. This is taken care by the window manager. It also took drawing loop from the main file.
GUI has changed very, very much. We have square tiles and different size.
Next, we added a config file so our private keys can be safely kept.
And I just wanted to improve weather widget, well maybe next time πŸ™‚



Leave a Reply

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

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

Google+ photo

You are commenting using your Google+ 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 )


Connecting to %s