This commit is contained in:
2026-02-12 10:20:00 +01:00
parent d5c104cda4
commit ba55ced7cf
5 changed files with 270 additions and 735 deletions
+4
View File
@@ -19,3 +19,7 @@
#define SMALL_TEXT_WIDTH 5 #define SMALL_TEXT_WIDTH 5
#define MEDIUM_TEXT_HEIGHT 7 #define MEDIUM_TEXT_HEIGHT 7
#define MEDIUM_TEXT_WIDTH 5 #define MEDIUM_TEXT_WIDTH 5
#define PROGRAM2_TEXT_ITERATIONS 5
#define PROGRAM2_TRIGGER_CHANCE 25
#define PROGRAM2_VEHICLE_COUNT 25
+79 -389
View File
@@ -4,425 +4,115 @@ const char index_html[] PROGMEM = R"rawliteral(
<title>LED Panel Control</title> <title>LED Panel Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style> <style>
body { font-family: Arial; text-align: center; margin:0px auto; padding-top: 30px;} * { box-sizing: border-box; margin: 0; padding: 0; }
.button { body { font-family: Arial, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
background-color: #4CAF50; h1 { text-align: center; margin-bottom: 30px; font-size: 1.5em; }
color: white; .container { max-width: 400px; margin: 0 auto; }
padding: 10px 20px; .card {
text-align: center; background: #16213e;
text-decoration: none; border-radius: 12px;
display: inline-block; padding: 16px 20px;
font-size: 16px; margin-bottom: 12px;
margin: 4px 2px; display: flex;
cursor: pointer; align-items: center;
justify-content: space-between;
} }
textarea { .card-label { font-size: 1em; }
width: 80%; .card-label small { display: block; color: #888; font-size: 0.75em; margin-top: 2px; }
height: 200px; .toggle { position: relative; width: 52px; height: 28px; flex-shrink: 0; }
margin-top: 20px; .toggle input { opacity: 0; width: 0; height: 0; }
.toggle .slider {
position: absolute; cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background: #444; border-radius: 28px;
transition: 0.3s;
} }
.text-controls { .toggle .slider:before {
margin-top: 20px; content: ""; position: absolute;
border: 1px solid #ccc; height: 22px; width: 22px;
padding: 10px; left: 3px; bottom: 3px;
margin-bottom: 10px; background: #fff; border-radius: 50%;
transition: 0.3s;
} }
.text-input-group { .toggle input:checked + .slider { background: #4CAF50; }
margin-bottom: 10px; .toggle input:checked + .slider:before { transform: translateX(24px); }
} .brightness-card {
.param-explanation { background: #16213e;
font-size: 0.8em; border-radius: 12px;
color: #666; padding: 16px 20px;
} margin-bottom: 12px;
pre {
text-align: left;
background-color: #f0f0f0;
padding: 10px;
white-space: pre-wrap;
}
#nodes-container {
margin-top: 20px;
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
}
.node-item {
border-bottom: 1px solid #eee;
padding: 5px;
margin-bottom: 5px;
}
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
.modal-content {
background-color: #fefefe;
margin: 15% auto; /* 15% from the top and centered */
padding: 20px;
border: 1px solid #888;
width: 80%; /* Could be more or less, depending on screen size */
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
} }
.brightness-card label { font-size: 1em; display: block; margin-bottom: 8px; }
.brightness-row { display: flex; align-items: center; gap: 12px; }
.brightness-row input[type=range] { flex: 1; accent-color: #4CAF50; }
.brightness-row span { min-width: 32px; text-align: right; }
</style> </style>
</head> </head>
<body> <body>
<h1>LED Panel Control</h1> <div class="container">
<button class="button" onclick="location.href='/upload-page'">Upload Image</button> <h1>LED Panel Control</h1>
<br>
<div>
<label for="brightness">Brightness: </label>
<input type="range" id="brightness" min="0" max="100" value="100" onchange="updateBrightness()">
<span id="brightnessValue">100</span>
</div>
<br>
<div id="nodes-container"> <div class="card">
<h2>Current Text Nodes</h2> <div class="card-label">Top Text<small>Witamy w PTI</small></div>
<div id="nodes-list"></div> <label class="toggle"><input type="checkbox" id="top_text" checked onchange="toggle('top_text', this.checked)"><span class="slider"></span></label>
</div> </div>
<div id="modify-modal" class="modal"> <div class="card">
<div class="modal-content"> <div class="card-label">Bottom Text<small>School programs</small></div>
<span class="close" onclick="closeModal()">&times;</span> <label class="toggle"><input type="checkbox" id="bottom_text" checked onchange="toggle('bottom_text', this.checked)"><span class="slider"></span></label>
<h3>Modify Text Node</h3>
<div class="text-input-group">
<input type="hidden" id="modifyGlobalIdInput">
<b>Node Global ID:</b> <span id="modifyGlobalIdDisplay"></span>
</div>
<div class="text-input-group">
<input type="text" id="modifyTextInput" placeholder="New text...">
<div class="param-explanation">New text for the node.</div>
</div>
<div class="text-input-group">
<input type="color" id="modifyColorPicker">
<div class="param-explanation">New color for the text.</div>
</div>
<div class="text-input-group">
<input type="number" id="modifySlownessInput" placeholder="Slowness" min="0" max="255">
<div class="param-explanation">Animation slowness (0-255).</div>
</div>
<div class="text-buttons">
<button class="button" onclick="modifyText()">Save Changes</button>
</div>
</div> </div>
</div>
<div id="modify-multicolor-modal" class="modal"> <div class="card">
<div class="modal-content"> <div class="card-label">Vehicles<small>Vehicle interruptions</small></div>
<span class="close" onclick="closeMultiColorModal()">&times;</span> <label class="toggle"><input type="checkbox" id="vehicles" checked onchange="toggle('vehicles', this.checked)"><span class="slider"></span></label>
<h3>Modify Multi-Color Text Node</h3>
<div class="text-input-group">
<input type="hidden" id="modifyMultiColorGlobalIdInput">
<b>Node Global ID:</b> <span id="modifyMultiColorGlobalIdDisplay"></span>
</div>
<div class="text-input-group">
<input type="text" id="modifyMultiColorTextInput" placeholder="New text...">
<div class="param-explanation">New text for the node.</div>
</div>
<div class="text-input-group">
<input type="text" id="modifyMultiColorColorInput" placeholder="e.g., #ff0000,#00ff00,#0000ff">
<div class="param-explanation">Comma-separated list of hex colors (max 4).</div>
</div>
<div class="text-buttons">
<button class="button" onclick="modifyMultiColorText()">Save Changes</button>
</div>
</div> </div>
</div>
<div class="text-controls"> <div class="brightness-card">
<h3>Add Text Node (Full Control)</h3> <label>Brightness</label>
<pre><code>void addNewTextNode(char text[TEXT_MAX_LENGTH + 1], uint32_t color, bool handle_pos_via_cursor = true, short pos_x = 0, short pos_y = 0, unsigned char scroll_slowness = 1, bool is_scrolling = true, bool is_small = true, short disappear_time = -1)</code></pre> <div class="brightness-row">
<div class="text-input-group"> <input type="range" id="brightness" min="0" max="100" value="100" oninput="updateBrightnessLabel()" onchange="sendBrightness()">
<input type="text" id="textInput" placeholder="Enter text..."> <span id="brightnessValue">100</span>
<div class="param-explanation">The text to display.</div> </div>
</div>
<div class="text-input-group">
<input type="color" id="textColorPicker" value="#ffffff">
<div class="param-explanation">Color of the text.</div>
</div>
<div class="text-input-group">
<input type="number" id="xInput" placeholder="X position">
<div class="param-explanation">X coordinate of the text.</div>
</div>
<div class="text-input-group">
<input type="number" id="yInput" placeholder="Y position">
<div class="param-explanation">Y coordinate of the text.</div>
</div>
<div class="text-input-group">
<select id="fontSizeInput">
<option value="small">Small</option>
<option value="medium">Medium</option>
</select>
<div class="param-explanation">Font size of the text.</div>
</div>
<div class="text-input-group">
<input type="number" id="slownessInput" placeholder="Slowness" value="2" min="0" max="255">
<div class="param-explanation">Animation slowness (0-255).</div>
</div>
<div class="text-input-group">
<input type="checkbox" id="isRepeatingInput">
<label for="isRepeatingInput">Repeating</label>
<div class="param-explanation">If checked, the text will wrap around the screen.</div>
</div>
<div class="text-buttons">
<button class="button" onclick="sendText()">Add Text</button>
<button class="button" onclick="sendText('top')">Send to Top</button>
<button class="button" onclick="sendText('bottom')">Send to Bottom</button>
</div>
</div>
<div class="text-controls">
<h3>Add Multicolor Text Node</h3>
<pre><code>void addNewMultiColor(char text[TEXT_MAX_LENGTH + 1], RGBWithIndex colors[4], unsigned char color_count, bool handle_pos_via_cursor = true, short pos_x = 0, short pos_y = 0, unsigned char scroll_slowness = 1, bool is_scrolling = true, bool is_small = true, short disappear_time = -1)</code></pre>
<div class="text-input-group">
<input type="text" id="multicolorTextInput" placeholder="Enter text...">
<div class="param-explanation">The text to display.</div>
</div>
<div class="text-input-group">
<input type="text" id="multicolorColorInput" placeholder="e.g., #ff0000,#00ff00,#0000ff">
<div class="param-explanation">Comma-separated list of hex colors (max 4).</div>
</div>
<div class="text-input-group">
<input type="number" id="multicolorXInput" placeholder="X position">
<div class="param-explanation">X coordinate of the text.</div>
</div>
<div class="text-input-group">
<input type="number" id="multicolorYInput" placeholder="Y position">
<div class="param-explanation">Y coordinate of the text.</div>
</div>
<div class="text-input-group">
<select id="multicolorFontSizeInput">
<option value="small">Small</option>
<option value="medium">Medium</option>
</select>
<div class="param-explanation">Font size of the text.</div>
</div>
<div class="text-input-group">
<input type="number" id="multicolorSlownessInput" placeholder="Slowness" value="2" min="0" max="255">
<div class="param-explanation">Animation slowness (0-255).</div>
</div>
<div class="text-input-group">
<input type="checkbox" id="multicolorIsRepeatingInput">
<label for="multicolorIsRepeatingInput">Repeating</label>
<div class="param-explanation">If checked, the text will wrap around the screen.</div>
</div>
<div class="text-buttons">
<button class="button" onclick="sendMulticolorText()">Add Multicolor Text</button>
</div> </div>
</div> </div>
<script> <script>
function updateBrightness() { function toggle(program, enabled) {
var brightness = document.getElementById("brightness").value; var xhr = new XMLHttpRequest();
document.getElementById("brightnessValue").innerText = brightness; xhr.open("POST", "/toggle-program", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("program=" + program + "&enabled=" + (enabled ? "true" : "false"));
}
function updateBrightnessLabel() {
document.getElementById("brightnessValue").innerText = document.getElementById("brightness").value;
}
function sendBrightness() {
var val = document.getElementById("brightness").value;
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("POST", "/brightness", true); xhr.open("POST", "/brightness", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("value=" + brightness); xhr.send("value=" + val);
} }
function sendText(position) { function fetchStatus() {
const text = document.getElementById('textInput').value;
const color = document.getElementById('textColorPicker').value;
const slowness = document.getElementById('slownessInput').value;
const x = document.getElementById('xInput').value;
const y = document.getElementById('yInput').value;
const fontSize = document.getElementById('fontSizeInput').value;
const isRepeating = document.getElementById('isRepeatingInput').checked;
if (!text) {
alert('Please enter some text.');
return;
}
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("POST", "/text", true); xhr.open("GET", "/program-status", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
let data = "text=" + encodeURIComponent(text) + "&color=" + encodeURIComponent(color) + "&slowness=" + slowness + "&is_repeating=" + isRepeating;
if(position) {
data += "&position=" + position;
} else {
if(x) data += "&x=" + x;
if(y) data += "&y=" + y;
}
if(fontSize) data += "&fontSize=" + fontSize;
xhr.onload = function() { xhr.onload = function() {
if (xhr.status === 200) { if (xhr.status === 200) {
fetchNodes(); var s = JSON.parse(xhr.responseText);
} document.getElementById("top_text").checked = s.top_text;
}; document.getElementById("bottom_text").checked = s.bottom_text;
xhr.send(data); document.getElementById("vehicles").checked = s.vehicles;
} document.getElementById("brightness").value = s.brightness;
document.getElementById("brightnessValue").innerText = s.brightness;
function sendMulticolorText() {
const text = document.getElementById('multicolorTextInput').value;
const colors = document.getElementById('multicolorColorInput').value;
const slowness = document.getElementById('multicolorSlownessInput').value;
const x = document.getElementById('multicolorXInput').value;
const y = document.getElementById('multicolorYInput').value;
const fontSize = document.getElementById('multicolorFontSizeInput').value;
const isRepeating = document.getElementById('multicolorIsRepeatingInput').checked;
if (!text) {
alert('Please enter some text.');
return;
}
if (!colors) {
alert('Please enter some colors.');
return;
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "/multicolor-text", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
let data = "text=" + encodeURIComponent(text) + "&colors=" + encodeURIComponent(colors) + "&slowness=" + slowness + "&is_repeating=" + isRepeating;
if(x) data += "&x=" + x;
if(y) data += "&y=" + y;
if(fontSize) data += "&fontSize=" + fontSize;
xhr.onload = function() {
if (xhr.status === 200) {
fetchNodes();
}
};
xhr.send(data);
}
function modifyText() {
const global_id = document.getElementById('modifyGlobalIdInput').value;
const color = document.getElementById('modifyColorPicker').value;
const text = document.getElementById('modifyTextInput').value;
const slowness = document.getElementById('modifySlownessInput').value;
if (!global_id) {
alert('Please enter a node ID.');
return;
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "/modify-text", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
let data = "global_id=" + global_id + "&color=" + encodeURIComponent(color) + "&text=" + encodeURIComponent(text) + "&slowness=" + slowness;
xhr.onload = function() {
if (xhr.status === 200) {
fetchNodes();
closeModal();
}
};
xhr.send(data);
}
function fetchNodes() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/nodes", true);
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var nodes = JSON.parse(this.responseText);
var nodesList = document.getElementById('nodes-list');
nodesList.innerHTML = '';
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var nodeDiv = document.createElement('div');
nodeDiv.className = 'node-item';
if (node.type === 'text') {
var color = '#' + ('000000' + node.color.toString(16)).slice(-6);
nodeDiv.innerHTML = '<b>ID:</b> ' + node.global_id + ', <b>Text:</b> ' + node.text + ', <b>Color:</b> <span style="color:' + color + '">' + color + '</span>' +
'<button style="margin-left: 10px;" onclick="populateModifyForm(' + node.global_id + ', \'' + color + '\', \'' + node.text + '\', ' + node.slowness + ')">Modify</button>';
} else if (node.type === 'multi-color') {
var colorsStr = node.colors.map(c => '#' + ('000000' + ((c.r << 16) | (c.g << 8) | c.b).toString(16)).slice(-6)).join(',');
nodeDiv.innerHTML = '<b>ID:</b> ' + node.global_id + ', <b>Text:</b> ' + node.text + ', <b>Type:</b> Multi-Color' +
'<button style="margin-left: 10px;" onclick="populateMultiColorModifyForm(' + node.global_id + ', \'' + colorsStr + '\', \'' + node.text + '\')">Modify</button>';
}
nodesList.appendChild(nodeDiv);
}
} }
}; };
xhr.send(); xhr.send();
} }
function populateModifyForm(global_id, color, text, slowness) { window.onload = fetchStatus;
document.getElementById('modifyGlobalIdInput').value = global_id;
document.getElementById('modifyGlobalIdDisplay').innerText = global_id;
document.getElementById('modifyColorPicker').value = color;
document.getElementById('modifyTextInput').value = text;
document.getElementById('modifySlownessInput').value = slowness;
document.getElementById('modify-modal').style.display = "block";
}
function closeModal() {
document.getElementById('modify-modal').style.display = "none";
}
function populateMultiColorModifyForm(global_id, colors, text) {
document.getElementById('modifyMultiColorGlobalIdInput').value = global_id;
document.getElementById('modifyMultiColorGlobalIdDisplay').innerText = global_id;
document.getElementById('modifyMultiColorColorInput').value = colors;
document.getElementById('modifyMultiColorTextInput').value = text;
document.getElementById('modify-multicolor-modal').style.display = "block";
}
function closeMultiColorModal() {
document.getElementById('modify-multicolor-modal').style.display = "none";
}
function modifyMultiColorText() {
const global_id = document.getElementById('modifyMultiColorGlobalIdInput').value;
const colors = document.getElementById('modifyMultiColorColorInput').value;
const text = document.getElementById('modifyMultiColorTextInput').value;
if (!global_id) {
alert('Please enter a node ID.');
return;
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "/modify-multicolor-text", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
let data = "global_id=" + global_id + "&colors=" + encodeURIComponent(colors) + "&text=" + encodeURIComponent(text);
xhr.onload = function() {
if (xhr.status === 200) {
fetchNodes();
closeMultiColorModal();
}
};
xhr.send(data);
}
window.onclick = function(event) {
if (event.target == document.getElementById('modify-modal')) {
closeModal();
}
if (event.target == document.getElementById('modify-multicolor-modal')) {
closeMultiColorModal();
}
}
setInterval(fetchNodes, 5000);
window.onload = fetchNodes;
</script> </script>
</body> </body>
</html> </html>
+138 -11
View File
@@ -6,7 +6,6 @@
#include "index.h" #include "index.h"
#include "fonts.h" #include "fonts.h"
#include "lowLevel.h" #include "lowLevel.h"
#include "upload_page.h"
#include "prototypes.h" #include "prototypes.h"
#ifdef __AVR__ #ifdef __AVR__
@@ -25,6 +24,16 @@ unsigned char bottom_text_state = 0;
unsigned char scroll_node_global_id = 0; unsigned char scroll_node_global_id = 0;
bool scroll_node_active = false; bool scroll_node_active = false;
bool program2_active = false;
unsigned char bottom_text_iteration_count = 0;
bool program_top_text_enabled = true;
bool program_bottom_text_enabled = true;
bool program_vehicles_enabled = true;
unsigned char program2_vehicles_spawned = 0;
short program2_drain_counter = 0;
bool program2_done = false;
Image saved_images[MAX_IMAGES_SAVED] = { Image saved_images[MAX_IMAGES_SAVED] = {
{},{} {},{}
}; };
@@ -526,7 +535,20 @@ void resetGlobalIdToAfterIntroValue()
void handleProgram1() void handleProgram1()
{ {
if (!main_animation_started) // --- Top text section ---
if (!program_top_text_enabled)
{
// Kill existing multi-color node if any
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (multi_color_text_node[i].disappear_time != 0)
{
multi_color_text_node[i].disappear_time = 1;
}
}
main_animation_started = false;
}
else if (!main_animation_started)
{ {
if (!isNodeExistingByGlobal(0) && !isNodeExistingByGlobal(1)) if (!isNodeExistingByGlobal(0) && !isNodeExistingByGlobal(1))
{ {
@@ -541,10 +563,54 @@ void handleProgram1()
} }
} }
// --- Bottom text section ---
if (!program_bottom_text_enabled)
{
// Kill active scroll node if any
if (scroll_node_active)
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].disappear_time != 0)
{
text_nodes[i].disappear_time = 1;
}
}
scroll_node_active = false;
}
if (program2_active)
{
program2_active = false;
}
return;
}
// Skip bottom text management if vehicles are active
if (program2_active) return;
// Bottom row: alternating scrolling texts // Bottom row: alternating scrolling texts
if (main_animation_started && if ((main_animation_started || !program_top_text_enabled) &&
(!scroll_node_active || !isNodeExistingByGlobal(scroll_node_global_id))) (!scroll_node_active || !isNodeExistingByGlobal(scroll_node_global_id)))
{ {
// A bottom text just finished — check if we should trigger vehicles
if (scroll_node_active)
{
bottom_text_iteration_count++;
scroll_node_active = false;
if (program_vehicles_enabled && bottom_text_iteration_count >= PROGRAM2_TEXT_ITERATIONS)
{
bottom_text_iteration_count = 0;
if ((unsigned char)random(0, 100) < PROGRAM2_TRIGGER_CHANCE)
{
program2_active = true;
resetProgram2();
fillPixels(0, 9, DISPLAY_MAX_X, DISPLAY_MAX_Y, 0);
return;
}
}
}
TextNode* node = nullptr; TextNode* node = nullptr;
if (bottom_text_state == 0) if (bottom_text_state == 0)
{ {
@@ -596,7 +662,16 @@ void drawVehichle(unsigned char index = 0, short pos_x = 0, short pos_y = 0)
unsigned char program2_scroll_progress = 0; unsigned char program2_scroll_progress = 0;
short program2_spacing_counter = 0; short program2_spacing_counter = 0;
void handleProgram2(unsigned char spacing = 4, unsigned char scroll_slowness = 1, short y = 0) void resetProgram2()
{
program2_scroll_progress = 0;
program2_spacing_counter = 0;
program2_vehicles_spawned = 0;
program2_drain_counter = 0;
program2_done = false;
}
void handleProgram2(unsigned char spacing = 4, unsigned char scroll_slowness = 1, short y = 0, unsigned char max_vehicles = 0)
{ {
if (scroll_slowness > program2_scroll_progress) if (scroll_slowness > program2_scroll_progress)
{ {
@@ -607,11 +682,26 @@ void handleProgram2(unsigned char spacing = 4, unsigned char scroll_slowness = 1
shiftGivenRectangleLeft(0, y, DISPLAY_MAX_X, y + 3, 1); shiftGivenRectangleLeft(0, y, DISPLAY_MAX_X, y + 3, 1);
// If we have a vehicle limit and reached it, drain remaining vehicles off screen
if (max_vehicles > 0 && program2_vehicles_spawned >= max_vehicles)
{
program2_drain_counter++;
if (program2_drain_counter >= DISPLAY_MAX_X + 1)
{
program2_done = true;
}
return;
}
program2_spacing_counter--; program2_spacing_counter--;
if (program2_spacing_counter <= 0) if (program2_spacing_counter <= 0)
{ {
drawVehichle(random(0, 11), DISPLAY_MAX_X - 7, y); drawVehichle(random(0, 11), DISPLAY_MAX_X - 7, y);
program2_spacing_counter = 8 + spacing; program2_spacing_counter = 8 + spacing;
if (max_vehicles > 0)
{
program2_vehicles_spawned++;
}
} }
} }
@@ -632,13 +722,50 @@ void loop()
{ {
handle_server(); handle_server();
handleProgram2(4, 1, 0); // Clear top area for text redraw
fillPixels(0, 0, DISPLAY_MAX_X, 7, 0);
// Manage top text + bottom state machine
handleProgram1();
// Draw/scroll top "Witamy w PTI" (skip if disabled — area already cleared)
if (program_top_text_enabled)
{
scrollAllMultiColorTexts(true);
handleMultiColorDisappearTimers();
}
// Handle bottom text disable mid-frame
if (!program_bottom_text_enabled)
{
if (program2_active)
{
program2_active = false;
}
fillPixels(0, 9, DISPLAY_MAX_X, DISPLAY_MAX_Y, 0);
}
// Handle vehicles disabled while vehicle animation active
else if (program2_active && !program_vehicles_enabled)
{
program2_active = false;
fillPixels(0, 9, DISPLAY_MAX_X, DISPLAY_MAX_Y, 0);
}
else if (program2_active)
{
handleProgram2(4, 1, 12, PROGRAM2_VEHICLE_COUNT);
if (program2_done)
{
program2_active = false;
fillPixels(0, 9, DISPLAY_MAX_X, DISPLAY_MAX_Y, 0);
}
}
else
{
// Clear bottom area for text redraw
fillPixels(0, 9, DISPLAY_MAX_X, DISPLAY_MAX_Y, 0);
scrollAllScrollableTexts(true);
handleDisappearTimers();
}
// pixels.clear();
// handleProgram1();
// handleDisappearTimers();
// handleMultiColorDisappearTimers();
// scrollAllScrollableTexts(true);
// scrollAllMultiColorTexts(true);
pixels.show(); pixels.show();
} }
+6
View File
@@ -12,6 +12,10 @@ extern Cursor cursor;
extern unsigned char brightness; extern unsigned char brightness;
extern Animation animations[MAX_ANIMATIONS_COUNT]; extern Animation animations[MAX_ANIMATIONS_COUNT];
extern bool program_top_text_enabled;
extern bool program_bottom_text_enabled;
extern bool program_vehicles_enabled;
void displayAnimationFrame(unsigned char animation_index); void displayAnimationFrame(unsigned char animation_index);
void tickAnimations(); void tickAnimations();
void playAnimation(unsigned char index, unsigned char frame_delay, bool loop); void playAnimation(unsigned char index, unsigned char frame_delay, bool loop);
@@ -35,6 +39,8 @@ void modifyTextNodeByGlobalId(unsigned char global_id, uint32_t new_color, char
void modifyMultiColorTextNodeByGlobalId(unsigned char global_id, char new_text[TEXT_MAX_LENGTH + 1], RGBWithIndex new_colors[4], unsigned char new_color_count); void modifyMultiColorTextNodeByGlobalId(unsigned char global_id, char new_text[TEXT_MAX_LENGTH + 1], RGBWithIndex new_colors[4], unsigned char new_color_count);
bool isNodeExistingByGlobal(unsigned char global_id); bool isNodeExistingByGlobal(unsigned char global_id);
void handleProgram1(); void handleProgram1();
void handleProgram2(unsigned char spacing, unsigned char scroll_slowness, short y, unsigned char max_vehicles);
void resetProgram2();
#endif // PROTOTYPES_H #endif // PROTOTYPES_H
+38 -330
View File
@@ -2,7 +2,6 @@
#include <WebServer.h> #include <WebServer.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "structs.h" #include "structs.h"
#include "upload_page.h"
#include "prototypes.h" #include "prototypes.h"
const char* ssid = "PPIA"; const char* ssid = "PPIA";
@@ -11,12 +10,6 @@ unsigned char brightness = 100;
WebServer server(80); WebServer server(80);
Pixel new_image[16][16];
char* fileContent = nullptr;
size_t fileContent_len = 0;
String upload_error_message = "";
void handleBrightness() { void handleBrightness() {
if (server.hasArg("value")) { if (server.hasArg("value")) {
brightness = server.arg("value").toInt(); brightness = server.arg("value").toInt();
@@ -26,327 +19,53 @@ void handleBrightness() {
void handleRoot() void handleRoot()
{ {
server.send(200, "text/html", index_html); size_t len = strlen(index_html);
} server.setContentLength(len);
server.send(200, "text/html", "");
void handleUploadPage() const size_t CHUNK = 256;
{ for (size_t i = 0; i < len; i += CHUNK) {
server.send(200, "text/html", upload_page_html); size_t n = (len - i < CHUNK) ? len - i : CHUNK;
} server.client().write((const uint8_t*)(index_html + i), n);
void handleBmpUpload() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
upload_error_message = "";
if (fileContent) {
free(fileContent);
fileContent = nullptr;
fileContent_len = 0;
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
if (upload.name == "image") {
fileContent = (char*)realloc(fileContent, fileContent_len + upload.currentSize);
memcpy(fileContent + fileContent_len, upload.buf, upload.currentSize);
fileContent_len += upload.currentSize;
}
} else if (upload.status == UPLOAD_FILE_END) {
if (upload.name == "image") {
// Read BMP header
char header[54];
memcpy(header, fileContent, 54);
int dataOffset = *(int*)&header[10];
int width = *(int*)&header[18];
int height = *(int*)&header[22];
short bitsPerPixel = *(short*)&header[28];
if (bitsPerPixel != 24) {
upload_error_message = "Unsupported BMP format: Only 24-bit BMPs are supported";
return;
}
if (width < 1 || width > 48 || height < 1 || height > 16) {
upload_error_message = "Invalid image dimensions";
return;
}
saved_images[0].width = width;
saved_images[0].height = height;
// Read pixel data
int row_padded = (width*3 + 3) & (~3);
char* pixel_data = fileContent + dataOffset;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int bmp_row = height - 1 - y;
saved_images[0].pixels[y][x].b = pixel_data[bmp_row*row_padded + x*3 + 0];
saved_images[0].pixels[y][x].g = pixel_data[bmp_row*row_padded + x*3 + 1];
saved_images[0].pixels[y][x].r = pixel_data[bmp_row*row_padded + x*3 + 2];
}
}
free(fileContent);
fileContent = nullptr;
fileContent_len = 0;
}
} }
} }
void handleToggleProgram() {
if (server.hasArg("program") && server.hasArg("enabled")) {
String program = server.arg("program");
bool enabled = server.arg("enabled") == "true";
if (program == "top_text") {
program_top_text_enabled = enabled;
void handleText() { } else if (program == "bottom_text") {
if (server.hasArg("text") && server.hasArg("color") && server.hasArg("slowness")) { program_bottom_text_enabled = enabled;
String text_str = server.arg("text"); } else if (program == "vehicles") {
String colorStr = server.arg("color"); program_vehicles_enabled = enabled;
unsigned char slowness = server.arg("slowness").toInt();
short disappear_time = server.hasArg("disappear") ? server.arg("disappear").toInt() : -1;
bool is_small = true;
if (server.hasArg("fontSize") && server.arg("fontSize") != "") {
if (server.arg("fontSize") == "medium") {
is_small = false;
}
}
bool is_repeating = false;
if (server.hasArg("is_repeating")) {
is_repeating = server.arg("is_repeating") == "true";
}
char text[TEXT_MAX_LENGTH + 1];
text_str.toCharArray(text, TEXT_MAX_LENGTH + 1);
uint32_t color = strtol(colorStr.substring(1).c_str(), NULL, 16);
if (server.hasArg("position")) {
String position = server.arg("position");
if (position == "top") {
addNewTextNode(text, color, false, 43, 0, slowness, true, is_small, disappear_time, is_repeating);
} else if (position == "bottom") {
addNewTextNode(text, color, false, -text_str.length() * 6, 9, slowness, true, is_small, disappear_time, is_repeating);
}
server.send(200, "text/plain", "OK");
} else { } else {
short x = 0; server.send(400, "text/plain", "Unknown program");
short y = 0; return;
if (server.hasArg("x") && server.arg("x") != "") {
x = server.arg("x").toInt();
}
if (server.hasArg("y") && server.arg("y") != "") {
y = server.arg("y").toInt();
}
addNewTextNode(text, color, false, x, y, slowness, true, is_small, disappear_time, is_repeating);
server.send(200, "text/plain", "OK");
} }
} else {
server.send(400, "text/plain", "Invalid arguments for /text");
}
}
void handleMulticolorText() {
if (server.hasArg("text") && server.hasArg("colors") && server.hasArg("slowness")) {
String text_str = server.arg("text");
String colors_str = server.arg("colors");
unsigned char slowness = server.arg("slowness").toInt();
short disappear_time = server.hasArg("disappear") ? server.arg("disappear").toInt() : -1;
bool is_small = true;
if (server.hasArg("fontSize") && server.arg("fontSize") != "") {
if (server.arg("fontSize") == "medium") {
is_small = false;
}
}
bool is_repeating = false;
if (server.hasArg("is_repeating")) {
is_repeating = server.arg("is_repeating") == "true";
}
char text[TEXT_MAX_LENGTH + 1];
text_str.toCharArray(text, TEXT_MAX_LENGTH + 1);
RGBWithIndex colors[4];
int color_count = 0;
String color_part = "";
int last_comma = -1;
for (int i = 0; i < colors_str.length() && color_count < 4; i++) {
if (colors_str.charAt(i) == ',') {
color_part = colors_str.substring(last_comma + 1, i);
last_comma = i;
if (color_part.length() > 0) {
long color_val = strtol(color_part.substring(1).c_str(), NULL, 16);
colors[color_count].r = (color_val >> 16) & 0xFF;
colors[color_count].g = (color_val >> 8) & 0xFF;
colors[color_count].b = color_val & 0xFF;
color_count++;
}
}
}
// last color
if (color_count < 4) {
color_part = colors_str.substring(last_comma + 1);
if (color_part.length() > 0) {
long color_val = strtol(color_part.substring(1).c_str(), NULL, 16);
colors[color_count].r = (color_val >> 16) & 0xFF;
colors[color_count].g = (color_val >> 8) & 0xFF;
colors[color_count].b = color_val & 0xFF;
color_count++;
}
}
if (color_count > 0) {
int text_len = text_str.length();
int part_len = text_len / color_count;
for (int i = 0; i < color_count; i++) {
colors[i].start_index = i * part_len;
}
}
short x = 0;
short y = 0;
if (server.hasArg("x") && server.arg("x") != "") {
x = server.arg("x").toInt();
}
if (server.hasArg("y") && server.arg("y") != "") {
y = server.arg("y").toInt();
}
addNewMultiColor(text, colors, color_count, false, x, y, slowness, true, is_small, disappear_time, is_repeating);
server.send(200, "text/plain", "OK"); server.send(200, "text/plain", "OK");
} else { } else {
server.send(400, "text/plain", "Invalid arguments for /multicolor-text"); server.send(400, "text/plain", "Missing program or enabled parameter");
}
} }
}
void handleModifyText() void handleProgramStatus() {
{ StaticJsonDocument<128> doc;
if (server.hasArg("global_id") && server.hasArg("color")) doc["top_text"] = program_top_text_enabled;
{ doc["bottom_text"] = program_bottom_text_enabled;
unsigned char global_id = server.arg("global_id").toInt(); doc["vehicles"] = program_vehicles_enabled;
String colorStr = server.arg("color"); doc["brightness"] = brightness;
uint32_t color = strtol(colorStr.substring(1).c_str(), NULL, 16);
String text_str = server.arg("text");
char text[TEXT_MAX_LENGTH + 1];
text_str.toCharArray(text, TEXT_MAX_LENGTH + 1);
unsigned char slowness = server.arg("slowness").toInt();
modifyTextNodeByGlobalId(global_id, color, text, slowness);
server.send(200, "text/plain", "OK");
}
else
{
server.send(400, "text/plain", "Invalid arguments for /modify-text");
}
}
void handleModifyMultiColorText() String json;
{ serializeJson(doc, json);
if (server.hasArg("global_id") && server.hasArg("colors") && server.hasArg("text")) server.send(200, "application/json", json);
{ }
unsigned char global_id = server.arg("global_id").toInt();
String colors_str = server.arg("colors");
String text_str = server.arg("text");
char text[TEXT_MAX_LENGTH + 1];
text_str.toCharArray(text, TEXT_MAX_LENGTH + 1);
RGBWithIndex colors[4]; void start_server()
int color_count = 0; {
String color_part = ""; WiFi.begin(ssid, password);
int last_comma = -1; while (WiFi.status() != WL_CONNECTED) {
for (int i = 0; i < colors_str.length() && color_count < 4; i++) {
if (colors_str.charAt(i) == ',') {
color_part = colors_str.substring(last_comma + 1, i);
last_comma = i;
if (color_part.length() > 0) {
long color_val = strtol(color_part.substring(1).c_str(), NULL, 16);
colors[color_count].r = (color_val >> 16) & 0xFF;
colors[color_count].g = (color_val >> 8) & 0xFF;
colors[color_count].b = color_val & 0xFF;
color_count++;
}
}
}
if (color_count < 4) {
color_part = colors_str.substring(last_comma + 1);
if (color_part.length() > 0) {
long color_val = strtol(color_part.substring(1).c_str(), NULL, 16);
colors[color_count].r = (color_val >> 16) & 0xFF;
colors[color_count].g = (color_val >> 8) & 0xFF;
colors[color_count].b = color_val & 0xFF;
color_count++;
}
}
if (color_count > 0) {
int text_len = text_str.length();
int part_len = text_len / color_count;
for (int i = 0; i < color_count; i++) {
colors[i].start_index = i * part_len;
}
}
modifyMultiColorTextNodeByGlobalId(global_id, text, colors, color_count);
server.send(200, "text/plain", "OK");
}
else
{
server.send(400, "text/plain", "Invalid arguments for /modify-multicolor-text");
}
}
void handleGetNodes()
{
StaticJsonDocument<2048> jsonDoc;
JsonArray nodes = jsonDoc.to<JsonArray>();
for (int i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].disappear_time != 0)
{
JsonObject node = nodes.createNestedObject();
node["global_id"] = text_nodes[i].global_id;
node["text"] = text_nodes[i].content;
node["color"] = text_nodes[i].color;
node["x"] = text_nodes[i].pos_x;
node["y"] = text_nodes[i].pos_y;
node["slowness"] = text_nodes[i].scroll_slowness;
node["is_repeating"] = text_nodes[i].is_repeating;
node["type"] = "text";
node["slowness"] = text_nodes[i].scroll_slowness;
}
}
for (int i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (multi_color_text_node[i].disappear_time != 0)
{
JsonObject node = nodes.createNestedObject();
node["global_id"] = multi_color_text_node[i].global_id;
node["text"] = multi_color_text_node[i].content;
JsonArray colors = node.createNestedArray("colors");
for(int j = 0; j < multi_color_text_node[i].color_count; j++)
{
JsonObject color = colors.createNestedObject();
color["r"] = multi_color_text_node[i].colors[j].r;
color["g"] = multi_color_text_node[i].colors[j].g;
color["b"] = multi_color_text_node[i].colors[j].b;
}
node["x"] = multi_color_text_node[i].pos_x;
node["y"] = multi_color_text_node[i].pos_y;
node["slowness"] = multi_color_text_node[i].scroll_slowness;
node["is_repeating"] = multi_color_text_node[i].is_repeating;
node["type"] = "multi-color";
node["slowness"] = multi_color_text_node[i].scroll_slowness;
}
}
String jsonString;
serializeJson(jsonDoc, jsonString);
server.send(200, "application/json", jsonString);
}
void start_server()
{
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
pixels.clear(); pixels.clear();
scrollAllScrollableTexts(true); scrollAllScrollableTexts(true);
pixels.show(); pixels.show();
@@ -356,20 +75,9 @@ void handleMulticolorText() {
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
server.on("/", handleRoot); server.on("/", handleRoot);
server.on("/nodes", HTTP_GET, handleGetNodes); server.on("/toggle-program", HTTP_POST, handleToggleProgram);
server.on("/text", HTTP_POST, handleText); server.on("/program-status", HTTP_GET, handleProgramStatus);
server.on("/modify-text", HTTP_POST, handleModifyText);
server.on("/multicolor-text", HTTP_POST, handleMulticolorText);
server.on("/modify-multicolor-text", HTTP_POST, handleModifyMultiColorText);
server.on("/brightness", HTTP_POST, handleBrightness); server.on("/brightness", HTTP_POST, handleBrightness);
server.on("/upload-page", HTTP_GET, handleUploadPage);
server.on("/upload-bmp", HTTP_POST, []() {
if (upload_error_message.length() > 0) {
server.send(400, "text/plain", upload_error_message);
} else {
server.send(200, "text/plain", "Upload OK");
}
}, handleBmpUpload);
server.begin(); server.begin();
} }