-
1Designing the Enclosure
I started by designing the entire enclosure in Autodesk Fusion 360, keeping the design compact, easy to assemble, and optimized for the Waveshare ESP32-S3 Matrix board.
To achieve a smooth and uniform glow, I used a lithophane-inspired diffuser instead of a standard flat diffuser. By varying its thickness, the light spreads more evenly across the surface, helping to reduce visible LED hotspots and creating a much softer ambient effect.
-
23D Printing the Enclosure
With the design complete, I printed all the parts using PLA filament. For the diffuser, I used matte white PLA with a 0.8 mm wall thickness. This provides excellent light diffusion while preserving the lithophane effect, resulting in a smooth, even glow across the entire surface.
For the housing, I used Black PLA with a 3 mm wall thickness to give the enclosure good rigidity and durability while maintaining a clean, minimal appearance.
-
3Hardware Assembly
Carefully insert the Waveshare ESP32-S3 Matrix board into the 3D-printed enclosure, making sure it sits flush and the LED matrix aligns with the front opening. The fit should be snug, so avoid applying excessive force during installation. Once everything is aligned, apply a very small amount of super glue around the edges to secure the board in place.
-
4Installing the Diffuser
Place the 3D-printed diffuser over the LED matrix, ensuring it sits evenly inside the enclosure without putting pressure on the board. Once you're satisfied with the alignment, secure it using a very small amount of super glue around the edges.
-
5OpenWeatherMap API Key
PrismCube uses OpenWeatherMap to retrieve live weather data. Before configuring the firmware, you'll need a free API key.
Visit the OpenWeatherMap website and create a free account. After verifying your email address, open the My API Keys page, where you'll find a default API key already generated for your account. You can use this key or create a new one if you prefer. Keep your API key handy—you'll need it in the next step when configuring Config.h.
Note: New API keys may take a few minutes to become active. If PrismCube can't fetch weather data immediately after uploading the firmware, wait a short while and try again.
-
6Configure the Firmware (Config.h)
Before uploading the firmware, the first thing you'll want to do is configure the project to match your own setup. To make this as simple as possible, all user-editable settings are stored in a single file called Config.h.
Whether you're connecting to a different Wi-Fi network, using your own weather location, or tweaking the lighting behavior, this is the only file you'll normally need to modify.
Configure Your Wi-Fi & Weather Location
Start by entering your Wi-Fi credentials and your OpenWeatherMap API key.
#define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" #define OWM_API_KEY "YOUR_OPENWEATHERMAP_API_KEY"
Next, choose how you'd like the cube to determine your location. You can either enter a city name, or (recommended) use latitude and longitude coordinates.
#define OWM_USE_LATLON 1 #define OWM_LAT 34.0522 #define OWM_LON -118.2437I recommend using coordinates because they're more accurate and eliminate any ambiguity that can occur with cities sharing the same name. The firmware checks for new weather updates every 10 minutes, which provides a good balance between responsiveness and network usage.
Verify the Hardware Configuration
If you're using the Waveshare ESP32-S3 Matrix board like I did, you won't need to change anything in this section.
#define LED_DATA_PIN 14 #define LED_COUNT 64 #define IMU_SDA_PIN 11 #define IMU_SCL_PIN 12These values define the onboard 8×8 RGB LED matrix and the built-in QMI8658 IMU.
Tip: Waveshare has released a few board revisions over time. If your LEDs or IMU aren't responding correctly, double-check your board's pinout before changing the firmware.
Adjust the Lighting Behavior (Optional)
You'll also find several constants that control how PrismCube behaves.
#define BRIGHTNESS_DAY 235 #define BRIGHTNESS_EVENING 153 #define BRIGHTNESS_NIGHT 45 #define BRIGHTNESS_MAX_CAP 235The maximum brightness is intentionally limited to 235 instead of the full 255. This helps reduce heat on the onboard LED matrix while keeping the cube bright enough for everyday use.
You'll also notice animation timing values such as:
#define CYCLE_CLEAR_MS 16000 #define CYCLE_STORM_MS 7000 #define CYCLE_FOG_MS 24000These determine how quickly each weather animation moves. Clear skies transition slowly, storms feel more energetic, and fog changes almost imperceptibly. Don't worry about these values yet—we'll see exactly how they're used in the next step.
-
7The Color Engine (PaletteEngine.h)
With the basic configuration complete, it's time to look at the part that gives PrismCube its personality. The Palette Engine is responsible for translating raw weather data into a lighting experience. Instead of deciding what the LEDs should display, it decides how the weather should feel through carefully designed color palettes and animations.
Defining the Weather Moods
The first thing you'll see is the WeatherMood enum.
enum class WeatherMood { CLEAR, PARTLY_CLOUDY, CLOUDY, LIGHT_RAIN, RAIN, THUNDERSTORM, FOG, SNOW, SUNRISE, SUNSET, NIGHT, RAINBOW_SPECIAL };Rather than working directly with weather condition IDs throughout the firmware, everything is first converted into one of these predefined moods. This keeps the rest of the code much cleaner and also makes it easy to add your own weather effects later.
Creating a Color Palette
Each mood is described using a simple structure called Palette3.
struct Palette3 { CRGB top, mid, bottom; // the 3 tones of the journey uint16_t fadeMs; // crossfade duration when switching INTO this mood uint16_t cycleMs; // how long one full top->mid->bottom->top loop takes bool stormFlicker = false; bool rainbowWash = false; };Although the variables are named top, mid, and bottom, they don't represent different areas of the LED matrix. Since the enclosure uses a frosted diffuser, all 64 LEDs blend together into a single point of light. Instead, these three colors represent the journey the light takes over time, smoothly transitioning from one color to the next before looping back again.
Deciding Which Mood to Display
Whenever new weather data is received, the firmware calls decideMood().
inline WeatherMood PaletteEngine::decideMood(const WeatherState& w) { if (!w.valid) return WeatherMood::CLEAR; if (w.sunrise > 0 && w.sunset > 0) { time_t now = time(nullptr); const time_t window = 35 * 60; if (now > 1700000000) { if (llabs((long long)now - (long long)w.sunrise) < window) return WeatherMood::SUNRISE; if (llabs((long long)now - (long long)w.sunset) < window) return WeatherMood::SUNSET; if (!w.isDaytime) return WeatherMood::NIGHT; } } if (looksLikeRainbowConditions(w)) return WeatherMood::RAINBOW_SPECIAL; int id = w.conditionId; if (id >= 200 && id <= 232) return WeatherMood::THUNDERSTORM; if (id >= 300 && id <= 321) return WeatherMood::LIGHT_RAIN; if (id >= 500 && id <= 531) return WeatherMood::RAIN; if (id >= 600 && id <= 622) return WeatherMood::SNOW; if (id >= 701 && id <= 781) return WeatherMood::FOG; if (id == 800) return WeatherMood::CLEAR; if (id == 801 || id == 802) return WeatherMood::PARTLY_CLOUDY; if (id == 803 || id == 804) return WeatherMood::CLOUDY; return WeatherMood::CLEAR; }The order of these checks is intentional. Sunrise, sunset, and nighttime are evaluated first, followed by special rainbow conditions, before finally checking the weather condition ID returned by OpenWeatherMap. This ensures the cube always displays the most meaningful visual experience. For example, if it's lightly raining during sunset, PrismCube will still prioritize the sunset colors instead of immediately switching to a rain palette.
Detecting Rainbow Conditions
One feature I wanted to make feel natural was Rainbow Mode. Instead of requiring manual activation, the firmware looks for weather conditions where a real rainbow is likely to appear.
inline bool PaletteEngine::looksLikeRainbowConditions(const WeatherState& w) { if (!w.valid || !w.isDaytime) return false; bool lightRain = (w.conditionId >= 300 && w.conditionId <= 321) || (w.conditionId == 500); return lightRain && w.cloudsPct <= RAINBOW_AUTO_CLOUD_MAX; }The logic is simple: if it's daytime, there's light rain, and cloud coverage is low enough for sunlight to break through, PrismCube automatically switches to its Rainbow Special palette. Feel free to experiment with RAINBOW_AUTO_CLOUD_MAX if you want Rainbow Mode to trigger more or less often in your area.
Building Each Weather Palette
Once the mood has been selected, the firmware loads its corresponding palette.
case WeatherMood::RAIN: p.top = CRGB(0x2E, 0x5D, 0x9E); p.mid = CRGB(0x3E, 0x9B, 0xB8); p.bottom = CRGB(0x1D, 0x3E, 0x6B); p.fadeMs = FADE_RAIN_MS; p.cycleMs = CYCLE_RAIN_MS; p.rainbowWash = true; break; case WeatherMood::THUNDERSTORM: p.top = CRGB(0x1A, 0x0F, 0x33); p.mid = CRGB(0x2B, 0x16, 0x50); p.bottom = CRGB(0x14, 0x0A, 0x24); p.fadeMs = FADE_STORM_MS; p.cycleMs = CYCLE_STORM_MS; p.stormFlicker = true; break;Every weather mood follows the same structure: three colors, a transition speed, a cycle duration, and optional visual effects. This makes it easy to customize the look of PrismCube without changing any animation logic. If you'd like warmer sunsets, brighter rainy days, or more dramatic storms, this is the best place to personalize the project.
Adding a Subtle Temperature Tint
As a final touch, every palette receives a small color adjustment based on the current temperature.
static CRGB tintForTemp(CRGB c, float tempC) { if (tempC <= 10.0f) { c.b = qadd8(c.b, 12); c.g = qadd8(c.g, 4); } else if (tempC >= 32.0f) { c.r = qadd8(c.r, 14); c.g = qadd8(c.g, 6); } return c; }Instead of changing the entire palette, this function simply nudges colder weather slightly toward blue and warmer weather toward amber. The effect is subtle, but it helps the lighting feel even more connected to the actual weather outside.
The Spanner
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.