LoL Akali Mask ( battery-powered APA102 setup )

Posted on
  • Hi there !

    I'm currently helping a friend building an animated "mask" from the LoL game, as seen on the following picture:

    We plan to have a total of 8 different "mouths", and the idea is to use a set of daisy-chained leds positioned within different "cavities" to represent each desired "leds combo": by turning on or off leds in different "cavities", we display the mouth n ( more or less like a "segment display", but using LEDs )

    The goal is to be able to start scheduled transitions betweens mouths at the press of the start button

    Now, onto the needy greedy:

    POC BOM:

    • currently using an Original Espruino board as controller
    • 2 x tactile switches to act as 'start' & 'reset' buttons
    • a PowerBoost 500C to boost [3.7V .. 4.2V] to 5V ( to power the LEDs & the Espruino board )
    • a 18650 12000mAh Li-Ion battery
    • APA102 ( or is it APA102C or another clone ? ) LEDS strip ( 25 of them )

    The charger/booster may be replaced by the 1000C version ( allowing up to 2A, in contrary to the 500C which is said to provide 1A from 3.7V batteries - although I'm driving 16 LEDs while testing since it shall draw max (60mA/led)*16= 960mA )

    It seems I'm getting somewhere in driving the LEDs thanks to this posts:

    Thing is, I'm now sure these HAVE to be driven at 5V ( else I got erratic colors powering 1, 3 or 16 of those through the 'Bat' pin on the original Espruino board) so powering those from 3.7V battery directly is a no-go, but I'm unsure of the best setup to control them: the SPI seems to be 'somewhat glitchy', and I'm guessing sure this comes from the pin config and not the spi.write calls themselves.

    • I tried with a level shifter, no success
    • I tried with 'af_opendrain' & 1.5k pullup resistors to 5v, no success
    • I tried with directly wiring Espruino pin ( B3 sck & B5 mosi ), success ONLY with software SPI

    I had some relative success driving 16 LEDs ( powered from BoostCharger 5V - actually 4.87V ) directly wired to B3 & B5, but in my tests, 'setInterval' wasn't 'as precise' as in my earlier test sketch ( in which I was using an OLED screen to display the mouth index & the timeout before a mouth update ). As the goal is to have 'somewhat precise' schedule for mouth updates, I think I have some things to pack right for this goal to be met ..

    So here goes the questions:

    • 1: IYO, what 'd be the best way to wire to those Sck & Data pins ?
    • 2: any way to get hardware SPI working to increase the refresh rate & lessen cpu usage ?
    • 3: would 'compiled' code helps in getting more precise & faster 'scheduled updates' ?
    • 4: am I driving these correctly at all ? ( or what may the 'spi behavior'* come from ? )

    *I find it quite weird that software spin kinda works but hardware doesn't

    The debug code currently used to drive the LEDs is the following

    //  -- extracts from APA102 test code --
    var spi = undefined;
    // ...
    spi = new SPI();
    spi.setup({miso:B4, mosi:B5, sck:B3, mode:1,order:"msb", baud: 4000000}); // seems ok for software spi
    // ...
    // driving 3 LEDS
    // syntax
    //spi.write(0,0,0,0, 0xe0+31,255,0,0, 0xe0+31,0,255,0, 0xe0+31,0,0,255, 0xFF ); // 1st R 2nd B 3rd G
    // ..
    var myArr = [ 
      new Uint8Array([0,0,0,0, 0xe0+31,255,255,255, 0xe0+31,255,0,0, 0xe0+31,0,10,0, 0xFF]),
      new Uint8Array([0,0,0,0, 0xe0+31,255,255,255, 0xe0+31,255,0,0, 0xe0+31,255,255,255, 0xFF]),
      new Uint8Array([0,0,0,0, 0xe0+31,0,0,255, 0xe0+31,255,0,0, 0xe0+31,255,255,255, 0xFF]),
      new Uint8Array([0,0,0,0, 0xe0+31,255,0,255, 0xe0+31,255,255,255, 0xe0+31,255,255,255, 0xFF]),
      new Uint8Array([0,0,0,0, 0xe0+31,255,0,0, 0xe0+31,0,0,0, 0xe0+31,255,255,255, 0xFF]),
      new Uint8Array([0,0,0,0, 0xe0+31,255,0,0, 0xe0+31,0,255,0, 0xe0+31,0,0,255, 0xFF])
    ];
    
    var idx = 0;
    var max = myArr.length;
    function loopOver(){
      //console.log(idx);
      if(idx+1 < max) idx++;
      else idx = 0;
      spi.write(myArr[idx]);
    }
    //var timr = setInterval(loopOver, 200);
    
    
  • ( no enough space in one post .. )

    The debug code currently handling the logic is the following
    ( I'll post the Illustrator script below in this post, as well as a a 'dumb try to get the shortest path linking every dots' ( html5 canvas pathfinding stuff ) not quite working as expected .. but hey, it may get better if given time to dod so ? ;p )

    // -- AKALI MASK --
    // - array generated from an Illustrator script pushing either '0, 0, 0' or '255,255,255' depending on the presence of pathItems above dots at certain coords in the artboard
    var mouthsLedsArrays = [
      [0,0,0,0,0,0,255,255,255,255,255,255,255­,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255­,255,255,255,255,255,255,255,0,0,0,0,0,0­],
      [255,255,255,255,255,255,0,0,0,0,0,0,0,0­,0,255,255,255,255,255,255,255,255,255,2­55,255,255,0,0,0,0,0,0,0,0,0,255,255,255­,255,255,255]
    ];
    
    // - timings extracted from a video/audio analysis of a chunk from the LoL soundtrack
    var absTimeMouthsIdxsArray = [
      //[<ms_from_start>, <mouth_idx>]
      [37, 0],
      [38, 1],
      [40, 0],
      [45, 1],
      [45.2, 0],
      [45.5, 1]
    ];
    
    // - relative timings to be used with 'setTimeout' to schedule the mouth updates
    // R: to map abolute times to relative timing in ms:
    // relTime = (absTime * 1000) - (1stAbsTime * 1000)
    var reTimeMouthsIdxsArray = [];
    for(var i=0; i< absTimeMouthsIdxsArray.length; i++){
      var relTime = (i === 0) ? ((absTimeMouthsIdxsArray[i][0] * 1000) - (absTimeMouthsIdxsArray[i][0] * 1000)) : (absTimeMouthsIdxsArray[i][0] * 1000) - (absTimeMouthsIdxsArray[i-1][0] * 1000);
      reTimeMouthsIdxsArray[i] = [relTime, absTimeMouthsIdxsArray[i][1]];
    }
    console.log('reTimeMouthsIdxsArray : ' + reTimeMouthsIdxsArray);
    console.log('reTimeMouthsIdxsArray.lengt­h : ' + reTimeMouthsIdxsArray.length);
    
    // few vars
    var timr = -1;
    var currMouthAnimIdx = 0; // where we're at in the 'reTimeMouthsIdxsArray'
    //var currMouthIdx = 0;
    var currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
    console.log('currMouthAnimIdx : ' + currMouthAnimIdx);
    console.log('currMouth : ' + currMouth);
    
    // - to be replaced by spy calls to update the leds
    // display mouth ( via LEDS or pretty much anything ?)
    function displayCurrMouth(){
      console.log('NEW MOUTH: ' + currMouth);
      if(currMouthAnimIdx+1 < reTimeMouthsIdxsArray.length){
        console.log('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx+1­][0] );
        g.clear();
        g.drawString('mouth idx: ' + currMouthAnimIdx, 10, 10);
        g.drawString('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx+1­][0], 10, 20);
        g.flip();
      }
      /*
      g.clear();
      g.drawString('mouth idx: ' + currMouthAnimIdx, 10, 10);
      //g.drawString('mouth: ' + currMouth.join(','), 10, 20);
      g.drawString('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx][­0] + 'ms', 10, 20);
      //g.drawString('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx][­0] + 'ms', 10, 30);
      g.flip();
      */
    }
    
    // transition between mouths
    function nxtMouth(){
      if(currMouthAnimIdx+1 < reTimeMouthsIdxsArray.length){ // didn't reach end of array yet
        currMouthAnimIdx = currMouthAnimIdx+1;
        //console.log('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx][­0] );
        //if(currMouthAnimIdx+1 < reTimeMouthsIdxsArray.length){ console.log('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx+1­][0] ); }
        timr = setTimeout(function(){
          currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
          //console.log('NEW MOUTH: ' + currMouth);
          displayCurrMouth();
          nxtMouth(); // schedule next call
        }, reTimeMouthsIdxsArray[currMouthAnimIdx][­0]);
      } else {
        timr = -1;
        console.log('end of mouth timed array reached -> animation complete !');
        g.clear();
        g.drawString('ANIMATION COMPLETE !', 10, 10);
        g.drawString('press startBtn again ;)', 10, 20);
        g.flip();
      }
    }
    
    // start & reset
    function startAnim(){
      console.log('start !');
      g.clear();
      g.drawString('next mouth in: ' + reTimeMouthsIdxsArray[currMouthAnimIdx+1­][0], 10, 10);
      g.flip();
      nxtMouth();
    }
    function resetAnim(){
      console.log('reset !');
      if(timr !== -1){
        clearTimeout(timr);
        timr = -1;
      }
      currMouthAnimIdx = 0;
      //currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
      currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
      g.clear();
      g.drawString('RESET COMPLETE !', 10, 10);
        g.drawString('press startBtn again ;)', 10, 20);
      g.flip();
    }
    
    // uC-specific pins
    var startBtnPin = A0;
    var resetBtnPin = A1;
    // watches
    var startBtnW;
    var resetBtnW;
    
    function startOled(){
      g.clear();
      g.drawString('waiting startBtn press ..', 10, 10);
      g.flip();
      
      // setup btns
      pinMode(startBtnPin, 'input_pullup');
      startBtnW = setWatch(function(e) { /*console.log(e.time-e.lastTime);*/ startAnim(); }, startBtnPin, { repeat:true, edge:'falling' });
      pinMode(resetBtnPin, 'input_pullup');
      resetBtnW = setWatch(function(e) { /*console.log(e.time-e.lastTime);*/ resetAnim(); }, resetBtnPin, { repeat:true, edge:'falling' });
    }
    
    var g = undefined;
    function onInit(){
      // I2C
      I2C1.setup({scl:B6,sda:B7, bitrate:1000000});
      g = require("SSD1306").connect(I2C1, startOled, { height : 32, contrast : 255 });
    }
    
  • The code generated by the Illustrator script & the Illustrator script ( CS5+ ) lies below

    // the generated led colors from above dots pathItem Y/N
    [[0,0,0,0,0,0,255,255,255,255,255,255,25­5,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,25­5,255,255,255,255,255,255,255,0,0,0,0,0,­0],[255,255,255,255,255,255,0,0,0,0,0,0,­0,0,0,255,255,255,255,255,255,255,255,25­5,255,255,255,0,0,0,0,0,0,0,0,0,255,255,­255,255,255,255]]
    // the dots coords, for debug ( & also to add in canvas script to try generating shortest path between dots )
    [[38.6038818359375,-28.5126953125],[53.5­712890625,-20.8525390625],[67.6137695312­5,-23.0546875],[83.61376953125,-28.22167­96875],[99.447265625,-23.0546875],[113.6­318359375,-20.9112854003906],[128.481628­417969,-28.4538269042969],[128.457397460­938,-47.0709228515625],[113.490051269531­,-54.731201171875],[99.44775390625,-52.5­283203125],[83.447509765625,-47.36132812­5],[67.61376953125,-52.5283203125],[53.4­285278320312,-54.671875],[38.57928466796­88,-47.12939453125]]
    
    

    Illustrator script, where logic is:

    • order of dots items on dots layer in .ai file determines generated colors indices
    • for each mouth layer, produce an array of color indices

      /*
      Generate a mouthsLedsArrays.json from the shapes in layers other than the top one in the currently opened Illustrator document
      */
      
      
      // ==== INCLUDES ====
      [#include](http://forum.espruino.com/sea­rch/?q=%23include) "json2.js" // jshint ignore:line
      
      // ==== ILLUSTRATOR FLAGS ====
      app.coordinateSystem = CoordinateSystem.ARTBOARDCOORDINATESYSTE­M;  // use artboard-based coords ( needed to easily get noc_x & noc_y )
      
      // ==== PNP HELPER ====
      // To digg: https://stackoverflow.com/questions/2252­1982/check-if-point-inside-a-polygon
      // also:http://alienryderflex.com/polygon/
      function inside(point, vs) {
      // ray-casting algorithm based on
      // http://www.ecse.rpi.edu/Homepages/wrf/Re­search/Short_Notes/pnpoly.html
      var x = point[0], y = point[1];
      var inside = false;
      for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
      var xi = vs[i][0], yi = vs[i][1];
      var xj = vs[j][0], yj = vs[j][1];
      
      var intersect = ((yi > y) != (yj > y))
          && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
      if (intersect) inside = !inside;
      }
      return inside;
      };
      
      // ==== SHORTEST COMBO HELPER ====
      function perm(xs) {
      var ret = [];
      
      for (var i = 0; i < xs.length; i = i + 1) {
      var rest = perm(xs.slice(0, i).concat(xs.slice(i + 1)));
      
      if(!rest.length) {
        ret.push([xs[i]])
      } else {
        for(var j = 0; j < rest.length; j = j + 1) {
          ret.push([xs[i]].concat(rest[j]))
        }
      }
      }
      return ret;
      }
      //console.log(perm([0,1,2]).join("\n"));­
      /*
      alternative timed permutator
      function permutator(inputArr) {
      var results = [];
      
      function permute(arr, memo) {
      var cur, memo = memo || [];
      
      for (var i = 0; i < arr.length; i++) {
        cur = arr.splice(i, 1);
        if (arr.length === 0) {
          results.push(memo.concat(cur));
        }
        permute(arr.slice(), memo.concat(cur));
        arr.splice(i, 0, cur[0]);
      }
      
      return results;
      }
      
      return permute(inputArr);
      }
      var t0 = performance.now(); //console.time('someFunction');
      //var res = permutator([0,1,2,3,4,5,6,7,8]); // ~ 1191.80ms
      //var res = permutator([0,1,2,3,4,5,6,7,8,9]); // ~12969.20ms
      var res = permutator([0,1,2,3,4,5,6,7,8,9,10]); // ~ 147567.70 mins
      console.log(res.length);
      var t1 = performance.now(); // console.timeEnd('someFunction');
      console.log("Call to doSomething took " + (t1 - t0).toFixed(2) + " milliseconds.")
      */
      
      // ==== UNITS HELPERS ====
      //function rndMm(num){ return ( num * 0.3528 ).toFixed(2); } // quick helper - getting mm
      function rndMm(num){ return ( num ).toFixed(2); } // quick helper - getting pixels
      
      // -= STEPS =-
      // - 0: for each mouth shape on the 'template' ( top-most ) layer, position one ( or more equally distant ) 'dot(s)' on the 'dots' layer
      // - 1: get the second-most layer ( the 'dots' one )
      // - 2: get all of its 'dots' pathItems in an array
      // - 3: create a 'maskMouthsArray', and for [2..n] other ( down from second ) layers, create a 'mouthArray' and:
      // ---- 4: for each dot on the 'dots' layer:
      // ------- 5: for evey shape in layer n:
      // ---------- 6: if shape 'contains' the dot, either set our current mouthArray[dot] to '1' or directly to '255, 255, 255' ( for direct RGB White mapping )
      // - 7: push our current 'mouthArray' to our 'maskMouthsArray'
      // - 8: once all layers have been processed, save the 'maskMouthsArray' in the 'mouthsLedsArrays.json' output file
      
      
      // -= QUICK IMPLM =-
      var layers = app.activeDocument.layers;
      // - 0: for each mouth shape on the 'template' ( top-most ) layer, position one ( or more equally distant ) 'dot(s)' on the 'dots' layer [TODO]
      var templateLayer = layers[0];
      alert('templateLayer: '+ templateLayer.name);
      // - 1: get the second-most layer ( the 'dots' one )
      var dotsLayer = layers[2];
      alert('dotsLayer: '+ dotsLayer.name);
      // - 2: get all of its 'dots' pathItems ( an array )
      var dots = dotsLayer.pathItems;
      
      // ---- BRUTE-FORCE OPTIMAL DOTS WIRING ORDER: start ----
      
      var dotsCoordsArr = [];
      // - 2.1: get array of the coords of the dots
      for(var dotI = 0; dotI < dots.length; dotI++){
      var dotC = dots[dotI].position;
      dotC[0] += dots[dotI].width/2.0; // dotCx
      dotC[1] -= dots[dotI].height/2.0; // dotCy
      dotsCoordsArr.push([ dotC[0], dotC[1] ]);
      }
      /*
      // - 2.2: get all possible permutations of their indices ( from 0 to length-1 )
      var dotsIndices = [];
      for(var dotI = 0; dotI < dots.length; dotI++){ dotsIndices.push(dotI); } // get every possible index
      // GET ONLY FEW FIRST DOTS TO TRY GOING THROUGH WHILE NOT FREEZING ??
      //dotsIndices = dotsIndices.slice(0, 4); // get 4 dots out of n ( 14 in our example ) -> gives 24 possible permutations
      dotsIndices = dotsIndices.slice(0, 8);
      var possibleIndicesPermutations = perm(dotsIndices);
      // --> THE LINE ABOVE IS WHAT MAKES ILLUSTRATOR JS INTERPRETER FREEZE WHEN APPLIED TO AN ARRAY OF 14 ITEMS
      // considering for [0..8] we'd get 362880 possible permutations
      alert('possible permutations: ' + possibleIndicesPermutations.length);
      //* --> the following make Illustrator no longer responsive .. aka crash ;/
      // - 2.3: for all permutations, sum the distances between i & i+1 for each permutation
      var summedDistances = [];
      for(var dotPermsI = 0; dotPermsI < possibleIndicesPermutations.length; dotPermsI++){ // for each permutation
      var summedDists = 0;
      var currPerm = possibleIndicesPermutations[dotPermsI];
      //alert('current permutation: ' + JSON.stringify(currPerm) );
      //for(var dotPermI; dotPermI < possibleIndicesPermutations[dotPermsI].l­ength; dotPermI++ ){ // for each dot index in it
      for(var dotPermI = 0; dotPermI < currPerm.length; dotPermI++ ){ // for each dot index in it
      var currPermCurrDot = currPerm[dotPermI];
      //alert('current permutation current dot idx: ' + JSON.stringify(currPerm[currPermCurrDot]­) );
      //alert('current permutation current dot idx: ' + JSON.stringify(currPermCurrDot) );
      dots[currPermCurrDot].name = 'LOL'+currPermCurrDot;
      // get distance between dot[i] & dot[i+1]
      if(dotPermI < possibleIndicesPermutations[dotPermsI].l­ength-1){ // beginning to end
        var dotC = dots[currPermCurrDot].position;
        dotC[0] += dots[currPermCurrDot].width/2.0; // dotCx
        dotC[1] -= dots[currPermCurrDot].height/2.0; // dotCy
        var dotC2 = dots[currPermCurrDot].position;
        dotC2[0] += dots[currPermCurrDot].width/2.0; // dotC2x
        dotC2[1] -= dots[currPermCurrDot].height/2.0; // dotC2y
        var dist =  Math.sqrt( Math.pow((dotC[0] - dotC2[0]), 2) + Math.pow((dotC[1] - dotC2[1]), 2) );
        summedDists += dist;
      } else { // end to beginning
        var dotC = dots[currPermCurrDot].position;
        dotC[0] += dots[currPermCurrDot].width/2.0; // dotCx
        dotC[1] -= dots[currPermCurrDot].height/2.0; // dotCy
        var dotC2 = dots[currPerm[0]].position;
        dotC2[0] += dots[currPerm[0]].width/2.0; // dotC2x
        dotC2[1] -= dots[currPerm[0]].height/2.0; // dotC2y
        var dist = Math.sqrt( Math.pow((dotC[0] - dotC2[0]), 2) + Math.pow((dotC[1] - dotC2[1]), 2) );
        summedDists += dist;
      }
      }
      summedDistances.push(summedDists);
      }
      // - 2.4: check which of the sum is the lowest
      var lowestSummedDistIdx = 0;
      var lowestSummedDist = summedDistances[0]; // set as 1st permutation's sum to start
      for(var sumDistI = 0; sumDistI < summedDistances.length; sumDistI++){
      if(summedDistances[sumDistI] < lowestSummedDist){
      lowestSummedDistIdx = sumDistI;
      lowestSummedDist = summedDistances[sumDistI];
      }
      }
      alert('best permutation total length: ' + lowestSummedDist);
      // - 2.5: re-order thz z-index of the dots by looping over the indices in the permutation result that has the lowest sum,
      //        from end to beginning & doing 'bring to front' operation on i
      var bestPermutation = possibleIndicesPermutations[lowestSummed­DistIdx];
      for (var i = bestPermutation.length - 1; i >= 0; --i) {
      dots[bestPermutation[i]].zOrder(ZOrderMe­thod.BRINGTOFRONT);
      }
      */
      // ---- BRUTE-FORCE OPTIMAL DOTS WIRING ORDER: end ----
      
      // DEBUG: for now, rename the dots based on their zIndexPosition
      var debugLayer = layers[1];
      var dotsPathPts = []; // used to draw one big path instead of many little ones
      for(var dotI = 0; dotI < dots.length; dotI++){
      dots[dotI].name = dotI; //dots[dotI].zOrderPosition;
      // TODO: add txt right within dot with idx
      //var pointTextRef = docRef.textFrames.add();
      var pointTextRef = debugLayer.textFrames.add();
      pointTextRef.contents = dotI; //"TextFrame #3";
      pointTextRef.story.textRange.justificati­on = Justification.CENTER;
      pointTextRef.textRange.size = 5;
      //var textColor_gray = new GrayColor();
      //textColor_gray.gray = 70;
      //pointTextRef.textRange.paragraphs[0].f­illColor = textColor_gray;
      var textColor_rgb = new RGBColor();
      textColor_rgb.blue = 255;
      pointTextRef.textRange.paragraphs[0].fil­lColor = textColor_rgb;
      //pointTextRef.contents.fillColor = app.colors.item("Red");
      var dotC = dots[dotI].position;
      dotC[0] += dots[dotI].width/2.0; // dotCx
      dotC[1] -= dots[dotI].height/2.0; // dotCy
      pointTextRef.top = dotC[1]; //700;
      pointTextRef.left = dotC[0]; //400;
      //pointTextRef.selected = true;
      //redraw();
      
      // draw a line between those as well
      if(dotI < dots.length-1){
      // get nxt dot
      var dotC2 = dots[dotI+1].position;
      dotC2[0] += dots[dotI+1].width/2.0; // dotCx
      dotC2[1] -= dots[dotI+1].height/2.0; // dotCy
      
      dotsPathPts.push([ dotC[0], dotC[1] ], [ dotC2[0], dotC2[1] ]);
      
      /*
      var strokeColor = new RGBColor();
      strokeColor.green = 255;
      var line = debugLayer.pathItems.add();
      line.stroked = true; // to be able to see it
      line.strokeColor = strokeColor;
      line.strokeWidth = 4 ;
      line.setEntirePath( [ [ dotC[0], dotC[1] ], [ dotC2[0], dotC2[1] ] ] );
      */
      }
      }
      var strokeColor = new RGBColor();
      strokeColor.green = 255;
      var line = debugLayer.pathItems.add();
      line.stroked = true; // to be able to see it
      line.filled = false;
      line.strokeColor = strokeColor;
      line.strokeWidth = 1;
      //line.setEntirePath( [ [ dotC[0], dotC[1] ], [ dotC2[0], dotC2[1] ] ] );
      line.setEntirePath( dotsPathPts );
      redraw();
      
      // - 3: create a 'maskMouthsArray', and for [2..n] other ( down from second ) layers, create a 'mouthArray' and:
      var maskMouthsArr = [];
      //var layerIdx= 2;
      var layerIdx= 3; // take in account added "debug" layer ( where we trace debug lines & where dots numbers are displayed )
      //for(layerIdx; layerIdx < layers.length -2; layerIdx++){ // process every layer except 1st & 2nd top-most
      for(layerIdx; layerIdx < layers.length; layerIdx++){ // process every layer except 1st & 2nd top-most
      alert('processing layer: '+ layers[layerIdx].name);
      var mouthArr = [];
      // ---- 4: for each dot on the 'dots' layer:
      for(var dotIdx = 0; dotIdx < dots.length; dotIdx++){
      // - 4.1.: getting the center of the dot
      var dotC = dots[dotIdx].position;
      dotC[0] += dots[dotIdx].width/2.0; // dotCx
      dotC[1] -= dots[dotIdx].height/2.0; // dotCy
      
      // ------- 5: for evey shape in layer n:
      var nLayerShapes = layers[layerIdx].pathItems;
      var dotShapeFound = false; // whether or not the dot has found a shape it fits in
      for(var layerShapeIdx = 0; layerShapeIdx < nLayerShapes.length; layerShapeIdx++){
        if(dotShapeFound == true) break; // don't loop further over other shapes since our current dot has found its shape mate
        // else, continue looping until we deduce that the 'mouth section' corresponding to that dot is not used on this 'mouthArray'
        // ---------- 6: if shape 'contains' the dot, either set our current mouthArray[dot] to '1' or directly to '255, 255, 255' ( for direct RGB White mapping )
        /*
        // --> HUGELY-IN-POC-AND-MAYBE-PROGRESS PART: bypassing the 'visibleBounds' & 'geometricBounds' limitations to check whether a dot is 'within' a shape <--
        // - 6.0: getting the center of the shape
        var c1 = nLayerShapes[layerShapeIdx].position;
        c1[0] += nLayerShapes[layerShapeIdx].width/2.0;
        //center[1] += obj.height/2.0;
        c1[1] -= nLayerShapes[layerShapeIdx].height/2.0;
        // - 6.1: change currently selected objs to have dots[dotIdx] & as selection for futher pathfinder bool op .. or just to 'group' those & apply the same idea ;)
        dots[dotIdx].selected = true;
        nLayerShapes[layerShapeIdx].selected = true;
        // - 6.2: group those to see if this affect the shape's center by a yota or more
        */
      
        // get pathPoints from shape ( R: currently, curves may produce weird results .. )
        var shapePathPts = nLayerShapes[layerShapeIdx].pathPoints;
        // loop over those & build an array of coords
        var polygon = [];
        for(var j=0; j < shapePathPts.length; j++){
          var anch = shapePathPts[j].anchor;  // its anchor
          polygon.push([anch[0], anch[1]]);
        }
        //var polygon = [ [ 1, 1 ], [ 1, 2 ], [ 2, 2 ], [ 2, 1 ] ]; // array of coordinates of each vertex of the polygon
        //inside([ 1.5, 1.5 ], polygon); // true - whether the test point in inside or outside the polygon
        if( inside([ dotC[0], dotC[1] ], polygon) == true ){
          dotShapeFound = true; // break outta loop
        }
      
      }
      if(dotShapeFound == true){
        mouthArr.push(255, 255, 255); // RGB LEDS like APA102 - full on ( white )
      } else {
        mouthArr.push(0, 0, 0); // RGB LEDS like APA102 - full off
      }
      }
      // - 7: push our current 'mouthArray' to our 'maskMouthsArray'
      // all dots have been processed for current mouth layer: push it to our maskMouths array
      maskMouthsArr.push(mouthArr)
      
      }
      // - 8: once all layers have been processed, save the 'maskMouthsArray' in the 'mouthsLedsArrays.json' output file
      //var container = { "expression": expression};
      //alert(JSON.stringify(container) );
      
      
      //File.saveDialog (prompt[, preset])
      var jsonFile = new File("mouthsLedsArrays.json"); // def file name
      var saveFilePath = jsonFile.saveDlg("Where to save generated stuff ? :)");
      //var filepath = "~/Desktop/" + randomname + ".txt";
      var write_file = File(saveFilePath);
      if (!write_file.exists) {
      // if the file does not exist create one
      //write_file = new File(filepath);
      write_file = new File(saveFilePath);
      } else {
      // if it exists ask the user if it should be overwritten
      var res = confirm("The file already exists. Overwrite ?", true, "titleWINonly");
      // if the user hits no stop the script
      if (res !== true) {
      //return; // threw an error on AI CS5 ..
      write_file = '';
      }
      }
      
      var out; // our output ( we know already that the file exist but to be sure )
      if (write_file !== '') {
      out = write_file.open('w', undefined, undefined); //Open the file for writing.
      write_file.encoding = "UTF-8";
      write_file.lineFeed = "Unix"; //convert to UNIX lineFeed
      // txtFile.lineFeed = "Windows";
      // txtFile.lineFeed = "Macintosh";
      }
      if (out !== false) { // got an output?
      write_file.writeln( JSON.stringify(maskMouthsArr) ); // write generated stuff to json file
      write_file.writeln( JSON.stringify(dotsCoordsArr) ); // debug coords array
      write_file.close();
      }
      
      
      /*
      // ==== EAGLE ILLUSTRATOR HELPER ====
      //var eagleCenter = undefined;
      //var eagleCenterPos = {};
      
      // getting the items selected
      var selecteditems = app.activeDocument.selection;
      //alert("Found" + selecteditems.length + " selected items ..");
      
      if(selecteditems.length < 1 ){ // || selecteditems.length > 2 ){
      alert("Please select two elements, not less or more");
      } else {
      // start building our expression
      var expression = {};
      
      // get objects centers
      var obj1 = selecteditems[0];
      alert('nullObjectCenter -> obj.name: ' + obj1.name);
      var c1 = obj1.position;
      c1[0] += obj1.width/2.0;
      //center[1] += obj.height/2.0;
      c1[1] -= obj1.height/2.0;
      
      expression.noc_x = parseFloat( rndMm( c1[0] ) );
      //expression.noc_y = parseFloat( rndMm( c1[1] ) );
      //expression.noc_y = parseFloat( rndMm( 64+ c1[1] ) );
      expression.noc_y = parseFloat( rndMm( c1[1] * -1 ) );
      /*
      var obj2 = selecteditems[1];
      var c2 = obj2.position;
      c2[0] += obj2.width/2.0;
      //center[1] += obj.height/2.0;
      c2[1] -= obj2.height/2.0;
      
      var objs = [
      {cX: rndMm(c1[0]), cY: rndMm(c1[1])},
      {cX: rndMm(c2[0]), cY: rndMm(c2[1])},
      ];
      * /
      
      expression.eyes = [];
      // for each object selected after the 1st one
      // /!\ R: the order is the STACKING order, not the one in which we selected objects
      for(var i=1; i < selecteditems.length; i++){
      var selectedItem = selecteditems[i];
      alert(selectedItem.typename);
      
      var pathPts = selectedItem.pathPoints;
      //alert('pathPts: ' + selectedItem.pathPoints);
      var polyPts = {};
      
      // loop over those
      //var cntr = 1;
      for(var j=0; j < pathPts.length; j++){
      
      
        var pathPt1 = pathPts[j]; // the point
        var pt1Anch = pathPt1.anchor;  // its anchor
        var p1x = pt1Anch[0];
        var p1y = pt1Anch[1];
        / *
        // also get next points info ( to ease later parsing, we change the way path points are stored in the json to something quicker eagle-side )
        var pathPt2 = pathPts[j+1]; // the next point
        var pt2Anch = pathPt2.anchor;  // its anchor
        var p2x = pt2Anch[0];
        var p2y = pt2Anch[1];
        * /
        // instead of adding those directly to our array holding the pathPts ready for Eagle, we compute their positions relative to our 'eagle_center'
        //var relPt1x = parseFloat( rndMm( p1x - eagleCenterPos['cx'] ) );
        //var relPt1y = parseFloat( rndMm( p1y - eagleCenterPos['cy'] ) );
        //var relPt2x = parseFloat( rndMm( p2x - eagleCenterPos['cx'] ) );
        //var relPt2y = parseFloat( rndMm( p2y - eagleCenterPos['cy'] ) );
        var relPt1x = parseFloat( rndMm( p1x - c1[0] ) );
        var relPt1y = parseFloat( rndMm( p1y - c1[1] ) );
      
        // std pathPoint.anchor x & y
        polyPts['x'+(j+1)] = relPt1x;
        polyPts['y'+(j+1)] = relPt1y;
      
        // DEBUG --
        polyPts['X_POS'+(j+1)] = parseFloat( rndMm( p1x ) );
        polyPts['Y_POS'+(j+1)] = parseFloat( rndMm( p1y ) );;
        // DEBUG --
      
        // quick test, to understand WHY & HOW I can't get bezier data out of shapes drawn using the pen tool ..
        / *
        if(pathPt1.leftDirection && pathPt1.rightDirection )
          alert('pathPt1.leftDirection exist on a point of that path, be it aof type "CORNER" or not ?! --> in: (' +
          pathPt1.leftDirection[0] + ', ' + pathPt1.leftDirection[1] + ') out: (' +
          pathPt1.rightDirection[0] + ', ' + pathPt1.rightDirection[1] + ') '
        );
        * /
      
        // for smoothed stuff, handle in & out control points
        // R: a rectangle or other shape becomes "SMOOTH" as soon as one of its point is modded ( ex: via pen tool )
        // Q: are Illustrator actual cubic curves ? ( then we'd need to add the next anchor point as p3 - but we don't need to do so here -> so no storage overhead from that ;) )
        if(pathPt1.pointType !== PointType.CORNER){ // other type is PointType.SMOOTH ( https://illustrator-scripting-guide.read­thedocs.io/jsobjref/PathPoint/ )
          polyPts['x'+(j+1)+'in'] = parseFloat( rndMm( pathPt1.leftDirection[0] - c1[0] ) );
          polyPts['y'+(j+1)+'in'] = parseFloat( rndMm( pathPt1.leftDirection[1] - c1[1] ) );
          polyPts['x'+(j+1)+'out'] = parseFloat( rndMm( pathPt1.rightDirection[0] - c1[0] ) );
          polyPts['y'+(j+1)+'out'] = parseFloat( rndMm( pathPt1.rightDirection[1] - c1[1] ) );
        }
        // as a test
        polyPts['x'+(j+1)+'pointType'] = pathPt1.pointType.toString().substr(path­Pt1.pointType.toString().indexOf('.')+1)­;
      
      }
      expression.eyes.push(polyPts);
      
      }
      
      //alert(JSON.stringify(expression) );
      var container = { "expression": expression};
      alert(JSON.stringify(container) );
      }
      */
      
      
  • .. and the code that tries ( in a very dumb manner currently ) to find the shortest path between all dots ( it 'd be neat if it was optimized & not accepting lines crossing among other stuff, but this is a very quick & dumb take on the travelling salesman problem or one of its derivative ? .. )

    If someone want to have fun ,why not allowing to specify ( or not ) start and/or end points & then let the magic happen ?

    so, the ( ugly ) code, enjoy ! :)

    <!DOCTYPE html>
    <!--
    Created using JS Bin
    http://jsbin.com
    
    Copyright (c) 2019 by anonymous (http://jsbin.com/qawemovawa/1/edit)
    
    Released under the MIT license: http://jsbin.mit-license.org
    -->
    <meta name="robots" content="noindex">
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width">
      <title>JS Bin</title>
    <style id="jsbin-css">
    [#canvas](http://forum.espruino.com/sear­ch/?q=%23canvas) {
      /*
      width: 160px;
      height: 80px;
      */
      border : 1px solid blue;
    }
    </style>
    </head>
    <body>
      <canvas id="canvas"></canvas>
    <script id="jsbin-javascript">
    // https://www.youtube.com/watch?v=BAejnwN4­Ccw
    // Coding Challenge #35.2: Lexicographic Order: https://www.youtube.com/watch?v=goUlyp4r­wiU
    // Coding Challenge #35.3: Traveling Salesperson with Lexicographic Order
    // Coding Challenge #35.4: Traveling Salesperson with Genetic Algorithm
    // Coding Challenge #35.4: Traveling Salesperson with Genetic Algorithm and Crossover: https://www.youtube.com/watch?v=hnxn6DtL­YcY
    var cnvs = document.querySelector('#canvas');
    var ctx = cnvs.getContext('2d');
    //cnvs.width = 160;
    //cnvs.height = 80;
    cnvs.width = 160*2;
    cnvs.height = 80*2;
    
    // draw dot helper
    var drawDot = function(ctx, x, y, rad){
      ctx.beginPath();
      ctx.arc(x, y, rad, 0, 2 * Math.PI);
      ctx.stroke();
      ctx.fill();
    }
    
    // coords array from Illustrator
    /*
    var coordsArr = [[38.6038818359375,-28.5126953125],[53.5­712890625,-20.8525390625],[67.6137695312­5,-23.0546875],[83.61376953125,-28.22167­96875],[99.447265625,-23.0546875],[113.6­318359375,-20.9112854003906],[128.481628­417969,-28.4538269042969],[128.457397460­938,-47.0709228515625],[113.490051269531­,-54.731201171875],[99.44775390625,-52.5­283203125],[83.447509765625,-47.36132812­5],[67.61376953125,-52.5283203125],[53.4­285278320312,-54.671875],[38.57928466796­88,-47.12939453125]];
    for(var i=0; i< coordsArr.length; i++){
      drawDot(ctx, coordsArr[i][0], cnvs.height + coordsArr[i][1], 2);
    }*/
    
    
    /* ==== following the "coding train" ==== */
    // swapping helper
    function swap(a, i, j){
      var tmp = a[i];
      a[i] = a[j];
      a[j] = tmp;
    }
    
    
    // distance calculation helper
    function dist(p1x, p1y, p2x, p2y){
      return Math.sqrt( Math.pow((p1x - p2x), 2) + Math.pow((p1y - p2y), 2) );
    }
    function calcDistance(points){
      var sum = 0;
      for(var i=0; i < points.length-1; i++){
        var d = dist(points[i][0], points[i][1],
                    points[i+1][0], points[i+1][1]);
        sum += d;
      }
      return sum;
    }
    
    var cities = [];
    var totalCities = 10;
    
    var recordDistance;
    var bestEver;
    
    function setup(){
      /* -- random pts --
      for(var i=0; i< totalCities; i++){
        var v = [Math.random()*cnvs.width, Math.random()*cnvs.height];
        //var v = [10, 10];
        cities[i] = v;
      }
      */
      ///* -- pts from Illu
      // 1st arr is "manually optimized"
      //var coordsArr = [[38.6038818359375,-28.5126953125],[53.5­712890625,-20.8525390625],[67.6137695312­5,-23.0546875],[83.61376953125,-28.22167­96875],[99.447265625,-23.0546875],[113.6­318359375,-20.9112854003906],[128.481628­417969,-28.4538269042969],[128.457397460­938,-47.0709228515625],[113.490051269531­,-54.731201171875],[99.44775390625,-52.5­283203125],[83.447509765625,-47.36132812­5],[67.61376953125,-52.5283203125],[53.4­285278320312,-54.671875],[38.57928466796­88,-47.12939453125]];
      // 2nd array is "intentionally un-optimized" of the 1st
      var coordsArr = [[38.5792846679688,-47.12939453125],[53.­5712890625,-20.8525390625],[67.613769531­25,-23.0546875],[38.6038818359375,-28.51­26953125],[99.447265625,-23.0546875],[83­.61376953125,-28.2216796875],[113.631835­9375,-20.9112854003906],[128.45739746093­8,-47.0709228515625],[67.61376953125,-52­.5283203125],[113.490051269531,-54.73120­1171875],[99.44775390625,-52.5283203125]­,[128.481628417969,-28.4538269042969],[8­3.447509765625,-47.361328125],[53.428527­8320312,-54.671875],];
      for(var i=0; i< coordsArr.length; i++){
        cities[i] = [coordsArr[i][0], cnvs.height + coordsArr[i][1]];
      }
      totalCities = cities.length;
      //*/
    
      //console.log(cities);
      var d = calcDistance(cities);
      recordDistance = d;
      bestEver = cities.slice();
      console.log('recordDistance: ' + recordDistance);
    }
    
    function draw(){
      // redraw
      requestAnimationFrame(draw);
      //setTimeout(draw, 200);
      //console.log('drawing ..');
    
      //ctx.clearRect(0, 0, cnvs.width, cnvs.height);
      ctx.fillStyle = 'black';
      ctx.fillRect(0, 0, cnvs.width, cnvs.height);
    
      // draw dots
      ctx.strokeStyle = 'white';
      ctx.fillStyle = 'white';
      for(var i=0; i< totalCities; i++){
        drawDot(ctx, cities[i][0], cities[i][1], 4);
      }
    
      // draw current cities liaisons
      ctx.strokeStyle = 'white';
      ctx.strokeWeight = 2;
      ctx.beginPath();
      // connect via lines
      for(var i=0; i< totalCities-1; i++){
        ctx.moveTo(cities[i][0], cities[i][1]);
        ctx.lineTo(cities[i+1][0], cities[i+1][1]);
      }
      ctx.closePath();
      ctx.stroke();
    
      // draw best cities liaisons
      ctx.strokeStyle = 'blue';
      ctx.strokeWeight = 4;
      ctx.beginPath();
      // connect via lines
      for(var i=0; i< totalCities-1; i++){
        ctx.moveTo(bestEver[i][0], bestEver[i][1]);
        ctx.lineTo(bestEver[i+1][0], bestEver[i+1][1]);
      }
      ctx.closePath();
      ctx.stroke();
    
      // swap things
      var i = Math.floor( Math.random()*cities.length);
      var j = Math.floor( Math.random()*cities.length);
      //cities = swap(cities, i, j);
      swap(cities, i, j);
    
      var d = calcDistance(cities);
      if(d < recordDistance){
        recordDistance = d;
        bestEver = cities.slice();
        console.log('recordDistance: ' + recordDistance);
      }
    }
    
    
    setup();
    draw();
    </script>
    </body>
    </html>
    
    
  • Hi,

    1: IYO, what 'd be the best way to wire to those Sck & Data pins ?

    Probably opendrain with a pullup. Are you sure you're using all 5v capable pins? You shouldn't use any marked with 3.3v on http://www.espruino.com/Original#pinout

    Could it be that your hardware SPI is just going too fast? With a long wire and a 1.5k pullup it might just not be enough to produce a clear signal. Do you have an oscilloscope?

    2: any way to get hardware SPI working to increase the refresh rate & lessen cpu usage ?

    Could be the output speed as above. It's worth noting that hardware SPI doesn't decrease CPU usage, so actually the gains from it are not as much as you might think.

    3: would 'compiled' code helps in getting more precise & faster 'scheduled updates' ?

    Maybe, but honestly there are other things I would try first like removing console.log in things that have to do fast, and maybe trying some simple benchmarking with getTime to see what's slow.

    Honestly I think for everyone I've seen that has tried to use compiled this far it's been a huge mistake, so I would avoid it if at all possible.

    4: am I driving these correctly at all ? ( or what may the 'spi behavior'* come from ? )

    As above really - it could be the SPI speed you're trying to use with the level shifting via resistor. If you have an oscilloscope it would be a huge help.

  • Hi there,

    1: I'm currently using B3 & B5 ( yup .. ), but I guess I'll switch to B13 & B15 or just use software SPI instead + 'opendrain'. I'll try the long wire + 1.5k pullup to 3.3V ( on both datat & clk or just one iyo ? ) - I can borrow an oscilloscope ( .. )

    2: understood for the "no that much gain" ;)

    3: yup for the logs ;p, and I'll try getTime benchmarking as you advise. Also I'll avoid compiled stuff for this particular goal ;)

    4: I 'll manage to get myself a scope & learn its many tricks, yet as far as I can tell, the following seemd to work ( even without 'opendrain' -> anyway, I'll go for safety with your above suggestions )

    // Nb: I have to digg SPI modes ..
    // Nb2: the LEDs seemed to respond even when passing "8000000" as baud ? ..
    //spi.setup({miso:B4, mosi:B5, sck:B3, mode:1,order:"msb", baud: 4000000}); // seems ok for software spi & hardware spi
      spi.setup({miso:B4, mosi:B5, sck:B3, mode:0,order:"msb", baud: 4000000}); // seems ok for software spi - mode 0||3, 1||2 ?
      // for SPI modes on APA102: https://cpldcpu.wordpress.com/2014/11/30­/understanding-the-apa102-superled/
      // for SPI modes in general ?
      // R: mode "1" was the 1st tested, seemed to work
      //    mode "0" seems to also work ?
    
  • I'd add pullups to both pins if you're trying to get a decent voltage swing on them.

    As far as SPI mode - honestly I don't know what the APA102 needs and I don't really have time to check, but if mode 0 works, use that - mode 0 is the 'standard' SPI mode that 99% of things use, so chances are it'll be that.

  • hello :)

    thanks for the hints ! ;)

    I'll have a try on this subject later today & 'll come back with the result

  • Hi there !

    back on this project ;p

    -> I received the said charger/booster and 'll test the pullups pins stuff ;)
    -> I also had answers from the Adafruit support team, as such:

    forget about voltage boosting and associated limitations and inefficiencies. You are better off powering both processor and pixels direct from your battery pack. We regularly run Neopixels and Dotstars directly from 3.7v LiPos

    https://forums.adafruit.com/viewtopic.ph­p?f=8&t=147139.

    So, before trying the said pullup thing, I have to try powering ~40 leds fully bright from one ( or more in parallel ? ) 3.7V 18650 batteries & powering an original Espruino board ( maybe later a Pico but I no longer have any handy ; ) ), yet for programming/flashing/debug reasons, I wish to be able to keep the USB connection to the laptop.

    I currently disconnect the battery jst connector when being connected using USB while the leds are getting power from the 3.7V battery & no problem has rosen yet, but I'm wondering how the board would react if I were to keep both USB and battery on JST connected ( the leds are getting power directly from the battery, not from the Bat pin, when usb is connected or not )
    Also, it seems I couldn't talk to the board when putting a 1000uF 16V cap across the battery power source & Gnd ? ( the board was recognized, but I couldn't get a working serial comm, even when using an external ftdi adapter & trying on hardware serial ports .. ( espruino original ) .. may it come from CTS/DTR left unconnected ? I'll have to try using bluetooth .. )

    This being said, I'm onto modding my current code to try the said ~40 leds, hoping I can have them 'blinking in sync' & last longer at least a minute ..

    ps: mode 0 seems to work reliably on my current setup, big thanks for the hint ;)

    last but not least, would you advise using external pullups when running off 3.7V & controlling 3.7V-powered leds ( without opendrain since no more needed ? I wonder how to fasten the said "voltage swing" .. )

    -> I'll see if I can adapt some "rainbow scrolleré onto those as a test ;p

  • Back with an alpha ( some things doesn't work as they should, and I'm still missing the finalized data ( timed mouth indices & mouths data ) ), but it's cleaner than previously ;)

    also, I managed to wreck my code somehow somewhere & now I can't control the last led any more :|

    if anyone's feeling brave enough to visually troubleshoot, be my guest ;)
    ( as always, I'll do so after some rest ;) )

    this being said, the pre-pre-pre-alpha code

    /* ==== Akali's Mask V0.1a ==== */
    
    // -- MODULES --
    // - little 'dotstar' module -
    // usage:
    // var dotStar = exports(spi); // pass spi obj
    //dotStar.write(ledsValues); // pass array of leds colors
    var exports = function (spi) {
      return { 
        write: function(ledsData){
          spi.write([0x00,0x00,0x00,0x00]); // start frame
          spi.write(ledsData); // led frames
          for(var y=0; y<(ledsData.length/2)/8; y++){ spi.write(0x00); } // end frame
        }
      };
    };
    
    // -- COLORS/PATTERNS HELPER --
    // R: APA102 are BGR for me
    function getColoredPattern(colR, colG, colB, start, end, clear) {
      var tstart = (start > 0)? start*4-3 : 0 || 0;
      var tend = end*4 || ledsBuff.length;
      for (var i=0;i<ledsBuff.length;i+=4) { // Q: is it where my 'one less led' comes from ? :/
        if(i>=tstart && i < tend){
          ledsBuff[i] = 0xe0+31;
          ledsBuff[i+1] = colB;
          ledsBuff[i+2] = colG;
          ledsBuff[i+3] = colR;
        } else if(clear) { // doesn't work ? :/
          ledsBuff[i] = 0xe0+31;
          ledsBuff[i+1] = 0;
          ledsBuff[i+2] = 0;
          ledsBuff[i+3] = 0;
        }
      }
      return ledsBuff;
    }
    
    // R: steps may (later ) be used to either:
    // - map '0' to '255, 0, 0, 0' & '1' to '255, 255, 255, 255' ( by def we store 1 & 0's, but lookup table set with R,G,B vals at each index can be used )
    //   Nb: we could then for example get the color of the item above a dot on Illustrator as an idx of its color in our 'shared' palette
    // - map 'R, G, B' to '255, B, G, R', what we currently do
    var colorsPalette = [[0, 0, 0], [255, 255, 255]];
    function getPattern(array, steps) {
      for (var i=0, y=0; i<ledsBuff.length;i+=4, y+=steps) {
        if(steps == 1){
          ledsBuff[i] = 0xe0+31;
          //var val = (array[y] == 1)? 255 : 0; // would only support on/ff instead of paletted idx which offer 255 choices
          ledsBuff[i+1] = colorsPalette[array[y]][2]; // B
          ledsBuff[i+2] = colorsPalette[array[y]][1]; // G
          ledsBuff[i+3] = colorsPalette[array[y]][0]; // R
        } else if(steps == 3){
          ledsBuff[i] = 0xe0+31;
          ledsBuff[i+1] = array[y+2]; // B
          ledsBuff[i+2] = array[y+1]; // G
          ledsBuff[i+3] = array[y]; // R
        } else if(steps == 4){
          ledsBuff[i] = array[y]; // brightness
          ledsBuff[i+1] = array[y+1]; // R
          ledsBuff[i+2] = array[y+2]; // G
          ledsBuff[i+3] = array[y+3]; // B
        }
      }
      return ledsBuff;
    }
    
    
    // -- LEDS WORKING CHECK --
    function checkLeds(delay){
      setTimeout(function(){
        dotStar.write(getColoredPattern(255, 0, 0, 0, ledsCount));
        setTimeout(function(){
          dotStar.write(getColoredPattern(0, 255, 0, 0, ledsCount));
          setTimeout(function(){
            dotStar.write(getColoredPattern(0, 0, 255, 0, ledsCount));
            setTimeout(function(){
              dotStar.write(getColoredPattern(255, 255, 255, 0, ledsCount));
              setTimeout(function(){
                dotStar.write(getColoredPattern(0, 0, 0, 0, ledsCount));
              },delay);
            },delay);
          },delay);
        },delay);
      },delay);
    }
    
    
    // -- MOUTHS --
    // TODO: generate dummys & finalize the leds mouths in Illustrator
    // for finalized: how many ? number of leds in each ? which leds are on for each ?
    // R: since we have to store at least 8 different mouths ( and by default, only on/off states of leds in those ),  storing 1's & 0's ( or paletted color idx)
    //    make more sense to lessen the space taken
    // ex: leds * BrRGB * mouths = 45 * 4 * 8 = 1440 bytes ( with brightness control )
    //     leds * RGB * mouths = 45 * 3 * 8 = 1080 bytes ( any color, no brightness control )
    //     leds * paletted idx * mouths = 45 * 1 * 8 = 360 bytes ( 255 possible colors )
    var ledsBuff = new Uint8ClampedArray(45*4); // used as tmp to fill in /map in values while keeping constant brightness
    var mouthsLedsArrays = [
      //new Uint8ClampedArray([0,0,0,0,0,0,255,255,2­55,255,255,255,255,255,255,0,0,0,0,0,0,0­,0,0,0,0,0,255,255,255,255,255,255,255,2­55,255,0,0,0,0,0,0]),
      //new Uint8ClampedArray(45*3),
    ];
    // dummies generator, to test stuff before finalizing the mouths leds arrays
    function generateDummies(howMany){
      for(var i=0; i<howMany; i++){ // R/ testing with 45 leds
        console.log('pushing dummy: [' + (i*9) + '..' + (i*9+9) + ']' );
        //ledsBuff = new Uint8ClampedArray(45*4); // fill buff with 0's
        getColoredPattern(0, 0, 0, 0, ledsCount); // same as above, faster way that doesn't re-allocate a clamped array
        //mouthsLedsArrays.push(getColoredPatter­n(255, 255, 255, i*9, i*9+9)); // push 9/45 of leds to our array ( uses buff internally ) - increasing
        //mouthsLedsArrays.push(getColoredPatter­n(255, 255, 255, i*9, i*9+9)); // push 9/45 of leds to our array ( uses buff internally ) - moving .. not ? :/
        mouthsLedsArrays.push(getColoredPattern(­255/9*i, 255/9*i, 255/9*i, i*9, i*9+9)); // push 9/45 of leds to our array ( uses buff internally )
      }
    }
    
    
    // -- TIMING --
    // TODO: get final timings ( absolute or relative )
    // R: we have generated 5 mouths for our debug, each with 9 leds
    var absTimeMouthsIdxsArray = [
      //[<ms_from_start>, <mouth_idx>]
      [37,    0],
      [38,    1],
      [40,    2],
      [45,    3],
      [45.2,  4],
      [45.5,  3],
      [45.7,  2],
      [45.8,  1],
      [45.9,  0],
      [45.95, 1],
      [45.99, 2],
      [46,    3],
      [50,    4]
    ];
    // R: to map abolute times to relative timing in ms: relTime = (absTime * 1000) - (1stAbsTime * 1000)
    var reTimeMouthsIdxsArray = [];
    for(var i=0; i< absTimeMouthsIdxsArray.length; i++){
      var relTime = (i === 0) ? ((absTimeMouthsIdxsArray[i][0] * 1000) - (absTimeMouthsIdxsArray[i][0] * 1000)) :
                                 (absTimeMouthsIdxsArray[i][0] * 1000) - (absTimeMouthsIdxsArray[i-1][0] * 1000);
      reTimeMouthsIdxsArray[i] = [relTime, absTimeMouthsIdxsArray[i][1]];
    }
    
    
    // -- ANIMATION --
    // transition between mouths
    function nxtMouth(){
      if(currMouthAnimIdx+1 < reTimeMouthsIdxsArray.length){ // didn't reach end of array yet
        currMouthAnimIdx = currMouthAnimIdx+1;
        timr = setTimeout(function(){
          currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
          // useful for generated dummies
          console.log('currMouthIdx: ' +currMouthAnimIdx);
          //dotStar.write(getPattern(currMouth, 4));// update leds - we pass an array of paletted idx, rgb values or BrRGB values & a 'step' to indicate format
          getColoredPattern(0, 0, 0, 0, ledsCount); // clear buffer 
          dotStar.write(getColoredPattern(0, 0, 100, 0, currMouthAnimIdx*4, true)); // debuuuuug
          // for final impl
          //dotStar.write(getPattern(currMouth, 1));// update leds - we pass an array of paletted idx, rgb values or BrRGB values & a 'step' to indicate format
          nxtMouth(); // schedule next call
        }, reTimeMouthsIdxsArray[currMouthAnimIdx][­0]);
      } else {
        timr = -1;
        console.log('end of mouth timed array reached -> animation complete !');
        resetState();
      }
    }
    
    
    // -- BUTTON PRESS HANDLER: START/RESET --
    function handleBtnPress(){
      if(animating){ // currently animating: reset
        resetState();
      }
      else { // currently stopped: animate
        animating = true;
        console.log('animating !');
        nxtMouth(); // TODO: uncomment once we have final mouths arrays or generated dummys
      }
    }
    
    function resetState(){
      console.log('resetting !');
      if(timr !== -1){
        clearTimeout(timr);
        timr = -1;
      }
      currMouthAnimIdx = 0;
      currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
      dotStar.write(getColoredPattern(0, 0, 0, 0, ledsCount)); // clear
      animating = false;
    }
    
    
    // -- SETUP --
    // - modules -
    var spi;
    var dotStar;
    // - pins -
    var btn = BTN1;
    var dotStarSck = B3;
    var dotStarMosi = B5;
    // - watches -
    var btnW;
    // - vars -
    var ledsCount = 45;
    var animating = false;
    var timr = -1;
    var currMouthAnimIdx = 0; // where we're at in the 'reTimeMouthsIdxsArray'
    //var currMouthIdx = 0;
    var currMouth = mouthsLedsArrays[reTimeMouthsIdxsArray[c­urrMouthAnimIdx][1]];
    
    
    function onInit(){
      // setup modules
      spi = SPI1;
      spi.setup({miso:B4, mosi:dotStarMosi, sck:dotStarSck, mode:0,order:"msb", baud: 4000000});
      //spi.setup({mosi:dotStarMosi, sck:dotStarSck, mode:0,order:"msb", baud: 4000000});
      dotStar = exports(spi);
      dotStar.write(getColoredPattern(0, 0, 0, 0, ledsCount)); // start with all leds fully off
    
      // setup button
      //pinMode(btn, 'input_pullup');
      btnW = setWatch(function(e) { handleBtnPress(); }, btn, { repeat:true, edge:'rising', debounce: 100 });
      
      // debug logs
      console.log('currMouthAnimIdx : ' + currMouthAnimIdx);
      console.log('currMouth : ' + currMouth);
      console.log('reTimeMouthsIdxsArray : ' + reTimeMouthsIdxsArray);
      console.log('reTimeMouthsIdxsArray.lengt­h : ' + reTimeMouthsIdxsArray.length);
      
      // generate dummies to test until we have final mouths
      generateDummies(5); // we pass how many we want
    
      // quick leds feedback
      checkLeds(1000); // we pass the delay between color change
      
      // feedback - fully booted indication
      digitalWrite(LED3, 1);
    }
    
  • No problem about running both off a LiPo. I do it all the time.

    In fact the docs in the reference page for the neopixel library say:

    Espruino devices tend to have 3.3v IO, while WS2812/etc run off of 5v. Many WS2812 will only register a logic '1' at 70% of their input voltage - so if powering them off 5v you will not
    be able to send them data reliably. You can work around this by powering the LEDs off a lower voltage (for example 3.7v from a LiPo battery)

    The original board itself should work fine when plugged into USB and the battery via JST. It's got circuitry in there for it, and in fact newer 'Original' boards even have pads in there to solder on a LiPo charger :)

    The only one to watch out for is the Espruino WiFi. That has no JST connector and no battery switchover circuitry - so you can't just connect a battery to the power pins and plug in USB because you'll basically be shorting the battery across 5v!

  • All right, that one good news ! :)

    -> I was currently disconnecting the battery when connecting over USB but it seemed I could control the leds "ok" ( but maybe not as reliabiliy as when running everything off the battery ;p )

    I'm currently working on how to optimise the leds/leds strip chunks so that I have the shortest non-crossing traces across the board ( I'm quite afraid using short wires 'll be quite hard since it'd unsolder one end while solderng the other due to heat .. I experienced that when joining two strip with a wire between both .. )
    I plan to try two different approaches:

    • 1st: "Pyralux" flexible pcb for proto ( or cheaper flexible fab pcb ? )
    • 2nd non-flexible pcbS ( as "sections" soldered together to form the circuit )
      The ability to get two layers flexible boards 'd be awesome ;p

    Ps: do you think I may have killed the last led on the strip by getting electrically shocked by touching it or is it just some code-side pb ? ( fact is: I tried loading previous versions of my code: still same trouble with the last one, aside from the scheduling maybe not as fast as expected & my test patterns not doing correctly .. yup, I have some mess to fix .. ) or would it come from me controlling leds powered via bat+ pin & no battery while connecting over usb ? :| .. ( I'm not sure how to test if an APA102 is dead or not, aside from directly connecting to it & sending controls to it only - thus this 'd mean a crack in the strip connection, which I doubt since it's not where I joined chuncks .. )

    -> I'll check that once done with my mapping problems ;)

    ps: do you / does anyone have any knowledge / experience with "breadth-first search" & the TSP problem ? ( I could really get some help on either writing or modding an Illustrator plugin to do some related stuff .. )

    thanks for the hints :)
    +

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

LoL Akali Mask ( battery-powered APA102 setup )

Posted by Avatar for stephaneAG @stephaneAG

Actions