Project: Web-Controlled Scrolling Text on Pixelblaze Using Tampermonkey + WebSocket
Overview:
I’m building a browser-based control panel (using Tampermonkey) to send scrolling text, brightness, speed, and size controls directly to a custom LED matrix powered by Pixelblaze.
Hardware:
- LED Matrix: 21 columns x 27 rows
- Mapping: Vertical zigzag starting from the bottom-right corner
- Controller: Pixelblaze
- Communication: WebSocket on port 81
Goals:
- Scroll text across the matrix
- Control brightness, scroll speed, and text size via browser UI
- Avoid flashing firmware or using Pixelblaze editor for manual input
Challenges Faced:
-
Matrix Mapping
- Incorrect dimensions (23x27 instead of 21x27) caused patterns to misalign or not display at all.
- Fixed by correcting to actual 21x27 layout.
-
WebSocket Connection
- Pixelblaze IP changed, breaking hardcoded WebSocket connections.
- Solved by using
window.location.hostname
to dynamically connect.
-
Out-of-Bounds Errors
- Defensive code was added to avoid referencing non-existent indices in
pixelData
or font buffers. - Added fallback to space (
charCode = 32
) for invalid glyphs.
- Defensive code was added to avoid referencing non-existent indices in
-
Font Rendering
- Simple 5x7 font using uppercase A–Z.
- Currently no support for symbols or numbers.
=============
Tampermonkey Script
// ==UserScript==
// @name PixelBlaze Toolkit
// @namespace http://tampermonkey.net/
// @version 0.4
// @description Control PixelBlaze with UI and WebSocket
// @match http://192.168.4.80/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
console.log("✅ TamperMonkey script injected.");
const panel = document.createElement('div');
panel.id = 'pbControlPanel';
panel.innerHTML = `
<div id="pbHeader">🎛️ PixelBlaze Control</div>
<label>Brightness:
<input type="range" id="brightnessSlider" min="0" max="1" step="0.01" value="1">
</label>
<label>Text to Scroll:
<input type="text" id="scrollTextInput" placeholder="Enter your text">
</label>
<label>Speed:
<input type="number" id="scrollSpeedInput" value="10">
</label>
<label>Size:
<input type="number" id="scrollSizeInput" value="1">
</label>
<button id="sendTextBtn">Send Text</button>
`;
document.body.appendChild(panel);
const style = document.createElement('style');
style.textContent = `
#pbControlPanel {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 10px 15px;
border-radius: 8px;
font-family: sans-serif;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
width: 240px;
}
#pbControlPanel label {
display: block;
margin-top: 10px;
}
#pbHeader {
font-weight: bold;
margin-bottom: 8px;
cursor: move;
}
#sendTextBtn {
margin-top: 10px;
padding: 5px 10px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
#sendTextBtn:hover {
background-color: #45a049;
}
`;
document.head.appendChild(style);
let isDragging = false, offsetX = 0, offsetY = 0;
const header = panel.querySelector('#pbHeader');
header.addEventListener('mousedown', function (e) {
isDragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
});
document.addEventListener('mousemove', function (e) {
if (isDragging) {
panel.style.left = (e.clientX - offsetX) + 'px';
panel.style.top = (e.clientY - offsetY) + 'px';
panel.style.right = 'auto';
}
});
document.addEventListener('mouseup', function () {
isDragging = false;
});
let socket = null;
function connectToPixelBlaze() {
const host = window.location.hostname;
socket = new WebSocket(`ws://${host}:81`);
socket.addEventListener("open", () => console.log("✅ WebSocket connected"));
socket.addEventListener("error", e => console.error("❌ WebSocket error:", e));
socket.addEventListener("close", () => {
console.warn("⚠️ WebSocket closed. Reconnecting...");
setTimeout(connectToPixelBlaze, 3000);
});
}
connectToPixelBlaze();
document.getElementById('brightnessSlider').addEventListener('input', function () {
if (socket && socket.readyState === WebSocket.OPEN) {
const payload = { brightness: parseFloat(this.value), save: false };
socket.send(JSON.stringify(payload));
}
});
document.getElementById("sendTextBtn").addEventListener("click", () => {
const text = document.getElementById("scrollTextInput").value || "Hello";
const speed = parseFloat(document.getElementById("scrollSpeedInput").value) || 10;
const size = parseFloat(document.getElementById("scrollSizeInput").value) || 1;
const asciiArray = [];
for (let i = 0; i < text.length && i < 60; i++) {
asciiArray.push(text.charCodeAt(i));
}
const payload = {
type: "setVars",
vars: {
pixelData: asciiArray,
scrollSpeed: speed,
textSize: size
}
};
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(payload));
console.log("✅ Sent pixelData:", asciiArray);
} else {
console.warn("⚠️ WebSocket not ready.");
}
});
})();
Pixelblaze pattern
export var pixelData = array(60);
export var scrollSpeed = 10;
export var textSize = 1;
var fontData = [/* insert your full A–Z fontData array here */];
var fontBuffer = array(5);
function getGlyph(c) {
if (c >= 97 && c <= 122) c -= 32;
if (c >= 65 && c <= 90) {
var offset = (c - 65) * 5;
for (var i = 0; i < 5; i++) fontBuffer[i] = fontData[offset + i];
} else {
for (i = 0; i < 5; i++) fontBuffer[i] = 0;
}
}
var t = 0;
var cols = 21;
var rows = 27;
export function beforeRender(delta) {
t += delta * scrollSpeed;
}
export function render(index) {
var col = index % cols;
var row = floor(index / cols);
var scrollOffset = floor(t);
var glyphIndex = floor((col + scrollOffset) / 6);
var charColumn = (col + scrollOffset) % 6;
var charCode = 32;
if (glyphIndex >= 0 && glyphIndex < arrayLength(pixelData)) {
charCode = pixelData[glyphIndex];
}
getGlyph(charCode);
var bits = (charColumn < 5) ? fontBuffer[charColumn] : 0;
var isOn = (bits >> row) & 1;
if (isOn) {
hsv(0.6, 1, 1); // cyan
} else {
hsv(0, 0, 0); // off
}
}