Hello and welcome to my seventh update on the Universal LED Animator. In those closing days work goes very fast (too fast sometimes, I'd say), and problems that have been left for later have to be dealt with. In this project, one such long-term problem has been talking to multiple peripherals (animators) simultaneously from a single central unit (the Beagle Bone Wireless). It has been solved now, chiefly thanks to me switching from BLE GATT SDK to a much more beginner-friendly Python library, namely Adafruit's Bluefruit LE. This is what I get in the Web interface at the moment:
It's kept deliberately default-ugly for a while, as I need to figure out a proper way of working with IFrames. It will become stylish with the help of some CSS in one of the future updates in the next few days.
Read on to find out how Bluefruit helped me out and what I learned about JavaScript.
Iterable BLE devices
In order to make my code able to work with more than one BLE peripheral, it has to first scan the spectrum, then pick up the interesting, UART-enabled devices, attempt a connection with each, and then mark them as ready and move to handling user's input. This required two big changes to my code: putting the BLE devices in a list and adding an ID number before any command in the command queue. The BLE subprocess looks like this:
def loop_a(q): ble = Adafruit_BluefruitLE.get_provider() def main(): #Get a clean start ble.clear_cached_data() adapter = ble.get_default_adapter() adapter.power_on() print('Using adapter: {0}'.format(adapter.name)) print('Disconnecting any connected UART devices...') UART.disconnect_devices() adapter.start_scan() # Begin scanning for UART-enabled BLE devices, keep repeating it if you find nothing: found = [] anim = [] while not found: print('Searching for UART devices...') # This method returns a list of found devices: found = UART.find_devices() # Let's list them: for device in found: print('Found UART: {0} [{1}]'.format(device.name, device.id)) print('Scan done.') # Let's try to connect to each of them: for device in found: print('Connecting to device: [{0}]...'.format(device.id)) device.connect() # Discover services print('Discovering services...') UART.discover(device) uart = UART(device) # Add to the list of animators: anim.append(uart) # Send a test message and report all is good: uart.write('Q') print("{0} waiting for commands.".format( device.id)) time.sleep(1.0) print('Setup done.') # Read the command queue as usual: while 1: if not (q.empty()): msg = q.get() # When a message comes, use its first character as the ID of the receiving animator: id = int(msg[0]) rcv = anim[id] print("TXed to {0}.".format(id)) rcv.write(msg) # Initialize BLE ble.initialize() # Run BLE loop ble.run_mainloop_with(main)
I expect no serious changes to this code in the near future. What you can see above is an expansion of my last week's code, facilitated greatly by studying this Bluefruit example. I hope this code is commented enough. It allows me to talk to all my animators (all 3 of them, as I have 3 Blend boards) and the command queue is the interface connecting it to the website, described below.
Color picking
Before we dive into the realm of Flask and JavaScript-based request handling, I should explain my approach to picking colours for the strips. In some strip patterns, the colour presented is fully user-definable - ranging from making the entire strip light up in a single color, to pixels lighting up and fading, marching, or even a Matrix rain-like effect that can take up any colour. All this is possible thanks to a JS widget called ColorJoe. It's very complete, easy to deploy and modify, and looks great. It generates events on every change of the picked colour, so I added a few lines to its JS code to make it send GET requests to the server. The resulting JS file (modified from demo.js on the project's GitGub) looks like this:
fixScale(document); main(); function main() { var hVal = document.getElementById('hslaValue'); colorjoe.registerExtra('text', function(p, joe, o) { e(p, o.text? o.text: 'text'); }); function e(parent, text) { var elem = document.createElement('div'); elem.innerHTML = text; parent.appendChild(elem); } colorjoe.hsl('hslPicker', '#113c38', [ ]).on('change', function(c) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { } }; xhttp.open("GET", "/set_hsl?hue=" + c.hue() + "&sat=" + c.saturation()+"&id=" + window.name, true); xhttp.send(); }).update(); }
All colour data is handled using the HSV colour space, natively supported by the Arduino FastLED library I'm using in the Blends, with some interesting twists. While the V (value, i.e. brightness) is quite straightforward to understand, there are some issues with the hues: we can't produce a decent yellow (while red mixes amazingly well with blue to give all shades of pink, it doesn't really go well with green), and obviously the 'white' is always fake, being a sum of all reds, greens and blues, giving weird shadows if not diffused in any way. While in a typical HSV use case the saturation value is used to map the colour between the most pure, and dull gray, in the case of LED strips there are no grays possible - there is no gray light, and no black lightsabers, after all. Instead, when decreasing saturation we get colours increasingly similar to the fake RGB white. Therefore, the saturation dimension of our colour picker has little demo value, but being able to change the colours with finger swipes is amazing anyway.
More JS, and HTML too
Having embedded ColorJoe in my web interface, I needed to think about how do I handle a multitude of devices from that single website. Having very little knowledge of modern Web development, I decided to use IFrames. Creating an IFrame dynamically for each connected animator (still a TODO), I read their IDs and put them in every GET request. This way, a request for brightness change gets one more field:
xhttp.open("GET", "/set_brightness?brightness=" + slider.value+"&id=" + window.name, true);
Window (IFrame) names are incremental IDs that will be generated dynamically in the future. At the moment I statically create two IFrames for testing, and have moved my entire pattern-related interface there, resulting in a new file, panel.html:
<!DOCTYPE html> <!--https://www.w3schools.com/howto/howto_js_rangeslider.asp--> <html> <head> <link rel="stylesheet" href="../css/colorjoe.css"> </head> <body> <h1>Panel</h1> <div> <section> <div id="hslPicker"></div> </section> </div> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <script src='../js/scale.fix.js'></script> <script src='../dist/colorjoe.js'></script> <script src='../js/demo.js'></script> <div id="slidecontainer"> <input type="range" min="0" max="255" value="0" id="brightness"> <p>Brightness: <span id="brightness_value"></span></p> </div> <div id="slidecontainer"> <input type="range" min="0" max="255" value="0" id="delay"> <p>Delay: <span id="delay_value"></span></p> </div> <div id="slidecontainer"> <input type="range" min="0" max="255" value="0" id="step"> <p>Step: <span id="step_value"></span></p> </div> <script> var slider = document.getElementById("brightness"); var output = document.getElementById("brightness_value"); output.innerHTML = slider.value; slider.oninput = function() { output.innerHTML = slider.value; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { } }; xhttp.open("GET", "/set_brightness?brightness=" + slider.value+"&id=" + window.name, true); xhttp.send(); } </script> <script> var slider2 = document.getElementById("delay"); var output2 = document.getElementById("delay_value"); output2.innerHTML = slider2.value; slider2.oninput = function() { output2.innerHTML = slider2.value; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { } }; xhttp.open("GET", "/set_delay?delay=" + slider2.value+"&id=" + window.name, true); xhttp.send(); } </script> <script> var slider3 = document.getElementById("step"); var output3 = document.getElementById("step_value"); output3.innerHTML = slider3.value; slider3.oninput = function() { output3.innerHTML = slider3.value; var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { } }; xhttp.open("GET", "/set_step?step=" + slider3.value+"&id=" + window.name, true); xhttp.send(); } </script> <script> /* When the user clicks on the button, toggle between hiding and showing the dropdown content */ function button(arg) { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { } }; xhttp.open("GET", "/set_mode?mode=" + arg+"&id=" + window.name, true); xhttp.send(); } </script> <p>Pattern:</p> <button onclick="button(1)">1</button> <button onclick="button(2)">2</button> <button onclick="button(3)">3</button> <button onclick="button(4)">4</button> <button onclick="button(5)">5</button> <button onclick="button(6)">6</button> <button onclick="button(7)">7</button> <button onclick="button(8)">8</button> <button onclick="button(9)">9</button> </body> </html>
As you can see, I have also changed the buttons to run a simple JS script that also creates a GET request with an ID and a mode number. The index.html now only serves a scaffolding of IFrames:
<!DOCTYPE html> <html> <head> <link rel="stylesheet" href="css/colorjoe.css"> </head> <body> <h1>Universal LED Animator</h1> <iframe name="0" width="360" height="700" src="templates/panel.html"></iframe> <iframe name="1" width="360" height="700" src="templates/panel.html"></iframe> </body> </html>
Some serious Flask code
Ok, not so serious perhaps. Most serious I've ever written anyway. What used to be a few lines of Python serving a few buttons and sliders has grown considerably, and parsing two-parameter GET requests (consisting of an animator ID and a animation parameter to be set) has been generalised in a single method:
#Parse any request. def parse_req(arg, desc, code): id = request.args.get("id") val = request.args.get(arg) print desc +": " + str(val)+" id: " + str(id) queue.put(str(id)+code+str(val)) #Serve the pattern buttons. @app.route("/") def index(): return render_template('index.html') @app.route('/templates/panel.html') def show_map(): return render_template('panel.html') @app.route("/set_mode") def set_mode(): parse_req("mode", "Mode", "m") #Recalculate the brightness here so that the Blend gets values in 0-255 range instead of 0.0-1.0 @app.route("/set_hsl") def set_hsl(): id = request.args.get("id") hue = int(float(request.args.get("hue"))*100) sat = int(float(request.args.get("sat"))*100) print "Hue: " + str(hue) + " sat: " + str(sat)+" id: " + str(id) queue.put(str(id)+'h'+str(int(hue*2.55))+'t'+str(int(sat*2.55))) #Serve the sliders. @app.route("/set_brightness") def set_brightness(): parse_req("brightness", "Brightness", "b") @app.route("/set_delay") def set_delay(): parse_req("delay", "Delay", "d") @app.route("/set_step") def set_step(): parse_req("step", "Step", "s")
This code, and the HTML layout and style, is still a work in progress, but this update shows the direction I'm heading. I'd love to post some videos but two of my three Blends stopped responding very recently and at the time of writing this post I'm still trying to solve the problem.
Top Comments