Addressable LED Multi-function Control Box

Hi.

I'm looking to build a control box for a set of WS2812B addressable LEDs.

The goal is to be able to control the lights in manner that feels like you're 'playing' them, almost like an instrument. I'm hoping to achieve this with some buttons that trigger different light patterns when pressed, and encoders to adjust different pattern parameters like hue, pattern rate etc.

The image shows a basic concept of the front panel to give a rough idea of what I'd like the final project to look like. I've also added some more in-depth descriptions of the different functions I would like the box to have.

I'm a bit of a novice when it comes to coding, so wanted to know if the proposed idea is feasible, and ultimately, where to start. I think a few options on how others would go about doing this would help me get the ball rolling :slight_smile:

Thanks for taking the time to read!

Adam

I'd use a theremin input to generate light intensities and colors.

That would be cool, but I'm going for something more like a launch pad/dj mixer feel

Cool project, mildly ambitious.

Do you need to have individual rotary encoders for each F/X slot?

Since they use relative motion for control, you could have just three, which would apply only to the effect that was, um, in effect.

If they were potentiometers, I could see wanting to adjust an effect before switching to it, this might be possible with the rotary encoders (using all from your design) if there was another means to see the current settings.

Are you planning any kind of additional display that shows what's going on and how things are set up?

a7

I receive a 2 channel audio from the music being played and flash a leds strip 6 different base colors and varying light intensity based upon frequency strength.

Thanks! I thought it might be....

My main thoughts behind individual encoders are 1. I'm not sure if you can control a number of individual variables through one encoder (e.g. hue value for FX1, 2, 3 and 4) separately
and 2. (albeit a little gimmicky) I think more things to turn = more fun, and in terms of user experience, knowing each FX has dedicated encoders makes things more straightforward?

So to answer, yes and no... it depends how achievable each option would be I guess.

I did consider putting in a display further down the line, but with individual hardware controls for each effect I didn't think it was that necessary.

However, if I went down the route of having just three encoders, a display would make a lot more sense.

In the future I would like to have an audio-in to be used for BPM detection, then map the FX rates to the BPM.... a little further down the line I reckon

When you get around to it perhaps the code below will lend to.

#include <WiFi.h>
#include <PubSubClient.h>
#include "certs.h"
#include "sdkconfig.h"
#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "esp_system.h" //This inclusion configures the peripherals in the ESP system.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"
#include "freertos/event_groups.h"
#include <Adafruit_NeoPixel.h>
#include "AudioAnalyzer.h"
////
/* define event group and event bits */
EventGroupHandle_t eg;
#define evtDo_AudioReadFreq       ( 1 << 0 ) // 1
////
QueueHandle_t xQ_LED_Info;
QueueHandle_t xQ_LstripOn;
QueueHandle_t xQ_LstripMode;
////
const int LED_COUNT = 108; //total number of leds in the strip
////
// When we setup the NeoPixel library, we tell it how many pixels, and which pin to use to send signals.
Adafruit_NeoPixel leds = Adafruit_NeoPixel( LED_COUNT, 26, NEO_GRB + NEO_KHZ800 );
//
WiFiClient wifiClient;
PubSubClient MQTTclient(mqtt_server, mqtt_port, wifiClient);
////
SemaphoreHandle_t sema_MQTT_KeepAlive;
////
void ULP_BLINK_RUN(uint32_t us);
////
void setup()
{
  ULP_BLINK_RUN(100000);
  eg = xEventGroupCreate();
  ////
  int FreqVal[7]; // used to set queue size
  xQ_LED_Info = xQueueCreate ( 1, sizeof(FreqVal) );
  xQ_LstripOn = xQueueCreate( 1, sizeof(bool) ); // sends a queue copy of the structure
  xQ_LstripMode = xQueueCreate( 1, sizeof(int) );
  ////
  sema_MQTT_KeepAlive = xSemaphoreCreateBinary();
  xSemaphoreGive( sema_MQTT_KeepAlive );
  ////
  // setting must be set before a mqtt connection is made
  MQTTclient.setKeepAlive( 90 ); // setting keep alive to 90 seconds
  connectToWiFi();
  connectToMQTT();
  //////////////////////////////////////////////////////////////////////////////////////////////
  xTaskCreatePinnedToCore( MQTTkeepalive, "MQTTkeepalive", 7000, NULL, 5, NULL, 1 );
  xTaskCreatePinnedToCore( fDo_AudioReadFreq, "fDo_ AudioReadFreq", 30000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fDo_LEDs, "fDo_ LEDs", 30000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fDoLstripMode, "fDoLstripMode", 2000, NULL, 3, NULL, 1 );
  xTaskCreatePinnedToCore( fDoLstripOn, "fDoLstripOn", 2000, NULL, 3, NULL, 1 );
  xEventGroupSetBits( eg, evtDo_AudioReadFreq );
} // setup()
////
void fDoLstripMode( void *pvParameters )
{
  int iMode = 0;
  for (;;)
  {
    if (xQueueReceive( xQ_LstripMode, &iMode,  portMAX_DELAY) == pdTRUE)
    {
      log_i( "Mode queue received %d", iMode );
    }
    //log_i( "memory fDoLstripMode %d",  uxTaskGetStackHighWaterMark(NULL) );
  }
  vTaskDelete ( NULL );
}
////
void fDoLstripOn( void *pvParameters )
{
  bool bOn = false;
  for (;;)
  {
    if (xQueueReceive( xQ_LstripOn, &bOn,  portMAX_DELAY) == pdTRUE)
    {
      log_i( "On queue received %d", bOn );
    }
    //log_i( "memory fDoLstripOn %d",  uxTaskGetStackHighWaterMark(NULL) );
  }
  vTaskDelete ( NULL );
}
////
void loop() {} // void loop
////
void fMQTT_Disconnect( )
{
  MQTTclient.disconnect();
}
////
void GetTheTime()
{
  char* ntpServer = "2.us.pool.ntp.org";
  int gmtOffset_sec = -(3600 * 7 );
  int daylightOffset_sec = 3600;
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();
}
////
// http://www.cplusplus.com/reference/ctime/strftime/
////
void MQTTkeepalive( void *pvParameters )
{
  for (;;)
  {
    //log_i( " mqtt keep alive run." );

    if ( (wifiClient.connected()) && (WiFi.status() == WL_CONNECTED) )
    {
      xSemaphoreTake( sema_MQTT_KeepAlive, portMAX_DELAY ); //
      MQTTclient.loop();
      xSemaphoreGive( sema_MQTT_KeepAlive );
    }
    else {
      log_i( "MQTT keep alive found MQTT status %s WiFi status %s", String(wifiClient.connected()), String(WiFi.status()) );
      if ( !(WiFi.status() == WL_CONNECTED) )
      {
        connectToWiFi();
      }
      connectToMQTT();
    }
    vTaskDelay( 200 );
  }
  vTaskDelete ( NULL );
}
////
int getHour()
{
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  char _time[ 5 ];
  strftime( _time, 80, "%T", &timeinfo );
  return String(_time).toInt();
}
////
void printLocalTime() {
  struct tm timeinfo;
  getLocalTime(&timeinfo);
  char _time[ 80 ];
  strftime( _time, 80, "%T", &timeinfo );
  log_i( "%s", _time);
  //  }
}
////
void connectToMQTT()
{
  log_i( "connect to mqtt" );
  while ( !MQTTclient.connected() )
  {
    MQTTclient.connect( clientID, mqtt_username, mqtt_password );
    log_i( "connecting to MQTT" );
    vTaskDelay( 250 );
  }
  log_i("MQTT Connected");
  MQTTclient.setCallback( mqttCallback );
  MQTTclient.subscribe( mqtt_topic );
}
////
void connectToWiFi()
{
  log_i( " Begin Connect to wifi" );
  while ( WiFi.status() != WL_CONNECTED )
  {
    WiFi.disconnect();
    WiFi.begin( SSID, PASSWORD );
    log_i(" waiting on wifi connection" );
    vTaskDelay( 4000 );
  }
  log_i( "End connected to WiFi" );
  WiFi.onEvent( WiFiEvent );
  GetTheTime();
}
////
static void mqttCallback(char* topic, byte * payload, unsigned int length)
{
  int i = 0;
  String temp = "";
  for ( i; i < length; i++) {
    temp += ((char)payload[i]);
  }
  //log_i( " topic %s payload %s", topic, temp );
  if ( (String)topic == topicLstripOn )
  {
    if ( temp == "0" )
    {
      bool bOn = false;
      xQueueOverwrite( xQ_LstripOn, (void *)&bOn );
    } else {
      bool bOn = true;
      xQueueOverwrite( xQ_LstripOn, (void *)&bOn );
    }
  }
  if ( (String)topic == topicLstripMode )
  {
    int i = temp.toInt();
    xQueueOverwrite( xQ_LstripMode, (void *)&i );
  }
} // void mqttCallback(char* topic, byte* payload, unsigned int length)
////
void fDo_LEDs( void *pvParameters )
{
  const int Brightness = 200;
  const int SEG = 6; // how many parts you want to separate the led strip into
  const int ledCount = LED_COUNT; //total number of leds in the strip
  int iFreqVal[7];
  int j;
  leds.begin(); // Call this to start up the LED strip.
  clearLEDs( ledCount );  // This function, defined below, de-energizes all LEDs...
  leds.show();  // ...but the LEDs don't actually update until you call this.
  leds.setBrightness( Brightness ); //  1 = min brightness (off), 255 = max brightness.
  for (;;)
  {
    if (xQueueReceive( xQ_LED_Info, &iFreqVal,  portMAX_DELAY) == pdTRUE)
    {
      j = 0;
      //assign different values for different parts of the led strip
      for (j = 0; j < ledCount; j++)
      {
        if ( (0 <= j) && (j < (ledCount / SEG)) )
        {
          //log_i( "segment 0 %d", iFreqVal[0] );
          set( j, iFreqVal[0], ledCount, SEG ); // set the color of led
        }
        else if ( ((ledCount / SEG) <= j) && (j < (ledCount / SEG * 2)) )
        {
          set( j, iFreqVal[0], ledCount, SEG );
        }
        else if ( ((ledCount / SEG * 2) <= j) && (j < (ledCount / SEG * 3)) )
        {
          set( j, iFreqVal[0], ledCount, SEG );
        }
        else if ( ((ledCount / SEG * 3) <= j) && (j < (ledCount / SEG * 4)) )
        {
          set( j, iFreqVal[0], ledCount, SEG );
        }
        else if ( ((ledCount / SEG * 4) <= j) && (j < (ledCount / SEG * 5)) )
        {
          set( j, iFreqVal[0], ledCount, SEG );
        }
        else
        {
          set( j, iFreqVal[0], ledCount, SEG );
        }
      }
      leds.show();
    }
    xEventGroupSetBits( eg, evtDo_AudioReadFreq );
  }
  vTaskDelete( NULL );
} // void fDo_ LEDs( void *pvParameters )
////
void fDo_AudioReadFreq( void *pvParameters )
{
  int FreqVal[7];
  const int NOISE = 10; // noise that you want to chop off
  const int A_D_ConversionBits = 4096; // arduino use 1024, ESP32 use 4096
  Analyzer Audio = Analyzer( 5, 15, 36 );//Strobe pin ->15  RST pin ->2 Analog Pin ->36
  Audio.Init(); // start the audio analyzer
  int64_t EndTime = esp_timer_get_time();
  int64_t StartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros
  for (;;)
  {
    xEventGroupWaitBits (eg, evtDo_AudioReadFreq, pdTRUE, pdTRUE, portMAX_DELAY);
    //EndTime = esp_timer_get_time() - StartTime;
    // log_i( "TimeSpentOnTasks: %d", EndTime );
    Audio.ReadFreq(FreqVal);
    for (int i = 0; i < 7; i++)
    {
      FreqVal[i] = constrain( FreqVal[i], NOISE, A_D_ConversionBits );
      FreqVal[i] = map( FreqVal[i], NOISE, A_D_ConversionBits, 0, 255 );
      // log_i( "Freq %d Value: %d", i, FreqVal[i]);//used for debugging and Freq choosing
    }
    xQueueSend( xQ_LED_Info, ( void * ) &FreqVal, 0 );
    //StartTime = esp_timer_get_time();
  }
  vTaskDelete( NULL );
} // fDo_ AudioReadFreq( void *pvParameters )
////
//the following function set the led color based on its position and freq value
//
void set(byte position, int value, int ledCount, int segment)
{
  int valueLowLimit = 20;
  // segment 0, red
  if ( (0 <= position) && (position < ledCount / segment) ) // segment 0 (bottom to top)
  {
    if ( value <= valueLowLimit )
    {
      leds.setPixelColor( position, 0, 0, 0 );
    }
    else
    {
      leds.setPixelColor( position, leds.Color( value , 0, 0) );
    }
  }
  else if ( (ledCount / segment <= position) && (position < ledCount / segment * 2) ) // segment 1 yellow
  {
    if ( value <= valueLowLimit )
    {
      leds.setPixelColor(position, leds.Color(0, 0, 0));
    }
    else
    {
      leds.setPixelColor(position, leds.Color( value, value, 0)); // works better to make yellow
    }
  }
  else if ( (ledCount / segment * 2 <= position) && (position < ledCount / segment * 3) ) // segment 2 pink
  {
    if ( value <= valueLowLimit )
    {
      leds.setPixelColor(position, leds.Color(0, 0, 0));
    }
    else
    {
      leds.setPixelColor(position, leds.Color( value, 0, value * .91) ); // pink
    }
  }
  else if ( (ledCount / segment * 3 <= position) && (position < ledCount / segment * 4) ) // seg 3, green
  {
    if ( value <= valueLowLimit )
    {
      leds.setPixelColor(position, leds.Color( 0, 0, 0));
    }
    else //
    {
      leds.setPixelColor( position, leds.Color( 0, value, 0) ); //
    }
  }
  else if ( (ledCount / segment * 4 <= position) && (position < ledCount / segment * 5) ) // segment 4, leds.color( R, G, B ), blue
  {
    if ( value <= valueLowLimit )
    {
      leds.setPixelColor(position, leds.Color( 0, 0, 0));
    }
    else //
    {
      leds.setPixelColor(position, leds.Color( 0, 0, value) ); // blue
    }
  }
  else // segment 5
  {
    if ( value <= valueLowLimit )
    {
      leds.setPixelColor(position, leds.Color( 0, 0, 0));
    }
    else
    {
      leds.setPixelColor( position, leds.Color( value, value * .3, 0) ); // orange
    }
  }
} // void set(byte position, int value)
////
void clearLEDs( int ledCount)
{
  for (int i = 0; i < ledCount; i++)
  {
    leds.setPixelColor(i, 0);
  }
} // void clearLEDs()
////
void WiFiEvent(WiFiEvent_t event)
{
  // log_i( "[WiFi-event] event: %d\n", event );
  switch (event) {
    //    case SYSTEM_EVENT_WIFI_READY:
    //      log_i("WiFi interface ready");
    //      break;
    //    case SYSTEM_EVENT_SCAN_DONE:
    //      log_i("Completed scan for access points");
    //      break;
    //    case SYSTEM_EVENT_STA_START:
    //      log_i("WiFi client started");
    //      break;
    //    case SYSTEM_EVENT_STA_STOP:
    //      log_i("WiFi clients stopped");
    //      break;
    case SYSTEM_EVENT_STA_CONNECTED:
      log_i("Connected to access point");
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      log_i("Disconnected from WiFi access point");
      break;
    //    case SYSTEM_EVENT_STA_AUTHMODE_CHANGE:
    //      log_i("Authentication mode of access point has changed");
    //      break;
    //    case SYSTEM_EVENT_STA_GOT_IP:
    //      log_i ("Obtained IP address: %s",  WiFi.localIP() );
    //      break;
    //    case SYSTEM_EVENT_STA_LOST_IP:
    //      log_i("Lost IP address and IP address is reset to 0");
    //      //      vTaskDelay( 5000 );
    //      //      ESP.restart();
    //      break;
    //    case SYSTEM_EVENT_STA_WPS_ER_SUCCESS:
    //      log_i("WiFi Protected Setup (WPS): succeeded in enrollee mode");
    //      break;
    //    case SYSTEM_EVENT_STA_WPS_ER_FAILED:
    //      log_i("WiFi Protected Setup (WPS): failed in enrollee mode");
    //      //      ESP.restart();
    //      break;
    //    case SYSTEM_EVENT_STA_WPS_ER_TIMEOUT:
    //      log_i("WiFi Protected Setup (WPS): timeout in enrollee mode");
    //      break;
    //    case SYSTEM_EVENT_STA_WPS_ER_PIN:
    //      log_i("WiFi Protected Setup (WPS): pin code in enrollee mode");
    //      break;
    //    case SYSTEM_EVENT_AP_START:
    //      log_i("WiFi access point started");
    //      break;
    //    case SYSTEM_EVENT_AP_STOP:
    //      log_i("WiFi access point  stopped");
    //      //      WiFi.mode(WIFI_OFF);
    //      //      esp_sleep_enable_timer_wakeup( 1000000 * 2 ); // 1 second times how many seconds wanted
    //      //      esp_deep_sleep_start();
    //      break;
    //    case SYSTEM_EVENT_AP_STACONNECTED:
    //      log_i("Client connected");
    //      break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
      log_i("WiFi client disconnected");
    //      break;
    //    case SYSTEM_EVENT_AP_STAIPASSIGNED:
    //      log_i("Assigned IP address to client");
    //      break;
    //    case SYSTEM_EVENT_AP_PROBEREQRECVED:
    //      log_i("Received probe request");
    //      break;
    //    case SYSTEM_EVENT_GOT_IP6:
    //      log_i("IPv6 is preferred");
    //      break;
    //    case SYSTEM_EVENT_ETH_GOT_IP:
    //      log_i("Obtained IP address");
    //      break;
    default: break;
  }
}
//////////////////////////////////////////////
/*
  Each I_XXX preprocessor define translates into a single 32-bit instruction. So you can count instructions to learn which memory address are used and where the free mem space starts.

  To generate branch instructions, special M_ preprocessor defines are used. M_LABEL define can be used to define a branch target.
  Implementation note: these M_ preprocessor defines will be translated into two ulp_insn_t values: one is a token value which contains label number, and the other is the actual instruction.

*/
void ULP_BLINK_RUN(uint32_t us)
{
  size_t load_addr = 0;
  RTC_SLOW_MEM[12] = 0;
  ulp_set_wakeup_period(0, us);
  const ulp_insn_t  ulp_blink[] =
  {
    I_MOVI(R3, 12),                         // #12 -> R3
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(#12)]
    M_BL(1, 1),                             // GOTO M_LABEL(1) IF R0 < 1
    I_WR_REG(RTC_GPIO_OUT_REG, 26, 27, 1),  // RTC_GPIO2 = 1
    I_SUBI(R0, R0, 1),                      // R0 = R0 - 1, R0 = 1, R0 = 0
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(#12)] = R0
    M_BX(2),                                // GOTO M_LABEL(2)
    M_LABEL(1),                             // M_LABEL(1)
    I_WR_REG(RTC_GPIO_OUT_REG, 26, 27, 0),// RTC_GPIO2 = 0
    I_ADDI(R0, R0, 1),                    // R0 = R0 + 1, R0 = 0, R0 = 1
    I_ST(R0, R3, 0),                      // RTC_SLOW_MEM[R3(#12)] = R0
    M_LABEL(2),                             // M_LABEL(2)
    I_HALT()                                // HALT COPROCESSOR
  };
  const gpio_num_t led_gpios[] =
  {
    GPIO_NUM_2,
    // GPIO_NUM_0,
    // GPIO_NUM_4
  };
  for (size_t i = 0; i < sizeof(led_gpios) / sizeof(led_gpios[0]); ++i) {
    rtc_gpio_init(led_gpios[i]);
    rtc_gpio_set_direction(led_gpios[i], RTC_GPIO_MODE_OUTPUT_ONLY);
    rtc_gpio_set_level(led_gpios[i], 0);
  }
  size_t size = sizeof(ulp_blink) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load( load_addr, ulp_blink, &size);
  ulp_run( load_addr );
} // void ULP_BLINK_RUN(uint32_t us)
//////////////////////////////////////////////

Thank you - ill try and wrap my head round it :slight_smile:

I think the ancillary display is more important when you have all the knobs, so you can be adjusting a parameter for an effect that is not the current effect, ahead of time.

Certainly logic can allow for three rotary encoders to have their activities apply to the current effect.

Feedback on the parameters is full loop - what you are seeing on the strip(s) is the feedback.

I totally agree that more knobs == more fun, but 9 of them wouldn't do anything at any given moment, unless they were adjusting blindly, or with the additional display by graphic or numerical feedback.

It's your thing, so we'll wait to see what you decide. I think one reason I went to having just three encoders was to simplify the hardware at only a minimal complication to the firmware.

There are some significant issues you are glossing over, understandably, at this point. Since you cop to being only

a bit of a novice when it comes to coding

I suggest you anticipate needing the classic techniques used for wringing the most out of these small machines. If you are unfamiliar with "Arduino blink without delay", googling that will turn up a crap-ton of exposure and explanation on the concept. Coming to a full understanding of the issues and how they can be solved in that tiny problem will be no waste of time, and be the beginning of getting some code together that will make this thing sing. Or blink, whatever.

And if you haven't, put array variables and functions on your to-learn-about list.

And please keep,us up,to date on you progress and problems.

a7

Thanks for all the pointers, certainly got a lot to consider. I'm going to take some time to brainstorm and learn what you mentioned, and I'll come back with the next steps.

Looking forward to speaking with you again!

Adam

Apologies if this is long winded, but its partly for my own personal record of the project :slight_smile:

Okay so...

Had a look over the things you recommended learning and think I've got a decent understanding. Can definitely see how they will be of importance.

I've had a long hard think about the encoder situation, and this is what I've come up with. I've decided to give FX 1 through 4 (or however many I end up having) three possible parameters - Hue, FX Rate and Custom - custom being for any unique parameters of the programmed pattern. With that decided, I've now got a few options on the encoder setup:

Option 1 - Leave as is, and settle for only seeing parameter changes when the FX is active. At first I didn't think this would be a huge deal, but come to think of it, there are better options.

Option 2 - Keep all encoders but add ancillary display. The display would show all FX parameters in real time, helping keep track of current settings. Pros - keeps track of parameters as stated, leaves options for expanding/upgrading capabilities in the future. Cons - more hardware/I/O pin space required and more code - could get complicated? I'm not sure what would would be required to make this work.

Option 3 - Use RGB encoders for the hue parameter that change colour according to the hue value set for the effect, and replace the Fade and Custom parameter encoders with pots. This leaves visual feedback for each parameter without needing to light up the strip, therefore negating the need for a display. Pros - less hardware (no display) freeing up some I/O pins, and less code = less memory taken up. Cons - complications surrounding wiring up 5 RGB encoders (4x FX and one master hue) - I'm guessing this would require lots of I/O space, probably more so than a display.

I've decided on keeping the full amount of encoders(/pots), now its just choosing the best approach for this setup. Any thoughts would be appreciated.

What sort of issues did you have in mind? Hardware/firmware related?

Thanks
Adam

I only meant that once you are settled on the hardware, the firmware will be fun.

How to switch between neopixel effects is a perennial question here.

Ppl will find example sketches that do one thing or another, then struggle to write a sketch to switch berween them, as the examples are usually not written with that in mind.

The culprit is almost always a reliance on delay() for timing.

If you know how to get along without any (any!) use of delay() in your coding, you will be fine.

If you understand the small handful of places where a call to delay() is tolerable, you will be fine.

I use delay() only when I am hacking up something and am too lazy to not to.

Sry @Grumpy_Mike did you write a tutorial where you took the Adafruit neopixel demonstartion code and rewrote it using finite state machines?

a7

I've made some progression with the basic features.

So far, I've managed to get the previously mentioned 'Normal Mode' of the LEDs working (on/off and hue adjust) with a rotary encoder.

I've also wired up x8 buttons to A1 using a voltage divider, and at the moment I have coded two of these to trigger a strobe and waterfall effect. With the strobe effect, I have wired in a pot to adjust the strobe rate, which leads me to my first (albeit minor) hiccup.

I'm not sure if the pot I'm using is linear or logarithmic, I just had it handy. When the pot is winded from left to right the strobe rate starts very slow, all the way up to about the last quarter where it will increase rapidly. In other words, the main/useful area of adjustment is just a very small part of the pot winding. Is there a way to tune pot values in the firmware so the strobe rate will increase in smoother increments?

Ive tried adding a 470R resistor from the winder to ground but didn't notice any difference.

See below for code and schematic:

// WORKING CODE MAIN V1.0.0

// Button array setup (for FX and Program modes) ----------------------------------------------------------

#define sw A1                              // Defines button array input pin

// WS2812B LED strip setup --------------------------------------------------------------------------------

#include <FastLED.h>                       // Includes FastLED Library
#define NUM_LEDS  20                       // Defines number of LEDs
#define LED_PIN   8                        // Defines LED data pin
CRGB leds[NUM_LEDS];

// Nomral function encoder setup --------------------------------------------------------------------------

int switchPin = 4;                         // button pin
int switchState = HIGH;                    // button value
int pinA = 2;                              // Rotary encoder Pin A
int pinB = 3;                              // Rotary encoder Pin B
int pinAstateCurrent = LOW;                // Current state of Pin A
int pinAStateLast = pinAstateCurrent;      // Last read value of Pin A
uint8_t hue = 0;                           // Hue variable for normal mode adjustment

// Normal Function Switch Setup ----------------------------------------------------------------------------

int normalSwitch = 13;

// Strobe setup --------------------------------------------------------------------------------------------

unsigned long actTime = 0;
unsigned long remTime = 0;
bool is_lit = false;

// ---------------------------------------------------------------------------------------------------------
void setup() {

  Serial.begin (9600);                     // Initialise the serial monitor

// FastLED setup -------------------------------------------------------------------------------------------

  FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(10);
  
// Normal mode encoder and switch setup --------------------------------------------------------------------

  pinMode (switchPin, INPUT_PULLUP);              // Enable the switchPin as input with a PULLUP resistor
  pinMode (pinA, INPUT_PULLUP);                   // Set PinA as input
  pinMode (pinB, INPUT_PULLUP);                   // Set PinB as input
  pinMode (normalSwitch, INPUT_PULLUP);
}

// ----------------------------------------------------------------------------------------------------------

void loop() {
 
  int value;
  value = digitalRead(normalSwitch);



  // Nomral mode start (controlled via SW2 (see schematic) ----------------------------------------------------------
  


  if (value == LOW) {
    if (analogRead(sw) >-5 && analogRead(sw) <5) {      // If Pin B is HIGH
      for (int i =  0; i < NUM_LEDS; i ++){
      leds[i] = CHSV(hue, 255, 255);
    }            // Print on screen
  }
  }

if (value == HIGH) {
    if (analogRead(sw) >-5 && analogRead(sw) <5) {      // If Pin B is HIGH
      for (int i =  0; i < NUM_LEDS; i ++){
      leds[i] = CRGB ::Black;
    }            // Print on screen
  }
  }


  // Normal mode encoder --------------------------------------------------------------------------------------

  pinAStateLast = pinAstateCurrent;        // Store the latest read value in the currect state variable
  switchState = digitalRead(switchPin);    // Read the digital value of the switch (LOW/HIGH)
  pinAstateCurrent = digitalRead(pinA);

  // Prints hue values to serial monitor

   if ((pinAStateLast == LOW) && (pinAstateCurrent == HIGH)) {
     if (digitalRead(pinB) == HIGH) {      // If Pin B is HIGH
       Serial.println(hue);                // Print on screen
     } else {
       Serial.println(hue);                // Print on screen
      }     
    }
  
  // Increases/decreases hue by 1 depending on rotation direction for fine tuning

  if ((pinAStateLast == LOW) && (pinAstateCurrent == HIGH)) {
    if (digitalRead(pinB) == HIGH) {      // If Pin B is HIGH
      hue += 1;             // Print on screen
    } else {
      hue += -1;            // Print on screen
    }
  }

  // Increases/decreses hue by 10 when encoder switch is pressed for fast adjustment

  if ((switchState == LOW) && (pinAStateLast == LOW) && (pinAstateCurrent == HIGH)) {
      if (digitalRead(pinB) == HIGH) {      // If Pin B is HIGH
      hue += 10;             
    } else {
      hue += -10;            
    }
  }



  // Button array (for FX and Program modes) --------------------------------------------------------------------

  // Waterfall 

  if(analogRead(sw) >1016 && analogRead(sw) <1027){ //Activates on SW10 (see schematic) being pushed 

      EVERY_N_MILLISECONDS(20){

      leds[0] = CHSV(35, random8(), random8(65,255));

      for (int i = NUM_LEDS - 1; i > 0; i--) {
        leds[i] = leds[i-1];

      }
  }
  }

  // Strobe

int strobeRate = (1 * analogRead(A5) + 20); //Not needed, just trying (unsucsessfully) to do some maths to tune the pot
Serial.println(strobeRate);

  if(analogRead(sw) >880 && analogRead(sw) <890){ //Activates on SW9 (see schematic) being pushed

      actTime = millis();
      if (actTime - remTime >= strobeRate){
        remTime = actTime;
        if(!is_lit) {
            is_lit = true;
            for (int i =  0; i < NUM_LEDS; i ++){
      leds[i] = CHSV(hue, 255, 255);
            FastLED.show();}
        } else {
            is_lit = false;
            fill_solid(leds, NUM_LEDS, CRGB::Black);
            FastLED.show();
        }
      }
  
  }

 


  FastLED.show();
}

Another few minor issues I've come across:

  1. On occasion, the waterfall effect will stutter, like a frame rate drop. Any thoughts on this?

  2. When activating the strobe effect, is it possible to make the first state of the LEDs always ON? At slower rates, there is sometimes a pause after the button is pressed where the code is running through the off period of the strobe, making it feel like there is a delay after pressing the button.

Thanks!
Adam

Seems like you must have a logarithmic taper potentiometer. It could be adjusted with an equation after the analogRead(). There are some hacks you could try, adding fixed resistors variously, but you should probably be better off just getting linear pots.

I'll leave what I had been writing below this link


Progressing with Arduino


which is another thread on the same matter I attempt to address. Experiments are fun, but a big program needs to be planned and organized, some woukd say before a single line of code is written.

The code is doing what you wrote. Or what it says, if you got it somewhere else.

I don't think you will get very far throwing code into this.

If you do not understand how the code you are borrowing works, it will be very hard to fix when you place it in new circumstances and find it unsatisfactory.

I recommend you just start learning how to program. Keep this project in mind, to focus your efforts, but work on very basic stuff so that you will be able to read and understand and exploit the code you come across that does stuff like you want.

The IDE has examples starting from zero. Of course google turns up plenty of code, but treat that like something you want to understand, to take apart to its smallest steps so you will get to know how small steps add up to big programs. Not like a black box. Read the code, read a bunch of code.

I said way up there that you should learn about arrays and functions, those come up fairly early if you start at the beginning and pretend you know nothing.

a7

I found the thread that had some good code for managing multiple pixel strip effects.

https://forum.arduino.cc/t/multiple-concurrent-effects-on-one-led-strip/684336/3

It demonstrates the basic techniques used to create non-blocking code with no need for using delay().

If you haven't already, soon enough you will see the need for taht.

a7

Didn't I write this code?

Who said you did not?

I @Grumpy_Mike'd you above, you did not respond. After some time, I went looking for the code (you wrote) and found it.

But now that you've tuned in, I ask again: was not this code the subject of a tutorial you wrote and posted somewhere?

a7

Sorry I didn't see, it is odd that I missed the notification as well.
It is just that I was surprised when the description of "some good code" applied to something that I had written. I normally get lambasted from some "software people" that I am doing it all wrong. Thanks for the complement.

I remember many many years ago I wrote an explanation of how stepping motors worked. A couple of years later some one came up to me after a talk I gave at a computer show, and asked me where I got that explanation from, because it was the first time that he had been able to understand how they worked after years of trying. When I told him "I made it up" he was not at all impressed. That puzzled me.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.