Universal SPI LED Controller: Prototype 1
PROTOTYPE 2 IS IN PROGESS, IT INCLUDES ESP32, CUSTOM PCB BREAKOUT BOARD, AND FULL SYSTEM DESIGN (ELECTRICAL AND MECHANICAL DESIGN)
PROTOTYPE 2 IS IN PROGESS, IT INCLUDES ESP32, CUSTOM PCB BREAKOUT BOARD, AND FULL SYSTEM DESIGN (ELECTRICAL AND MECHANICAL DESIGN)
During my time with the Tesla Interior Lighting & Switches team, we encountered a significant design and cost challenge:
Each time we purchased new LEDs from external suppliers, the only way to control them was by buying a proprietary LED controller made specifically for that LED model. As our projects scaled toward production, the costs quickly became unsustainable.
To solve this, I proposed and developed a universal SPI LED controller box. This device allows our lighting designers to directly control RGBW values, create animations, and prototype lighting effects without needing to go through the firmware team for every iteration. This drastically reduced communication overhead, sped up the design cycle, and saved costs.
I am currently working on integrating the system with an ESP32 to enable Wi-Fi connectivity, allowing lighting designers to use an intuitive tool like TouchDesigner to create and upload preset animations wirelessly. In addition, I plan to design a custom injection-molded enclosure for the controller to give it a professional, production-ready appearance.
Future iterations will also include custom switch designs and a seamless plug-and-play interface, so users can easily connect the LED strip’s data line directly to the controller and start controlling lighting effects without any additional setup.
Prototype 1 Demonstration:
This video above shows the first working prototype of the Universal SPI LED Controller, assembled using an Arduino Uno R3 that I had available at the time.
I presented this proof-of-concept to my senior colleagues, who approved moving forward with a more advanced version based on the ESP32 platform. The demonstration highlights the core functionality and validates the concept before advancing to future development stages.
#include <LiquidCrystal.h>
#include <Keypad.h>
#include <NeoPixelBus.h>
#define POT_PIN A5
#define LED_PIN 6
#define NUM_LEDS 60
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);
// Keypad setup
const byte ROWS = 4;
const byte COLS = 2;
char keys[ROWS][COLS] = {
{'1', '2'},
{'3', '4'},
{'5', '6'},
{'7', '8'}
};
byte rowPins[ROWS] = {5, 4, 3, 2};
byte colPins[COLS] = {A0, A1};
Keypad customKeypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// RGBCCT LED strip setup
NeoPixelBus<NeoGrbwwFeature, Neo800KbpsMethod> strip(NUM_LEDS, LED_PIN);
// Global state
char activeState = 'S'; // 'S' = Select mode, 'A' = Adjust mode
char selectedColor = ' '; // R, G, B, W, C
uint8_t redValue = 0;
uint8_t greenValue = 0;
uint8_t blueValue = 0;
uint8_t warmWhiteValue = 0;
uint8_t coolWhiteValue = 0;
void setup() {
lcd.begin(16, 2);
strip.Begin();
strip.Show();
displaySelectPrompt();
}
void loop() {
if (activeState == 'S') {
handleSelectState();
} else if (activeState == 'A') {
handleAdjustState();
}
}
// Handle color selection input
void handleSelectState() {
char key = customKeypad.getKey();
if (key) {
bool validKey = true;
switch (key) {
case '2': selectedColor = 'R'; break;
case '4': selectedColor = 'G'; break;
case '6': selectedColor = 'B'; break;
case '8': selectedColor = 'W'; break;
case '5': selectedColor = 'C'; break;
default: validKey = false; break;
}
if (validKey) {
activeState = 'A';
lcd.clear();
}
}
}
// Handle brightness adjustment state
void handleAdjustState() {
int potValue = analogRead(POT_PIN);
int brightness = map(potValue, 0, 1023, 0, 255);
int percentage = map(potValue, 0, 1023, 0, 100);
updateLedsTemporarily(brightness);
updateLcdWithPercentage(percentage);
char key = customKeypad.getKey();
if (key == '7') { // '#' confirms
switch (selectedColor) {
case 'R': redValue = brightness; break;
case 'G': greenValue = brightness; break;
case 'B': blueValue = brightness; break;
case 'W': warmWhiteValue = brightness; break;
case 'C': coolWhiteValue = brightness; break;
}
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(getColorName(selectedColor) + " Set To:");
lcd.setCursor(0, 1);
lcd.print(percentage);
lcd.print("%");
delay(2000);
lcd.clear();
displaySelectPrompt();
activeState = 'S';
}
}
// Apply final LED values to strip
void setFinalLedColor() {
RgbwwColor finalColor(redValue, greenValue, blueValue, warmWhiteValue, coolWhiteValue);
for (int i = 0; i < NUM_LEDS; i++) {
strip.SetPixelColor(i, finalColor);
}
strip.Show();
}
// Show temporary brightness while adjusting
void updateLedsTemporarily(int tempBrightness) {
uint8_t r = redValue, g = greenValue, b = blueValue, ww = warmWhiteValue, cw = coolWhiteValue;
switch (selectedColor) {
case 'R': r = tempBrightness; break;
case 'G': g = tempBrightness; break;
case 'B': b = tempBrightness; break;
case 'W': ww = tempBrightness; break;
case 'C': cw = tempBrightness; break;
}
RgbwwColor tempColor(r, g, b, ww, cw);
for (int i = 0; i < NUM_LEDS; i++) {
strip.SetPixelColor(i, tempColor);
}
strip.Show();
}
// Update LCD with current adjustment percentage
void updateLcdWithPercentage(int percent) {
String line1 = "Adjust " + getColorName(selectedColor) + ":";
while (line1.length() < 16) line1 += " ";
lcd.setCursor(0, 0);
lcd.print(line1);
String line2 = String(percent) + "%";
while (line2.length() < 16) line2 += " ";
lcd.setCursor(0, 1);
lcd.print(line2);
}
// Display initial color selection prompt
void displaySelectPrompt() {
setFinalLedColor();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Select a Color: ");
lcd.setCursor(0, 1);
lcd.print("R,G,B,WW,CW ");
}
// Return printable color name
String getColorName(char colorChar) {
switch (colorChar) {
case 'R': return "Red";
case 'G': return "Green";
case 'B': return "Blue";
case 'W': return "Warm White";
case 'C': return "Cool White";
default: return "Unknown";
}
}