element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Members
    Members
    • Benefits of Membership
    • Achievement Levels
    • Members Area
    • Personal Blogs
    • Feedback and Support
    • What's New on element14
  • Learn
    Learn
    • Learning Center
    • eBooks
    • STEM Academy
    • Webinars, Training and Events
    • More
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • More
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • More
  • Products
    Products
    • Arduino
    • Dev Tools
    • Manufacturers
    • Raspberry Pi
    • RoadTests & Reviews
    • Avnet Boards Community
    • More
  • Store
    Store
    • Visit Your Store
    • Choose Another Store
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
Bluetooth Unleashed Design Challenge
  • Challenges & Projects
  • Design Challenges
  • Bluetooth Unleashed Design Challenge
  • More
  • Cancel
Bluetooth Unleashed Design Challenge
Blog Universal LED Animator #7 - Getting there with the web interface
  • Blog
  • Forum
  • Documents
  • Events
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Blog Post Actions
  • Subscribe by email
  • More
  • Cancel
  • Share
  • Subscribe by email
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: mgal
  • Date Created: 17 Jul 2018 11:00 PM Date Created
  • Views 147 views
  • Likes 6 likes
  • Comments 3 comments
  • flask
  • beaglebone wireless
  • javascript
Related
Recommended

Universal LED Animator #7 - Getting there with the web interface

mgal
mgal
17 Jul 2018

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
  • Color picking
  • More JS, and HTML too
  • Some serious Flask code

 

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.

Anonymous

Top Comments

  • mcb1
    mcb1 over 4 years ago +1

    Looks like you're getting it all together.

    I'm always amazed that if you look hard enough, you'll find someone has done something similar to what you want, and you can add a bit to get it there.…

  • DAB
    DAB over 4 years ago +1

    Nice update.

     

    I may have to borrow some of your code for one of my projects.

     

    DAB

Parents
  • DAB
    DAB over 4 years ago

    Nice update.

     

    I may have to borrow some of your code for one of my projects.

     

    DAB

    • Cancel
    • Vote Up +1 Vote Down
    • Reply
    • More
    • Cancel
Comment
  • DAB
    DAB over 4 years ago

    Nice update.

     

    I may have to borrow some of your code for one of my projects.

     

    DAB

    • Cancel
    • Vote Up +1 Vote Down
    • Reply
    • More
    • Cancel
Children
No Data
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2022 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • Facebook
  • Twitter
  • linkedin
  • YouTube