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’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.)
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.
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()
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.