Network Accessible Mote Kit

IP_ADDR/white is the one you want to be trying. The internal server error indicates something is probably going wrong with the white() function.

It could be that your white thread is conflicting with your white method. So maybe rename one of them?

To debug the white method, you might want to simplify the code to:

@app.route("/white")
def white():
    return jsonify(message = "testing")

and then navigate to the url to check it works.

Tested, and yes, that works! :)

Great, so now you’ll want to replace it with:

@app.route("/white")
def white():
    return jsonify(message = get_status(run_animation(WhiteThread(mote))))

making sure to add the following to the top of the file:

from white_thread import WhiteThread

and rename your thread appropriately.

“rename your thread appropriately.” ?
What am I renaming to what?

In white_thread.py change:

class white(MoteThread):

to

class WhiteThread(MoteThread):

as it may be conflicting.

Very odd, click white, and font page changes from “MoteServer is currently on and in ‘Fairy Lights’ mode!” to “testing”, yet the word testing is nowhere to be found in any of the files, so far as I can see.
And the mote strips stay as they were, they don’t go to white.

I’d say that’s because you need to quit and re-run the mote_server.py with the latest changes but I guess you might have tried that already?

I tend to shut down and start over, just to eliminate any of those kind of things.

Weird, I’d only expect the “testing” text to appear on the webpage if you were using the old white function instead of the newest one.

I’ve just tried again today, the only difference being I’ve killed the server PID.
I now get the “running in ‘white’ mode!” message, but the motes are doing nothing.
(OK in other modes.)

pi@raspberrypi:~ $ sudo python3 /home/pi/FlaskApp/mote/mote_server.py
 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit)
192.168.1.2 - - [24/Jan/2018 09:39:06] "GET / HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:06] "GET /static/spectrum.css HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:06] "GET /static/site.css HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:06] "GET /static/jquery-1.9.1.js HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:07] "GET /static/spectrum.js HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:17] "GET /white HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:31] "GET /fairy HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:39:44] "GET /rainbow HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:40:04] "GET /white HTTP/1.1" 200 -
192.168.1.2 - - [24/Jan/2018 09:41:01] "GET /rainbow HTTP/1.1" 200 -

Also, I notice mote_server.py is running twice, is that correct?

pi@raspberrypi:~ $ ps -ef | grep python
root      2137  1930  0 09:38 pts/0    00:00:00 sudo python3 /home/pi/FlaskApp/mote/mote_server.py
root      2141  2137 28 09:38 pts/0    00:00:47 python3 /home/pi/FlaskApp/mote/mote_server.py

Your white_thread.py file should look something like this:

from mote_thread import MoteThread

class WhiteThread(MoteThread):
    def __init__(self, mote):
        self.mote = mote
        MoteThread.__init__(self, name="white")

    def run(self):
        while not self.stopped():
            for channel in range(4):
                for pixel in range(16):
                    self.mote.set_pixel(channel + 1, pixel, 255, 255, 255)
            self.mote.show()
            self.wait(0.1)

Strictly speaking, for just setting the motes to white, we don’t need to be using Threading, but it’s a good starting point for doing more complicated effects.

That’s fixed it,
Hopefully I have a better idea how to get further with it myself now.

I’ve made a shelf holder from a 220mm length of 50mm PVC trunking, by cutting it from |_| to |_ shape, and adding some small holes through which I can cable tie the mote to the holder.

Using the weight of enough books, it’s held in place on the shelf, although I might use some Velcro strips for a better fix.

Thanks for all your help. :)

1 Like

Hi,

Having a play with this again in lockdown, trying to add another option on the Mote Server menu.



pi@pi_mote_lights:~/FlaskApp/mote $ sudo python mote_server_NEW.py
Traceback (most recent call last):
File "mote_server_NEW.py", line 10, in <module>
from sens_thread import SensThread
File "/home/pi/FlaskApp/mote/sens_thread.py", line 227, in <module>
run()
NameError: name 'run' is not defined

Here’s the full code:

from colorsys import hsv_to_rgb
from flask import Flask, render_template, jsonify, make_response, redirect, url_for
import datetime
from mote import Mote
from rainbow_thread import RainbowThread
from soft_cheer_thread import CheerThread
from slave_thread import SlaveThread
from fairy_thread import FairyThread
from white_thread import WhiteThread
from sens_thread import SensThread
from manual_thread import ManualThread, TransitionClass
from queue import Queue

app = Flask(__name__)

mote = Mote()
mote.configure_channel(1, 16, False)
mote.configure_channel(2, 16, False)
mote.configure_channel(3, 16, False)
mote.configure_channel(4, 16, False)

mote_on = True
current_mode = "manual"

mode_nice_names = {
    "manual"  : "Manual",
    "rainbow" : "Rainbow",
    "cheer"   : "CheerLights",
    "disco"   : "Disco",
    "white"   : "White", 
    "sens"    : "Sens",       <<<<<<<<<< New Added Function
    "fairy"   : "Fairy Lights",
    }

channel_colors = {
    1 : "FF0000",
    2 : "00FF00",
    3 : "0000FF",
    4 : "FFFFFF" }

animation_thread = None

manual_queue = Queue()

def set_mode(mode):
    global current_mode
    current_mode = mode

def get_status(error=False):
    if (mote_on):
        if not error:
            return "MoteServer is currently on and in '"+mode_nice_names[current_mode]+"' mode!"
        else:
            return "MoteServer is current on but hit a snag running '"+mode_nice_names[current_mode]+"' :("
    return "MoteServer is currently off :("

def mote_off():
    global mote_on
    global animation_thread
    
    if animation_thread != None:
        animation_thread.join()
        animation_thread = None
    
    mote.clear()
    mote.show()
    mote_on = False

def init_mote():    
    if mote_on:
        run_animation(ManualThread(mote, manual_queue), True)
        for channel in range(1,5):
            manual_queue.put(TransitionClass(channel, channel_colors[channel]))

def stop_animation():
    global animation_thread
    if animation_thread != None:
        animation_thread.join()
        animation_thread = None
        return True
    return False

def run_animation(thread, force=False):
    global animation_thread
    exception = False

    try:
        # Don't re-init the same mode
        if force or current_mode != thread.name:
            stop_animation()
        
            if mote_on:
                animation_thread = thread
                animation_thread.start()
                set_mode(animation_thread.name)
    except:
        animation_thread = None
        exception = True

    return exception

@app.route("/")
def root():
    templateData = {
      'on'     : 'checked' if mote_on else '',
      'mode'   : current_mode,
      'status' : get_status()
      }

    return render_template('home.html', **templateData)

@app.route("/manual")
def manual():
    if current_mode != "manual":
        init_mote()

    return jsonify(message = get_status())
        
@app.route("/configure_manual")
def configure_manual():
    if current_mode != "manual":
        init_mote()
    
    return render_template('manual.html')
    
@app.route("/getColor/<int:channel>/")
def getColor(channel):
    return jsonify(color = channel_colors[channel])

@app.route("/setColor/<int:channel>/<string:color>")
def setColor(channel, color):
    response = "Channel " + str(channel) + ". "
    try:
        channel_colors[channel] = str(color)
        if mote_on:
            # Add color to queue
            manual_queue.put(TransitionClass(channel, color))
    except:
        response = response + "There was an error setting the colour."
        
    return jsonify(message = response)

@app.route("/rainbow")
def rainbow():
    return jsonify(message = get_status(run_animation(RainbowThread(mote))))

@app.route("/cheer")
def cheer():
    return jsonify(message = get_status(run_animation(CheerThread(mote))))

@app.route("/disco")
def disco():
    # prevent a second instance as it raises socket error
    if current_mode != "disco":
        return jsonify(message = get_status(run_animation(SlaveThread(mote, "192.168.0.14", 7777))))
    else:
        return jsonify(message = get_status())
    
@app.route("/fairy")
def fairy():
    return jsonify(message = get_status(run_animation(FairyThread(mote))))
    
    
@app.route("/white")
def white():
    return jsonify(message = get_status(run_animation(WhiteThread(mote))))

@app.route("/sens")
def sens():
    return jsonify(message = get_status(run_animation(SensThread(mote))))    

@app.route("/on")
def on():
    global mote_on
    mote_on = True
    init_mote()

    return jsonify(message = get_status())

@app.route("/off")
def off():
    mote_off()

    return jsonify(message = get_status())

@app.errorhandler(404)
def not_found(error):
    return make_response(jsonify({'error': 'Not found'}), 404)

if __name__ == "__main__":
    init_mote()
    app.run(host='0.0.0.0', port=80, debug=False)
    # When app terminated:
    mote_off()

sens_thread.py

"""runs a sensory garden"""
from random import randrange, choice, shuffle, random
import time
from math import sqrt
from mote import Mote
from mote_thread import MoteThread
from colorsys import hsv_to_rgb
import numpy
import random
#from numpy import append, linspace

class SensThread(MoteThread):
    class ColorConstant:
        """holds this colour"""
        constant_col = [0,0,0]
        def __init__(self, constant_col):
            self.constant_col = constant_col

        def get(self, coord):
            return self.constant_col

        def __repr__(self):
            return "Constant " + str(self.constant_col)

    class ColorChannelConstant:
        """one colour per channel"""
        constant_cols = []
        def __init__(self, rand_cols):
            for i in range(4):
                self.constant_cols.append(rand_cols.get(None))

        def get(self, coord):
            return self.constant_cols[coord[0]-1]

    class ColorChequerboard:
        """alternating"""
        cols = []
        def __init__(self, rand_cols):
            self.cols = [rand_cols.get(None), rand_cols.get(None)]

        def get(self, coord):
            index = (coord[0]+coord[1])%2
            return self.cols[index]

    class ColorRandom:
        """avoids repetition"""
        prev_pattern = -1
        values = [50, 100, 150]
        patterns = [[1,1,0], [1,0,1], [0,1,1], [0,0,1], [0,1,0], [1,0,0]]

        def get(self, coord):
            # don't want colours too similar, and don't want black:
            pattern_index = randrange(len(self.patterns))
            if pattern_index == self.prev_pattern:
                return self.get(coord)
            self.prev_pattern = pattern_index
            pattern = self.patterns[pattern_index]
            c = [pattern[0]*choice(self.values), pattern[1]*choice(self.values), pattern[2]*choice(self.values)]
            return c

    def remap(v, in_range, out_range):
        """rescales a value from one range to another"""
        in_v_norm = (in_range[1]-v)/(in_range[1]-in_range[0])
        clamped_norm = min(1, max(0, in_v_norm))
        return out_range[0] + clamped_norm*(out_range[1] - out_range[0])
        
    def lerp_i(a, lhs, rhs):
        """integer lerp"""
        return int((rhs-lhs)*a + lhs)
        
    def lerp_cols(a, lhs, rhs):
        """blends between lhs and rhs"""
        return [lerp_i(a, lhs[0], rhs[0]), lerp_i(a, lhs[1], rhs[1]), lerp_i(a, lhs[2], rhs[2])]

    class ColorFade:
        """handles fades"""
        start_col = [0,0,0]
        end_col = [1,1,1]
        def __init__(self, start_col, end_col):
            self.start_col = start_col
            self.end_col = end_col

        def get(self, coord):
            return lerp_cols(self.get_alpha(coord), self.start_col, self.end_col)
        
        def __repr__(self):
            return self.__class__.__name__ + " " + str(self.start_col) + " > " + str(self.end_col)
            
    class ColorChannelFade(ColorFade):
        """fades between colours on one channel"""
        def get_alpha(self, coord):
            return coord[1]/15.0
        
    class ColorGlobalFade(ColorFade):
        """fades across all pixels, chained end-to-end"""
        def get_alpha(self, coord):
            return ((coord[0]-1)*16+coord[1])/63.0
        
    class ColorPixelFade(ColorFade):
        """fades between pixels on the same row"""
        def get_alpha(self, coord):
            return (coord[0]-1)/3.0

    class ColorHorizCenterFade(ColorFade):
        """1 in the middle, 0 at the edges"""
        def get_alpha(self, coord):
            return abs(8-coord[1])/8.0
        
    class ColorRadialFade(ColorFade):
        """circular"""
        def get_alpha(self, coord):
            norm_to_center = [abs(1.5-(coord[0]-1))/1.5, abs(7.5-coord[1])/7.5]
            dist_to_center = sqrt(norm_to_center[0]*norm_to_center[0] + norm_to_center[1]*norm_to_center[1])
            alpha = remap(dist_to_center, [0.1, 1.2], [0, 1])
            return alpha
        
    def pattern_race():
        fills = [0,0,0,0]
        completed = 0;
        while completed < 4:
            # pick an incomplete channel
            c = randrange(len(fills))
            if fills[c] == 16:
                continue
            # add to fills, set the pixel
            yield (c+1, fills[c])
            fills[c] += 1
            if fills[c] >= 16:
                completed += 1
            
            
    def pattern_scatter():
        pixels = [(c,p) for c in range(1,5) for p in range(16)]
        shuffle(pixels)
        for p in pixels:
            yield (p[0], p[1])
            
    def pattern_floodfill():
        """fills each channel in order"""
        for channel in range(1,5):
            for pixel in range(16):
                yield (channel, pixel)

    def pattern_horizflood():
        """fills by pixel and then by channel"""
        for pixel in range(16):
            for channel in range(1,5):
                yield (channel, pixel)

    def pattern_horizsnakeflood():
        """zipping back and forth"""
        for pixel in range(16):
            snakedir = range(1,5) if (pixel%2)==0 else range(4,0,-1)
            for channel in snakedir:
                yield (channel, pixel)
                
    def pattern_snakeflood():
        """fills each channel in order"""
        for channel in range(1,5):
            snakedir = range(16) if (channel%2)==0 else range(15,-1,-1)
            for pixel in snakedir:
                yield (channel, pixel)
                
    def pattern_reverse(other_pattern):
        """sets each pixel black in reverse order of other_pattern"""
        other_reversed = reversed(list(other_pattern))
        for update in other_reversed:
            yield (update[0], update[1])

    def run():
        """let's goooo"""
        mote = Mote()

        mote.configure_channel(1, 16, False)
        mote.configure_channel(2, 16, False)
        mote.configure_channel(3, 16, False)
        mote.configure_channel(4, 16, False)

        rand_cols = ColorRandom()
        
        patterns = [
            pattern_floodfill,
            pattern_scatter,
            pattern_race,
            pattern_horizflood,
            pattern_horizsnakeflood,
            pattern_snakeflood,
        ]
        black = lambda: ColorConstant([0,0,0])
        colors = [
            ColorRandom,
            lambda: ColorConstant(rand_cols.get(None)),
            lambda: ColorChannelConstant(rand_cols),
            lambda: ColorChannelFade(rand_cols.get(None), rand_cols.get(None)),
            lambda: ColorGlobalFade(rand_cols.get(None), rand_cols.get(None)),
            lambda: ColorPixelFade(rand_cols.get(None), rand_cols.get(None)),
            lambda: ColorHorizCenterFade(rand_cols.get(None), rand_cols.get(None)),
            lambda: ColorRadialFade(rand_cols.get(None), rand_cols.get(None)),
            lambda: ColorChequerboard(rand_cols),
        ]
        last_col_template = None
        last_pattern_template = None
        
        while True:
            pattern_template = choice(patterns)
            if pattern_template == last_pattern_template:
                continue
            pattern = pattern_template() if random() < 0.5 else pattern_reverse(pattern_template())

            color_template = black if (random() < 0.33 and last_col_template is not None) \
                else choice(colors)
            if color_template == last_col_template:
                continue
            color = color_template()

            for update in pattern:
                c = color.get(update)
                prev_c = mote.get_pixel(update[0], update[1])
                for step in range(8):
                    step_c = lerp_cols(step/7.0, prev_c, c)
                    mote.set_pixel(update[0], update[1], step_c[0], step_c[1], step_c[2])
                    mote.show()
                    time.sleep(0.03)

            last_col_template = color_template
            last_pattern_template = pattern_template
run()

Thank you.

Three and a half years on, this is still running nicely on my shelves.

Time now though to retire the old Pi Zero, and move on to a Pi3a

USB Mote is running just fine, now to install the Flask server side of things.
I have it all recently backed-up, and documented from 2020, so it should be easier this time around.

1 Like