Using frequency data to turn on LEDs

Hi all,

I asked this question in another thread with different topic and decided to start a new one in order to get some help.

Here’s my situation:

I have an ESP32 connected to 96 sensors(touch, with a simple on/off state). The state of the sensors is fed to PB via serial, in the first 6 bits of the freq array(16 sensors per bit). On PB I have 1152 LEDs connected via an output expander.

Every time a sensor turns on, the corresponding 12 LEDS(1152/96 sensors) should turn on, stay on for as long as the sensor is on. If the sensor turns off, LEDs should stay on for an additional 2 seconds then have a 1 second fade out.

@wizard helped me with providing the code below, but no LEDs will turn on no matter which sensor changes state(this code assumes that there are 16 LEDs per sensor btw, but I have moved to 12 per due to space).

export var frequencyData = array(32)
var fades = array(96)
var fadeAmount
var t1

export function beforeRender(delta) {
  t1 = time(.1)
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed

  for (var sensor = 0; sensor < 96; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    intValue = frequencyData[element * 2] | (frequencyData[element * 2 + 1] << 8)
    if (intValue & (1 << bit))
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
  }
}

export function render(index) {
  var sensor = floor(index / 16)
  var v = fades[sensor]

  // Rainbow pattern
  h = 1 //t1 + index / pixelCount
  s = 1
  v = v // Use the fade value for brightness
  hsv(h, s, v)
}

Unfortunately I can’t get my head around the math involved and I’m quite bad at coding so I have no idea how to address this. I did many lame attempts using chatgpt with no meaningful result.

For reference, here’s what I’m getting at Vars Watch for frequencyData[0] when some of sensors 1-16 are on:

Sensor 1: 0.000015
Sensor 2: 0.000031
Sensors 1&2: 0.000046
Sensor 10: 0.007813
Sensor 16: 0.5

Any help would be highly appreciated:)

This might be just a slight mix-up about how sensor data is encoded in the frequencyData array. Here’s a version of the pattern that might help you debug.

It has a built-in decode test (if the decode works the way I think it does) that will step through each sensor, set the appropriate bit in frequencyData and light up its pixels.

First, set NUM_SENSORS and PIXELS_PER_SENSOR to match your setup.

Then run the pattern with sensors unhooked and TEST_MODE set to 1 to see if it lights your pixels appropriately.

If that works, set TEST_MODE to 0, hook up your sensors and see how it behaves with real data.

If it’s still not working, there may be an issue on the encoding end - if you post the relevant section of code, I’d be glad to take a look!

export var frequencyData = array(32)
var fades = array(96)
var fadeAmount
var t1

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 64  

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 4

// Set this to 1 to use synthetic data that lights each sensor in turn
// Set to 0 for "production mode", using real sensor input
var TEST_MODE = 1

// Set the bit for a specified sensor to either off (v == 0),
// or on (v > 0)
export var element
function setSensor(sensor,v) {
  element = floor(sensor / 16)
  bit = sensor % 16
  mask = ((1 >> 8) << bit) 
  if (v > 0) {
    frequencyData[element] |= mask    
  }
  else {
    frequencyData[element] &= ~mask       
  }
}

var lastT2 = -1; 
export function beforeRender(delta) {
  t1 = time(.1)
  t2 = floor(time(0.2) * NUM_SENSORS)
    
  if (TEST_MODE && lastT2 != t2) {
    setSensor(t2,1)
    lastT2 = t2
  }
  
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed

  for (var sensor = 0; sensor < 96; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    intValue = frequencyData[element] 
    if (intValue & ((1 >> 8) << bit))
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
  }
  
  if (TEST_MODE) setSensor(t2,0)  
  
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  var v = fades[sensor]

  // Rainbow pattern
  h = t1 + index / pixelCount
  s = 1
  v = v // Use the fade value for brightness
  hsv(h, s, v)
}
1 Like

Test mode works, here’s what I get with real data:

Sensors 1-8 on every bit don’t turn on, sensors 9-16 do. Here’s the relevant code on the ESP32 side:

  // Pack buttons into SB10Frame.freq array
            int arrayIndex = ((buttonNumber - 1) / 8);                   // Calculate array index based on button number
            int bitIndex = ((buttonNumber - 1) % 8);                     // Calculate bit position within the integer
            SB10Frame.freq[arrayIndex] |= (ledState[i][j] << bitIndex);  // Set the corresponding bit in the array

Even though it looks equally divided to me, it seems that the lower bits are not correctly interpreted.

Thanks a lot for the help btw!

This might be pretty simple – each element of SB10.Frame.freq[] has 16 bits, not 8. If you change the 8 to 16 in both the arrayIndex and bitIndex calculations, that ought to fix it!

Ok, I’m seriously confused now. The freq array in the sensor board’s GitHub page has 64 elements, but in PB 32, so I always assumed that on the Arduino side it was broken in lower/upper bit…

I changed it to 16, now I’m getting the following:

frequencyData 0, 1 and 2 get populated with the sensor states.

Only sensors 1-8, 17-24, 33-40, 49-56, 65-72 and 81-88 populate the array, out of which only 17-24, 49-56 and 81-88 turn on the LEDs.

The sensor board frequency data is definitely set up as an array of 32 16-bit (uint16_t) integers, per:

Make sure SB10Frame.freq is allocated as uint16_t freq[32], and the 16-based arrayIndex and bitIndex ought to generate the right locations for the switches. There’s another possible (easily fixable) issue with byte order, but even in that case, all the segments should light up. They just might not light in the right order.

I had it as byte freq[64], copied from a post here I believe, but even turning it to uint16t still only LEDs on the upper bits turn on(9-16, 25-32 etc, with freq0-5 getting populated. The lower bit sensors change the values of freq, they just don’t turn any LEDs on.

// Define the data structure to be sent over serial
struct {
  char header[6] = "SB1.0";  // Start of frame header
  uint16_t freq[32];
  uint16_t energyAverage;
  uint16_t maxFrequencyMagnitude;
  uint16_t maxFrequency;  //in hz
  int16_t accelerometer[3];
  uint16_t light;
  uint16_t analogInputs[5];
  char end[4] = "END";                    // End of frame marker
} __attribute__((__packed__)) SB10Frame;  // Attribute to ensure proper packing of the struct in memory

I’ve got some ESP/Arduino code around for faking a sensor board. I’ll hook it up with some test data “sensors” in the morning and see what I can come up with!

Thank you so much for going through all this trouble:)

I’m glad to help – really liked your last project, and looking forward to seeing this one up and running!

Try this version of the pattern and let me know what it does. The critical changes are on lines 46/47. This one works - lights up all the segments – with my test arduino program, which sequentially triggers all the sensors. (And if it still doesn’t work with your sender, let me know, and I can send you my code to compare.)

export var frequencyData = array(32)
var fades = array(96)
var fadeAmount
var t1

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 64  

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 4

// Set this to 1 to use synthetic data that lights each sensor in turn
// Set to 0 for "production mode", using real sensor input
var TEST_MODE = 0

// Set the bit for a specified sensor to either off (v == 0),
// or on (v > 0)
function setSensor(sensor,v) {
  element = floor(sensor / 16)
  bit = sensor % 16
  mask = ((1 >> 8) << bit) 
  if (v > 0) {
    frequencyData[element] |= mask    
  }
  else {
    frequencyData[element] &= ~mask       
  }
}

var lastT2 = -1; 
export function beforeRender(delta) {
  t1 = time(.1)
  t2 = floor(time(0.2) * NUM_SENSORS)
  
  if (TEST_MODE && lastT2 != t2) {
    setSensor(t2,1)
    lastT2 = t2
  }
  
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed

  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16)
    bit = sensor % 16
    intValue = frequencyData[element] << 16
    if (intValue & (1<< bit)) {
      b = element
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
    }
  }
  
  if (TEST_MODE) setSensor(t2,0)  
  
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  var v = fades[sensor]

  // Rainbow pattern
  h = t1 + index / pixelCount
  s = 1
  v = v // Use the fade value for brightness
  hsv(h, s, v)
}

Ha!!!

You have no idea how happy I am after 1+ month of trying to figure this out:))

I added a second set of sensors on bits 7-12. How can I have different hue on each set?

Here’s where I am at the moment, I expected that the if (index == fades[sensor]) approach would work, tried a few other ways that failed as well…

export var frequencyData = array(32)
var fades = array(96)
var fades2 = array(96)
var fadeAmount
var t1

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96  

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12


// Set the bit for a specified sensor to either off (v == 0),
// or on (v > 0)
function setSensor(sensor,v) {
  element = floor(sensor / 16)
  bit = (sensor % 16) 
  mask = ((1 >> 8) << bit) 
  if (v > 0) {
    frequencyData[element] |= mask    
  }
  else {
    frequencyData[element] &= ~mask       
  }
}

function setSensor2(sensor2,v) {
  element2 = floor(sensor2 / 16) + 6
  bit2 = (sensor2 % 16) 
  mask2 = ((1 >> 8) << bit) 
  if (v > 0) {
    frequencyData[element2] |= mask2    
  }
  else {
    frequencyData[element2] &= ~mask2       
  }
}


export function beforeRender(delta) {
  t1 = time(.1)
  
  
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed
  
  fades2.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed

  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16) //+ 6
    bit = sensor % 16
    intValue = frequencyData[element] << 16
    if (intValue & (1<< bit)) {
      b = element
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
    }
  }
  
  for (var sensor2 = 0; sensor2 < NUM_SENSORS; sensor2++) {
    element2 = floor(sensor2 / 16) + 6
    bit2 = sensor2 % 16
    intValue = frequencyData[element2] << 16
    if (intValue & (1<< bit2)) {
      b = element2
      fades2[sensor2] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
    }
  }
  
  
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  var v1 = fades[sensor]
  
  var sensor2 = floor(index / PIXELS_PER_SENSOR)
  var v2 = fades2[sensor2]
  
  v = v1 + v2
  
  if (index == fades[sensor]) {
    h = 1
  } else {
    h = t1 + index / pixelCount
  }

  // Rainbow pattern
  //h = t1 + index / pixelCount
  s = 1
  v = v // Use the fade value for brightness
  hsv(h, s, v)
}

This will eventually be an interactive game for my kids, one of a couple of things I’m building at the moment(including a variation of the last project, again for my kids) that hopefully won’t take a year to finish:)

Here’s one way to color the first set of sensors red and the second blue:

(This includes a function that will let you set a hue for individual sensors or groups of any size and change them at any time, which could eventually be fun for gameplay.)

Click for Pattern Code
export var frequencyData = array(32)

// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 12

var fades = array(NUM_SENSORS)
var hues  = array(NUM_SENSORS)
var fadeAmount

// Use setSensorHue() to set a color for groups of sensors.  You can
// have as many colors as necessary, up to one per sensor, and change
// them at any time.
//

// set first 96 sensors to red
setSensorHue(0, 0, 96)

// set second 96 sensors to blue
setSensorHue(0.6667, 96, 96)

// sets the hue for a range of sensors of length 'count', starting at index 'start'
function setSensorHue(h,start,count) {
  for (i = 0; i < count; i++) {
    hues[start + i]  = h
  }
}

export function beforeRender(delta) {
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed

  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16) //+ 6
    bit = sensor % 16
    intValue = frequencyData[element] << 16
    if (intValue & (1<< bit)) {
      b = element
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
    }
  }
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  
  if (sensor >= 96) {
    // do something with pixels not attached to a sensor
    // in this case just leave them off
    return;
  }

  // get data for the second set of sensors  
  var sensor2 = sensor + 96

  v = fades[sensor]
  v2 = fades[sensor2]

  // this will set h to the color of the most recently activated sensor
  // that hasn't already faded out.
  h = max (hues[sensor] * (v > 0), hues[sensor2] * (v2 > 0))
  s = 1
  hsv(h, s, v + v2)
}

Fantastic once again:)

What happens when I want to have a more complex animation happening in the individual sets?

If for example I wanted to do a simple rainbow on set 1, I cannot just do that:

setSensorHue(t1 + index / pixelCount, 0, 96) as index can only be used within render.

My plan was to adapt existing patterns to the sets, that’s why I went for the if statement within render, so that I could adapt anything and just add a v = v1+ v2 or something similar.

Here’s a variation to take a look at: Instead of setting just a color for active and inactive sensors, this lets you set a pattern for each state (on/off) of each sensor.

You can set different active/inactive patterns for single sensors, or for groups of sensors. And there’s really not much limiting the number of patterns you can have going at the same time.

Here, I’ve got it set up to:

  • run a pulsing blue background across the entire object where no sensor is active
  • show a rainbow pattern over the sensor’s associated pixels for sensors 0-96 when active
  • color the sensor’s pixel’s bright red for the second bank of sensors

But you could have as many patterns doing different things as there are sensors with this setup. Hopefully, this will give you some ideas to play with!

More Pattern Code!
export var frequencyData = array(32)


// Set this to the actual number of sensors you're using
var NUM_SENSORS = 96 * 2

// Set this to the number of pixels you want each sensor to light
var PIXELS_PER_SENSOR = 2

var fades = array(NUM_SENSORS)
var activePattern  = array(NUM_SENSORS)
var inactivePattern  = array(NUM_SENSORS)
var fadeAmount

// variables to hold final hue, saturation and brightness
// these should be set by individual pattern functions
var hue,sat,bri

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setActivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    activePattern[start + i] = pFn
  }
}

// sets a pattern function for a range of activated sensors of length count,
// starting at index start
function setInactivePattern(pFn,start,count) {
  for (i = 0; i < count; i++) {
    inactivePattern[start + i] = pFn
  }
}

//////////////////////////////////////////////////////////////
// a few pattern functions as examples.  You can do anything
// you want in these, as long as you eventually set the 
// hue, sat and bri values.

// Displays a rainbow pattern over the pixels associated with
// a single sensor
function rainbowPatternByGroup(index,x,y,z) {
  hue = t1 + (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1)
  sat = 1
  bri = 1
}

// RED
function solidRed(index,x,y,z) {
  hue = 0;
  sat = 1;
  bri = 1;
}

// BLUE
function pulsingBlue(index,x,y,z) {
  hue = 0.6667;
  sat = 1;
  bri = 1 - (0.925 * wave(t1));
}

// Rainbow pattern over all pixels
function rainbowAllPixels(index,x,y,z) {
  hue = t1 + index/pixelCount
  sat = 0.8
  bri = 0.2
}

// set patterns for active and inactive sensors
// - you can change these in beforeRender() at any point
// - you can run as many of these as there are sensors
// - you can have as many in your pattern as you want.
setInactivePattern(pulsingBlue,0,NUM_SENSORS)

setActivePattern(rainbowPatternByGroup,0,96)
setActivePattern(solidRed,96,96)

// timer value, set in beforeRender() and used by several pattern functions
var t1

export function beforeRender(delta) {
  t1 = time(0.04)
  
  fadeAmount = delta / 1000 // fade out over 1 second
  fades.mutate(v => max(0, v - fadeAmount)) // fade down to zero based on time elapsed
  
  for (var sensor = 0; sensor < NUM_SENSORS; sensor++) {
    element = floor(sensor / 16) //+ 6
    bit = sensor % 16
    intValue = frequencyData[element] << 16
    if (intValue & (1<< bit)) {
      b = element
      fades[sensor] = 3; // 1 for a 1 second fade, plus 2 to hold it solid for 2s)
    }
  }
}

export function render(index) {
  var sensor = floor(index / PIXELS_PER_SENSOR)
  
  if (sensor >= 96) {
    // do something with pixels not attached to a sensor
    // in this case just leave them off
    return;
  }

  // get data for the second set of sensors  
  var sensor2 = sensor + 96

  // get current brightness for each sensor bank
  v = fades[sensor]
  v2 = fades[sensor2]
  
  // see if a sensor in either bank has been activated
  if ((v + v2) > 0) {
  
    // get current activation state for each sensor bank
    // (this will be 1 if on, 0 if off)
    on = v > 0
    on2 = v2 > 0
    
    // pick the color of the most recently activated sensor
    if (v >= v2) {
      activePattern[sensor](index) 
    }
    else {
      activePattern[sensor2](index) 
    }
    bri = v + v2
  }
  
  // if no sensor has been activated, use the inactive pattern
  else {
    inactivePattern[sensor](index)
  }

  hsv(hue,sat,bri)
}

This is absolutely fantastic and quite explanatory, thank you for all the comments, it makes it easier for me to understand:)

Playing with the inactive LEDs opens new possibilities for me, hadn’t even thought of it:))

Last(I promise) question:

When a sensor becomes active, what’s the way to turn on its LEDs progressively from the 2 ends(for 12 LEDs per sensor, 1-12, 2-11, 3-10 etc to 6)?

To get fade-from-ends-to-center working, you’ll need a couple of things:

First, a pattern function like this to actually do the fade. (This one just fades from red – you could easily do something fancier with hue, as long as you keep the brightness logic.)

// Light an active region in red, then fade it from the edges
// to the center at the same speed the whole segment is fading.
function redFadeFromEdges(index,x,y,z) { 
  
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1) 
  sensor = floor(index / PIXELS_PER_SENSOR)  
  
  hue = 0
  sat = 1

  // fade from edges to center at the same speed the whole
  // segment is fading out.
  bri = 1 - abs(pos - 0.5) - (1 - fades[sensor] / 3)         
}

and then a change to the blending logic for active patterns. At around line 129 in the previous pattern’s code, we need to change the line
bri = v + v2
to
bri *= v + v2
so that the final brightness takes both the overall fade state and any brightness change we compute inside a pattern function into account.

Once you’ve made the code changes, just set some sensors to use redFadeFromEdges as their active pattern, and you should see the fade.

Sorry, sometimes my descriptions are less than desired:) I meant to fade in in that way(from the edges) in a controllable speed, then fade out using fades[sensor].

Oh, to do that, just use the function below, redGrowFromCenter(), instead of the previous redFadeFromEdges()!

// On activation, "grow" the lighted region of the segment outward
// from the center.
function redGrowFromCenter(index,x,y,z) { 
  
  // calculate position in the sensor's group of pixels
  var pos = (index % PIXELS_PER_SENSOR) / (PIXELS_PER_SENSOR - 1) 
  sensor = floor(index / PIXELS_PER_SENSOR)  
  
  hue = 0
  sat = 1

  // on activation, grow from center at speed set by growSpeed
  // higher values are faster, lower are slower.
  // (you could put this on a slider or make it global.  It's
  // just here to keep the code short)
  var growSpeed = 2
  bri = 1-abs(pos - 0.5) - growSpeed * (1 - (3.5 - fades[sensor]))
}

This is pure gold for me, can’t thank you enough:)

Here’s what I’m getting:

Growspeed > 0.96, LEDs are off, never turn on. < 0.96 down to 0, gradually get brighter until all on(not animated, each value will just set the brightness and will remain there).

I’m trying super hard to understand the math behind it-not getting it but trying. Why is it 3.5 - fades[sensor]? What is the 3.5?

You named it growFromCenter, is that literal? I meant from the sides to the center:)

First, be sure you’ve made the other necessary change to the code in the render() function. It’ll be way down towards the bottom. This is essential. The line with:
bri = v + v2
must be changed to:
bri *= v1 + v2
or none of this will work right.

The 3.5 is because we’re using the sensor’s fades[sensorNo] value, which, once the sensor is triggered, will remain greater than zero for 3 seconds. (It’d be clearer if we changed all the 3 second values in the code to a constant named “MASTER_FADEOUT_TIME” or something like that.)

The expression 3 - fades[x] value gives us a sawtooth wave that goes upward as the fade progresses. Since the value starts super small and grows very slowly, I added 0.5 to bias it to start lighting LEDs earlier in the activation cycle.

If you’ve got everything working right a growSpeed value of 1 to 2 really ought to provide a reasonable starting point for fine-tuning. Values much less than 1 will be a little weird, because eventually, they’ll cause the fade-in to take longer than the overall activation cycle.

The version I gave you is set up to grow from the center outward. I guess I misunderstood. But you can “reverse the polarity” by changing its brightness calculation:
bri = 1-abs(pos - 0.5) - growSpeed * (1 - (3.5 - fades[sensor]))
to
bri = abs(pos - 0.5) - growSpeed * (1 - (3.5 - fades[sensor]))

(just remove the 1- at the start!)