Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/matrix-mqtt-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ without waiting for another CoreView change.
Matrix-only custom widgets may emit an `effect` scene with an effect name,
palette, speed, intensity, and optional text. Effects are generated locally by
the target; CoreView sends parameters, never a pixel stream. The reference
firmware supports `scanner`, `rainbow_waves`, `aurora`, `digital_rain`, `fire`,
`twinkle`, `color_vortex`, and `confetti`.
firmware supports `scanner`, `bouncing_text`, `rainbow_waves`, `aurora`,
`digital_rain`, `fire`, `twinkle`, `color_vortex`, and `confetti`. Scanner and
Bouncing Text use the optional `text` field as their display word; the
reference UI limits it to one word of up to 16 characters.

Notification scenes may also include an optional `icon` (`info`, `warning`,
`success`, `door`, `lock`, `motion`, `water`, or `fire`) and `flashBorder`.
Expand Down
6 changes: 4 additions & 2 deletions esphome/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ Create a profile for other boards, pinouts, panel chains, or geometries.
- Theme-aware foreground, background, accent, critical color, palette, and
brightness handling.
- Clock and one-field status scenes, including text fitting/marquee behavior.
- Matrix-only local effects: `scanner`, `rainbow_waves`, `aurora`,
`digital_rain`, `fire`, `twinkle`, `color_vortex`, and `confetti`.
- Matrix-only local effects: `scanner`, `bouncing_text`, `rainbow_waves`,
`aurora`, `digital_rain`, `fire`, `twinkle`, `color_vortex`, and `confetti`.
Scanner and Bouncing Text use the one-word display text configured on their
Matrix Effect widget.
- Notification/alert presentation with optional standard icons and flashing
border, plus transient ticker support.
- SendSpin Music Visualizer scenes: local spectrum bars, level meter, and
Expand Down
26 changes: 25 additions & 1 deletion esphome/hardware/esp32s3-hub75-3x2-32x16.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@ display:
const Color critical = color_from_hex(id(coreview_matrix_critical), Color(255, 32, 0));
it.fill(background);
if ((int32_t) (now_ms - id(coreview_boot_until)) < 0) {
it.print(width / 2, height / 2, id(coreview_font_small), primary, TextAlign::CENTER, "COREVIEW");
// A short local splash confirms that the panel and renderer are alive
// before Wi-Fi/MQTT have finished connecting.
const uint32_t splash_ms = 2500U;
const uint32_t remaining = id(coreview_boot_until) > now_ms ? id(coreview_boot_until) - now_ms : 0U;
const uint32_t elapsed = remaining < splash_ms ? splash_ms - remaining : 0U;
const int bar_width = std::max(20, std::min(width - 16, 56));
const int bar_x = (width - bar_width) / 2;
const int bar_y = std::min(height - 7, height / 2 + 5);
const int fill_width = std::max(1, (int) ((uint32_t) (bar_width - 2) * elapsed / splash_ms));
it.print(width / 2, bar_y - 12, id(coreview_font_small), primary, TextAlign::TOP_CENTER, "COREVIEW");
it.rectangle(bar_x, bar_y, bar_width, 5, secondary);
it.filled_rectangle(bar_x + 1, bar_y + 1, fill_width, 3, accent);
return;
}
if (!id(coreview_claimed)) {
Expand Down Expand Up @@ -166,6 +177,19 @@ display:
const int d = abs(y - band);
if (d < 6) it.draw_pixel_at(x, y, hue((uint8_t) (90 + x + frame)) * (uint8_t) ((6 - d) * 38));
}
} else if (id(coreview_effect) == "bouncing_text") {
const std::string word = id(coreview_title).empty() ? "COREVIEW" : id(coreview_title);
int x1, y1, text_width, text_height;
it.get_text_bounds(0, 0, word.c_str(), id(coreview_font_small), TextAlign::TOP_LEFT, &x1, &y1, &text_width, &text_height);
const int max_x = std::max(0, width - text_width);
const int max_y = std::max(0, height - text_height);
const int x_period = std::max(1, max_x * 2);
const int y_period = std::max(1, max_y * 2);
const int x_phase = (int) (frame % (uint32_t) x_period);
const int y_phase = (int) ((frame * 2U) % (uint32_t) y_period);
const int x = x_phase <= max_x ? x_phase : x_period - x_phase;
const int y = y_phase <= max_y ? y_phase : y_period - y_phase;
it.print(x, y, id(coreview_font_small), hue((uint8_t) (frame * 3U)), TextAlign::TOP_LEFT, word.c_str());
} else if (id(coreview_effect) == "rainbow_waves" || id(coreview_effect) == "color_vortex") {
for (int x = 0; x < width; x++) for (int y = 0; y < height; y++) it.draw_pixel_at(x, y, hue((uint8_t) (x * 3 + y * 7 + frame * 2)));
} else if (id(coreview_effect) == "digital_rain") {
Expand Down
6 changes: 5 additions & 1 deletion esphome/packages/coreview-matrix-base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,11 @@ mqtt:
id(coreview_matrix_critical) = theme["critical"] | "#ff2000";
id(coreview_brightness) = std::max(1, std::min(255, theme["brightness"] | 64));
id(coreview_scene) = scene["kind"] | "unsupported";
id(coreview_title) = scene["title"] | "CoreView";
const char *scene_title = scene["title"] | "CoreView";
const char *effect_text = scene["text"] | "";
id(coreview_title) = id(coreview_scene) == "effect" && effect_text[0]
? effect_text
: scene_title;
id(coreview_detail) = scene["detail"] | "";
id(coreview_severity) = scene["severity"] | "info";
id(coreview_icon) = scene["icon"] | "";
Expand Down
9 changes: 5 additions & 4 deletions server/public/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -4089,15 +4089,15 @@ <h3 class="relationship-title">How This Connects</h3>
};
}
if (normalizedKind === "matrix_effect") {
const effects = ["scanner", "rainbow_waves", "aurora", "digital_rain", "fire", "twinkle", "color_vortex", "confetti"];
const effects = ["scanner", "bouncing_text", "rainbow_waves", "aurora", "digital_rain", "fire", "twinkle", "color_vortex", "confetti"];
const palettes = ["neon", "ocean", "sunset", "forest", "party"];
return {
...base,
effect: effects.includes(String(widget.effect || "scanner").toLowerCase()) ? String(widget.effect || "scanner").toLowerCase() : "scanner",
palette: palettes.includes(String(widget.palette || "neon").toLowerCase()) ? String(widget.palette || "neon").toLowerCase() : "neon",
speed: Math.max(1, Math.min(100, Math.round(Number(widget.speed || 35)))),
intensity: Math.max(1, Math.min(100, Math.round(Number(widget.intensity || 60)))),
text: String(widget.text || "").trim()
text: String(widget.text || "").trim().split(/\s+/)[0].slice(0, 16)
};
}
if (normalizedKind === "music_visualizer") {
Expand Down Expand Up @@ -4387,10 +4387,11 @@ <h3 class="relationship-title">How This Connects</h3>
`;
} else if (widget.kind === "matrix_effect") {
extraField = `
<div class="field"><label>Effect</label><select class="widget-effect"><option value="scanner">Scanner</option><option value="rainbow_waves">Rainbow Waves</option><option value="aurora">Aurora</option><option value="digital_rain">Digital Rain</option><option value="fire">Fire</option><option value="twinkle">Twinkle</option><option value="color_vortex">Color Vortex</option><option value="confetti">Confetti</option></select></div>
<div class="field"><label>Effect</label><select class="widget-effect"><option value="scanner">Scanner</option><option value="bouncing_text">Bouncing Text</option><option value="rainbow_waves">Rainbow Waves</option><option value="aurora">Aurora</option><option value="digital_rain">Digital Rain</option><option value="fire">Fire</option><option value="twinkle">Twinkle</option><option value="color_vortex">Color Vortex</option><option value="confetti">Confetti</option></select></div>
<div class="field"><label>Palette</label><select class="widget-palette"><option value="neon">Neon</option><option value="ocean">Ocean</option><option value="sunset">Sunset</option><option value="forest">Forest</option><option value="party">Party</option></select></div>
<div class="field"><label>Speed</label><input class="widget-effect-speed" type="number" min="1" max="100" value="${widget.speed}" /></div>
<div class="field"><label>Intensity</label><input class="widget-effect-intensity" type="number" min="1" max="100" value="${widget.intensity}" /></div>`;
<div class="field"><label>Intensity</label><input class="widget-effect-intensity" type="number" min="1" max="100" value="${widget.intensity}" /></div>
<div class="field full"><label>Display word</label><input class="widget-text" maxlength="16" value="${escapeHtml(widget.text || "")}" placeholder="COREVIEW" /></div>`;
} else if (widget.kind === "music_visualizer") {
extraField = `
<div class="field"><label>Visualizer</label><select class="widget-music-mode"><option value="spectrum">Spectrum Bars</option><option value="meter">Level Meter</option><option value="pulse">Pulse</option></select></div>
Expand Down
7 changes: 4 additions & 3 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2708,15 +2708,15 @@ function normalizeCustomProfileWidget(widget = {}) {
};
}
if (normalizedKind === "matrix_effect") {
const effects = ["scanner", "rainbow_waves", "aurora", "digital_rain", "fire", "twinkle", "color_vortex", "confetti"];
const effects = ["scanner", "bouncing_text", "rainbow_waves", "aurora", "digital_rain", "fire", "twinkle", "color_vortex", "confetti"];
const palettes = ["neon", "ocean", "sunset", "forest", "party"];
return {
...base,
effect: effects.includes(String(widget.effect || "scanner").toLowerCase()) ? String(widget.effect || "scanner").toLowerCase() : "scanner",
palette: palettes.includes(String(widget.palette || "neon").toLowerCase()) ? String(widget.palette || "neon").toLowerCase() : "neon",
speed: Math.max(1, Math.min(100, Math.round(Number(widget.speed || 35)))),
intensity: Math.max(1, Math.min(100, Math.round(Number(widget.intensity || 60)))),
text: String(widget.text || "").trim()
text: String(widget.text || "").trim().split(/\s+/)[0].slice(0, 16)
};
}
if (normalizedKind === "music_visualizer") {
Expand Down Expand Up @@ -3340,7 +3340,8 @@ function compileMatrixScene(runtime) {
palette: runtime?.themeState?.matrix?.effectPalette || effectWidget.palette || "neon",
speed: Number(effectWidget.speed || 35),
intensity: Number(effectWidget.intensity || 60),
text: effectWidget.text || ""
text: effectWidget.text || "",
title: effectWidget.text || "COREVIEW"
};
}
const musicWidget = (Array.isArray(layout.widgets) ? layout.widgets : []).find((widget) => widget?.kind === "music_visualizer");
Expand Down
Loading