Controlling LED chain via Bluetooth | indoor bouldering App

Dear coding buddies,

Since about one year, we are working on a very nice project. Summed up shortly, it is about indoor bouldering (climbing), we made a concept for DIY climbing walls and I created the Android app for it. Now, I constructed an easy Arduino-based LED System which lights up the holds which the climber should take in the respective climbing route.

So far, it works well. However, to be honest, I'm a very bad coder. I programmed the Android app using a graphical programming environment. Now, with the Arduino, there is no way around coding. So I had to code again. The point is - I know my Arduino code is crap. It works somehow, but there is so much potential for enhancements.

If you please have a look at my scratch, you will surely find a lot of things that can be coded better. Maybe you can help me with this. I'm sure I will learn a lot if you can guide me.

The whole project is a non-profit comunity work and we already spent uncountable hours for it, but it's a real cool project and we have a lot of fun. If you are interested in it, have a look at www.i-nowa.com (German). Also, I'm going to make games for kids with the LED System (eg. a snake that you have to climb away from). It's an open project, maybe someone is interested in joining? Ideas and help are always appreciated!

So, here's the scratch (some parts are even not tested yet). Please don't be too cruel to me :wink:

//  ~~~     iNoWa - the indoor NorthWall.   ~~~
//  NANO-BLE Firmware für die
//  Steuerung des LED Systems
//
//  Version 1.0.0
//  03.01.2022
//##############################################

#include "FastLED.h"
#include <JC_Button.h>                                        // https://github.com/JChristensen/JC_Button
#define NUM_LEDS 154
#define RDY_LED 2
#define TST_BUTTON 11
#define PULLUP true                                           // To keep things simple, we use the Arduino's internal pullup resistor.
#define INVERT false                                           // Since the pullup resistor will keep the pin high unless the
                                                              // switch is closed, this is negative logic, i.e. a high state
                                                              // means the button is NOT pressed. (Assuming a normally open switch.)
#define DEBOUNCE_MS 100
#define SPK_PIN 12
#define DATA_PIN 13

int brightness = 254;

CRGB leds[NUM_LEDS];
Button Testbutton(TST_BUTTON, PULLUP, INVERT, DEBOUNCE_MS);  // Declare the button.

int fadeAmount = 5;  // Set the amount to fade I usually do 5, 10, 15, 20, 25 etc even up to 255.
int count;
int nbr = 0;
char inByte;  // incoming serial byte
String text = "";
String data[25][2];

void setup() {

  Serial.begin(9600);                                       // Start serial connection
  delay(1000);                                              // Soft startup to ease the flow of electrons.
  FastLED.addLeds<WS2811, DATA_PIN, RGB>(leds, NUM_LEDS);   // Configure LED chain
  FastLED.setBrightness(brightness);                        // set your brightness as desired

  startupLEDsTest();  // optional R,G,B test

  count = 0;
  
  // Start Melody
  tone(SPK_PIN, 264);
  delay(300); 
  tone(SPK_PIN, 330); 
  delay(300); 
  tone(SPK_PIN, 396); 
  delay(450); 
  noTone(SPK_PIN); 

  FastLED.clear();

}

void startupLEDsTest() {
  // startup blink test to confirm LEDs are working.
  FastLED.setBrightness(127);
  fill_solid(leds, NUM_LEDS, CRGB(255,0,0));  // fill red
  FastLED.show();
  delay(1000);
  fill_solid(leds, NUM_LEDS, CRGB(0,255,0));  // fill green
  FastLED.show();
  delay(1000);
  fill_solid(leds, NUM_LEDS, CRGB(0,0,255));  // fill blue
  FastLED.show();
  delay(1000);
  FastLED.clear();
  fill_rainbow( leds, NUM_LEDS, 0, 7);        // rainbow
  FastLED.show();
  delay(2000);
  FastLED.setBrightness(brightness);          // reset brightness to master level
  FastLED.clear();
  FastLED.show();

} //end_startupLEDsTest

void checkButton() {

  Testbutton.read();
  
  if (Testbutton.wasReleased()) {  // Test button pressed
    Serial.println(nbr);

    nbr++;    // increase button press counter
    
    if (nbr == 5) {
      nbr = 0;
      FastLED.clear();
      tone(SPK_PIN, 864);   // signal for mode 0
      delay(300); 
      noTone(SPK_PIN);
    }    
    else if (nbr == 1) {
      fill_solid(leds, NUM_LEDS, CRGB(255,0,0));  // fill red
      tone(SPK_PIN, 864);   // signal for mode 1
      delay(70); 
      noTone(SPK_PIN);
    }
    else if (nbr == 2) {
      fill_solid(leds, NUM_LEDS, CRGB(0,255,0));  // fill green
      for (int i=0;i<2;i++){  
        tone(SPK_PIN, 864);   // signal for mode 2
        delay(70); 
        noTone(SPK_PIN);
        delay(70);
      }   
    }
    else if (nbr == 3) {
      fill_solid(leds, NUM_LEDS, CRGB(0,0,255));  // fill green
      for (int i=0;i<3;i++){  
        tone(SPK_PIN, 864);   // signal for mode 3
        delay(70); 
        noTone(SPK_PIN);
        delay(70);
      }
    }
    else if (nbr == 4) {
    fill_rainbow( leds, NUM_LEDS, 0, 7);        // rainbow
    for (int i=0;i<4;i++){  
        tone(SPK_PIN, 864);   // signal for mode 4
        delay(70); 
        noTone(SPK_PIN);
        delay(70);
      }
   }
   
   FastLED.show();    // light it up  
 }
  
}

//glitter effect
void addGlitter( fract8 chanceOfGlitter) {
  if( random8() < chanceOfGlitter) {
    leds[ random16(NUM_LEDS) ] += CRGB::White;}
}

void loop() {
  
  digitalWrite(RDY_LED, HIGH);       // System signals be to be ready
  
  char Buf[50];

  //checkButton();                     // check if Testbutton is pressed
  
  if (Serial.available()) {          // check for incoming data --> if available
    delay(20);                       // just wait a little bit for more characters to arrive
    while (Serial.available())
    {
      inByte = Serial.read();        // store incoming data

      if ((!(inByte == '/')) && (!(inByte == ':')) && (!(inByte == '#')) && (!(inByte == '!'))) {
        text.concat(inByte);
      }
      if (inByte == ':') {           // number:color seperator
        data[count][0] = text;       // here is a number
        text = "";
      }
      if (inByte == '/') {          // hold separator
        data[count][1] = text;      // here is a char
        text = "";
        count++;
      }
      if (inByte == 'P') {          // pulsate signal for ambience light mode
        data[0][1] = text;
        text = "";
        count++;
      }
      if (inByte == 'S') {          // sparkle signal for ambience light mode
        data[1][1] = text;
        text = "";
        count++;
      }
      if (inByte == '!') {          // signal for ambience light mode
        FastLED.clear();
        fill_solid(leds, NUM_LEDS, CRGB(data[0][0].toInt(),data[1][0].toInt(),data[2][0].toInt()));  // fill in color user has set
        FastLED.setBrightness(data[3][0].toInt());                                                   // set brightness user has set
        if (data[0][1] == "S"){
          addGlitter(30);
        }
        // signal that data has successfully been processed
        tone(SPK_PIN, 2000);
        delay(100);
        tone(SPK_PIN, 1000); 
        delay(100); 
        noTone(SPK_PIN); 

        FastLED.show();         // light it up
      }
      if (inByte == '#') {          // end signal (boulder mode)
        for(int i=0; i<count; i++){
          Serial.print(data[i][0]);
          Serial.println(data[i][1]);
      }
      
      FastLED.clear();
      FastLED.show();

        // fill up pixel array
        for (int i = 1; i < 155; i++) {    
          for (int j = 0; j < count; j++) {
            if (data[j][0].toInt() == i) {
               if (data[j][1] == "g") {
                leds[i-1] = CRGB::Green;
              }
              else if (data[j][1] == "b") {
                leds[i-1] = CRGB::Blue;
              }
              else if (data[j][1] == "l") {
                leds[i-1] = CRGB::Purple;
              }
              else if (data[j][1] == "r") {
                leds[i-1] = CRGB::Red;
              }
            }
          }
        }
        count = 0;

        // signal that data has successfully been processed
        tone(SPK_PIN, 1000);
        delay(100);
        tone(SPK_PIN, 2000); 
        delay(100); 
        noTone(SPK_PIN); 

        FastLED.show();         // light it up
        
      }
    }
  }
  if (data[0][1] == "P"){     // pulsate signal for ambience light
    for(int i = 0; i < NUM_LEDS; i++ ){
       leds[i].fadeLightBy(brightness);
    }
    FastLED.show();
    brightness = brightness + fadeAmount;
    // reverse the direction of the fading at the ends of the fade: 
    if(brightness == 0 || brightness == 255){
      fadeAmount = -fadeAmount ; 
    }    
    delay(9);  // This delay sets speed of the fade. I usually do from 5-75 but you can always go higher.
  }
}

Really, nobody? Maybe too special?

Anyway, the posted sketch is not working completely, like I wrote before.

Working part:
"boulder mode", that means when a boulder route is to be shown. In this case, the App sends a string like 140:g/120:g/55:b/44:l/67:r/# . This works fine so far but as I also said, I know my code is ineffective (uses too much mem for the vars), but I'm lacking the coding experience in C.

Not working so far:
The "ambience light mode" that means that the LEDs are used for lighting the room or just for sowing light effects. I fixed some parts and by now, it works that the set color is shown by the LEDs with the given brightness; however, "glitter" and "pulsate" are not working. Maybe you could please give me a hint what I'm doing wrong. Thank you!

Here's the recent sketch:

//  ~~~     iNoWa - the indoor NorthWall.   ~~~
//  NANO-BLE Firmware für die
//  Steuerung des LED Systems
//
//
//  Version 1.0.1
//  07.01.2022
//##############################################

#include "FastLED.h"
#include <JC_Button.h>                                        // https://github.com/JChristensen/JC_Button
#define NUM_LEDS 154
#define RDY_LED 2
#define TST_BUTTON 11
#define PULLUP true                                           // To keep things simple, we use the Arduino's internal pullup resistor.
#define INVERT false                                           // Since the pullup resistor will keep the pin high unless the
                                                              // switch is closed, this is negative logic, i.e. a high state
                                                              // means the button is NOT pressed. (Assuming a normally open switch.)
#define DEBOUNCE_MS 100
#define SPK_PIN 12
#define DATA_PIN 13

int brightness = 254;

CRGB leds[NUM_LEDS];
Button Testbutton(TST_BUTTON, PULLUP, INVERT, DEBOUNCE_MS);  // Declare the button.

int fadeAmount = 5;  // Set the amount to fade I usually do 5, 10, 15, 20, 25 etc even up to 255.
int count;
int nbr = 0;
char inByte;  // incoming serial byte
String text = "";
String data[25][2];

void setup() {

 pinMode(RDY_LED, OUTPUT);

  Serial.begin(9600);                                       // Start serial connection
  delay(1000);                                              // Soft startup to ease the flow of electrons.
  FastLED.addLeds<WS2811, DATA_PIN, RGB>(leds, NUM_LEDS);   // Configure LED chain
  FastLED.setBrightness(brightness);                        // set your brightness as desired

  startupLEDsTest();  // optional R,G,B test

  count = 0;
  
  // Start Melody
  tone(SPK_PIN, 264);
  delay(300); 
  tone(SPK_PIN, 330); 
  delay(300); 
  tone(SPK_PIN, 396); 
  delay(450); 
  noTone(SPK_PIN); 

  FastLED.clear();

}

void startupLEDsTest() {
  // startup blink test to confirm LEDs are working.
  FastLED.setBrightness(127);
  fill_solid(leds, NUM_LEDS, CRGB(255,0,0));  // fill red
  FastLED.show();
  delay(1000);
  fill_solid(leds, NUM_LEDS, CRGB(0,255,0));  // fill green
  FastLED.show();
  delay(1000);
  fill_solid(leds, NUM_LEDS, CRGB(0,0,255));  // fill blue
  FastLED.show();
  delay(1000);
  FastLED.clear();
  fill_rainbow( leds, NUM_LEDS, 0, 7);        // rainbow
  FastLED.show();
  delay(2000);
  FastLED.setBrightness(brightness);          // reset brightness to master level
  FastLED.clear();
  FastLED.show();

} //end_startupLEDsTest

void checkButton() {

  Testbutton.read();
  
  if (Testbutton.wasReleased()) {  // Test button pressed
    Serial.println(nbr);

    nbr++;    // increase button press counter
    
    if (nbr == 5) {
      nbr = 0;
      FastLED.clear();
      tone(SPK_PIN, 864);   // signal for mode 0
      delay(300); 
      noTone(SPK_PIN);
    }    
    else if (nbr == 1) {
      fill_solid(leds, NUM_LEDS, CRGB(255,0,0));  // fill red
      tone(SPK_PIN, 864);   // signal for mode 1
      delay(70); 
      noTone(SPK_PIN);
    }
    else if (nbr == 2) {
      fill_solid(leds, NUM_LEDS, CRGB(0,255,0));  // fill green
      for (int i=0;i<2;i++){  
        tone(SPK_PIN, 864);   // signal for mode 2
        delay(70); 
        noTone(SPK_PIN);
        delay(70);
      }   
    }
    else if (nbr == 3) {
      fill_solid(leds, NUM_LEDS, CRGB(0,0,255));  // fill green
      for (int i=0;i<3;i++){  
        tone(SPK_PIN, 864);   // signal for mode 3
        delay(70); 
        noTone(SPK_PIN);
        delay(70);
      }
    }
    else if (nbr == 4) {
    fill_rainbow( leds, NUM_LEDS, 0, 7);        // rainbow
    for (int i=0;i<4;i++){  
        tone(SPK_PIN, 864);   // signal for mode 4
        delay(70); 
        noTone(SPK_PIN);
        delay(70);
      }
   }
   
   FastLED.show();    // light it up  
 }
  
}

//glitter effect
void addGlitter( fract8 chanceOfGlitter) {
  if( random8() < chanceOfGlitter) {
    leds[ random16(NUM_LEDS) ] += CRGB::White;}
}

void loop() {
  
  digitalWrite(RDY_LED, HIGH);       // System signals be to be ready
  
  char Buf[50];

  //checkButton();                     // check if Testbutton is pressed
  
  if (Serial.available()) {          // check for incoming data --> if available
    delay(20);                       // just wait a little bit for more characters to arrive
    while (Serial.available())
    {
      inByte = Serial.read();        // store incoming data
      Serial.print(inByte);
      if ((!(inByte == '/')) && (!(inByte == ':')) && (!(inByte == '#')) && (!(inByte == '!')) && (!(inByte == 'S')) && (!(inByte == 'P'))) {
        text.concat(inByte);
      }
      if (inByte == ':') {           // number:color seperator
        data[count][0] = text;       // here is a number
        text = "";
      }
      if (inByte == '/') {          // hold separator
        data[count][1] = text;      // here is a char
        text = "";
        count++;
      }
      if (inByte == 'P') {          // pulsate signal for ambience light mode
        data[0][0] = 'P';
        text = "";
      }
      if (inByte == 'S') {          // sparkle signal for ambience light mode
        data[1][0] = 'S';
        text = "";
      }
      if (inByte == '!') {          // signal for ambience light mode
        data[count][1] = text;      // here comes the brightness value
        text = "";
        count = 0;
        FastLED.clear();
        
        Serial.println();
        
        fill_solid(leds, NUM_LEDS, CRGB(data[0][1].toInt(),data[1][1].toInt(),data[2][1].toInt()));  // fill in color user has set
        
        Serial.print(data[0][1].toInt());Serial.print(data[1][1].toInt());Serial.print(data[2][1].toInt());
        Serial.println(data[3][1].toInt());
        
        FastLED.setBrightness(data[3][1].toInt());                                                   // set brightness user has set

        Serial.println(data[1][0]);
        // want to add some glitter (sparkle)?
        if (data[1][0] == "S"){
          addGlitter(30);
        } 

        FastLED.show();         // light it up
      }
      if (inByte == '#') {          // end signal (boulder mode)
        for(int i=0; i<count; i++){
          Serial.print(data[i][0]);
          Serial.println(data[i][1]);
      }
      
      FastLED.clear();
      FastLED.show();

        // fill up pixel array
        for (int i = 1; i < 155; i++) {    
          for (int j = 0; j < count; j++) {
            if (data[j][0].toInt() == i) {
               if (data[j][1] == "g") {
                leds[i-1] = CRGB::Green;
              }
              else if (data[j][1] == "b") {
                leds[i-1] = CRGB::Blue;
              }
              else if (data[j][1] == "l") {
                leds[i-1] = CRGB::Purple;
              }
              else if (data[j][1] == "r") {
                leds[i-1] = CRGB::Red;
              }
            }
          }
        }
        count = 0;

        // signal that data has successfully been processed
        tone(SPK_PIN, 1000);
        delay(100);
        tone(SPK_PIN, 2000); 
        delay(100); 
        noTone(SPK_PIN); 

        FastLED.show();         // light it up
        
      }
    }
  }
  if (data[0][0] == "P"){     // pulsate signal for ambience light
    for(int i = 0; i < NUM_LEDS; i++ ){
       leds[i].fadeLightBy(brightness);
    }
    FastLED.show();
    brightness = brightness + fadeAmount;
    // reverse the direction of the fading at the ends of the fade: 
    if(brightness == 0 || brightness == 255){
      fadeAmount = -fadeAmount ; 
    }    
    delay(9);  // This delay sets speed of the fade. I usually do from 5-75 but you can always go higher.
  }
}

Hello.

I watched the videos . Looks like a great activity.

As for your code, I can make two suggestions.

Where you use multiple "if...else if " -- see if the "case" will do the job better.

The loop void in the first sketch is rather long. It's better if it is a small number of procedure calls and the detailed work done inside separately defined procedures. The loop void will then be easier to understand (with good commenting).

Thank you @HillmanImp !

I will implement those recommendations. Also, thank you for you interest in our sport! Climbing is really a nice thing and everyone can profit from it.

For the other issues, I hope someone can advise further.

Greetings

Your Serial Input reading routine is not the way it is typically done. Please review Serial Input Basics which is the fundamental tutorial about how to read a message and parse the data from it.

What Arduino are you using?

Using String objects instead of null terminated character arrays can create issues.

I do abseiling and tree climbing. I would love to try out your home bouldering wall.

I think your project is clever.

The suggestion from cattledog is a good one -- read the tutorial on serial comms.

You have a couple of advantages -- the serial data transmitted is short and it is only sent when someone presses a button on the phone. The data in each "sentence" sent is not much and will fit into the Arduino serial buffer which is 50 bytes. So you won't be losing characters as can happen if the sentences are longer or they are continuously ongoing.

As catteldog says, advanced users use char arrays to store the incoming serial data rather than strings. But I think you'll be ok as it isn't a high data load. You might like to send the sentence terminating with a newline and receive it with "(Serial.readStringUntil('\n')". That way you don't have to worry about the time between incoming characters.

It's also advised to NOT use delay() when processing serial data. Use millis().

I suggest you concentrate on reducing the size of the setup and loop blocks of code. Its good if they can be easily read -- the detailed work should not be seen. For instance, you could replace the Melody statements with a simple function call "PlayMelody()" and put the detailed code in a separate procedure. That way we don't see the detail every time we look at the setup procedure. Apply that tactic whenever the code starts to "blow out".

You could also pass some parameters to PlayMelody() so different melodies could be played by the one procedure. You have one melody for the start and another for "data successfully processed". Just pass the freq and delay duration as parameters. Apply that tactic as much as possible -- when repeating similar operations write one procedure to serve different situations rather than writing the "same" code for each situation.

You seem to be declaring most parameters at top of sketch. This makes them global. Only do this if you really need them to be global. I think each of "count", "nbr", "text" and "inbyte" are only used in one procedure each. So they are not needed to be global, so declare them inside the procedure that uses them. It's dangerous to have them global as you might inadvertently change them outside the procedure and not be aware of it.

Are you making new versions of the sketch at a rapid rate? You have identified this version at top of source code "Version 1.0.1 07.01.2022". That's good practice. But that info does not exist in the compiled code loaded into the Arduino. If you put a "serial.print" statement in setup() containing the version number & date, then that will appear in the Arduino IDE monitor. So you will be able to tell what version you have running in the Arduino.

What did you use to build the Android app? MIT App Inventor? I use that.

That's correct, I know. I have to tell you, I started from 0. I tried out what happens and finally found a way it works, but not "elegant". First, I tried to send the full string at once and then noticed that the Arduino can not buffer all the text. So I switched the strategy and now I'm sending packets, like 124:g/, and the the next one untikl the last. That's why I concat .

Well I did, but I didn't fully understand, that's why I asked for help. But thanks for the hint. I hoped someone can maybe show me in code with my sketch a practical example of how it should be done. But, I will try out and I hope I will find the right way.

Anyone maybe please can advise me with that, too?

Don't know where you're living, but you're invited to come and try out :wink:

Thank you!

That' s the point. My messages are longer, so I send them in packets. Also, as I need to send different types of string patterns for the different operating modes, so I need to be flexible with the data type in the array.

Me too, AI2. I'm a systems administrator in my profession and not a coder, so I really liked the way of programming with blocks. It's easy and comfortable, you don't need to care about any language specific things etc. and it's really fun to implement logic and create a working, complex app this way.

Will do that, thank you for all of your suggestions and your time for reading and writing!!

At the moment, my strategy to signal end - terminate the massage - is to send chars like # or ! as termination for different operating modes.
I partly understand your suggestions to process the data in the "fine" way, but I suppose
I need to think more until I know what to do in code.

It's an Arduino Nano 33 BLE clone, it's called "BLE-NANO" from "KeyWish". I like it, it's almost half the price of the original one. However, there are no specific BLE header libraries, it seems. That's a pitty because if there was this library, the whole bluetooth data transmission would be much easier. The BLE libraries for the original Arduino Nano BLE are not working, unfortunately.

It's an Arduino Nano 33 BLE clone, it's called "BLE-NANO" from "KeyWish". I like it, it's almost half the price of the original one. However, there are no specific BLE header libraries, it seems. That's a pitty because if there was this library, the whole bluetooth data transmission would be much easier. The BLE libraries for the original Arduino Nano BLE are not working, unfortunately.

I would ask if the Android app you wrote is for BLE or classic bluetooth? If BLE, why are you using that?

If indeed you are fixed with BLE, then the BLE-NANO and the similar DFRobot Bluno nano are difficult to use dual chip mergers of of an AT328 and a CC2540. I would avoid these devices, and get a more truely integrated BLE device lke on the Nano 33 BLE or an ESP32. There is better library support and more flexibility of use.

I am having trouble with receiving data from a GPS module. With some coding schemes I can reliably receive the sentences I need, but with other schemes I lose critical data. I'm looking for a solution too.

Others have discussed this problem in several forum discussions over the years. Something I've learned is that when the buffer fills, new data does NOT go on to the buffer, replacing data that was there first. The new data is lost.

The example below is interesting. It uses Serial.event() and looks simple. Robin's tutorial is not in favour of using Serial.event().

Serial is slow, Arduino is fast by comparison. As long as your code is non blocking and doesn't use delay, it should be easy for the Arduino to keep up.

That said, even at the glacial speed of 9600 baud, that's still ~960 characters per second worst case, which means that it will only take 66mS to fill the 64 byte serial receive buffer, so you need to check for input and read what's there on every iteration of loop and that loop has to be fast.

SerialEvent is just a slightly obfuscated way of doing this. I prefer to do an explicit read in loop itself.

You may find if you have a lot going on in loop, that you need to read serial every time, but do only one of your other tasks on each iteration.

Yep, I get that the Arduino is a good deal faster than serial. But that just befuddles me more as it suggests that it ought to be easy.

If I just read a line and print it to the monitor and do nothing else, it works perfectly.

It's when I do other things between reading the serial that problems arise (as you warn).

I have been wondering if printing to the monitor is a process that interferes with the serial process.

I'll get there -- I only need to get two sentences reliably and I did achieve that but now I've lost it.

Could be - there is a 64 byte output buffer too and if you fill it, your code blocks. It helps if you use the fastest baud rate the Arduino can reliably manage. That'll be way faster than the GPS is sending.

Thanks. That's something definite I can try.

Sleep time.

What do you mean? Do nothing? Do the wrong thing?

Add some serial prints to verify that they're actually being called.

Because all newer devices use that (BT 4.2). I'm not happy with it due to fine location requirement, however, it's commonly used so I used it, too. Also, I couldn't find a cheap Arduino with Classic BT onboard.

Yes, they do nothing at the moment. "Glitter" is called, I checked it, I'll do with "pulsate", too, but I'm quite sure my algorith is just crap and I couldn't find out how to do until now

May I ask you to split that up in an own thread (@mods)? Otherwise, it will be very confusing here with two separate topics. Thank you

Yes, of course.

It wasn't neccessary to delete your posts but thank you for yor kindness!

No, that's fine. It is a different topic and this topic is yours.

I've put my problem in a new topic in Programming.

BTW, can your app save a particular route?

Yes of course. Users can share their routes with all others. The app uses a MySQL database as backend, so I would call it "cloud bouldering" :wink:

The Fast Led library has Glitter examples. Have you tried them?

You may find it easier to work on both your "glitter" and "pulsate" issues in a simplified environment without the communication.

Indeed, you appear to have a workable solution to the 20 byte message limitations of the BLE packet size and the need to send and assemble multiple packets.

Do you have issues other than the fast led routines?