Files
2026-02-12 10:20:00 +01:00

771 lines
21 KiB
Arduino

#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
#include "config.h"
#include "index.h"
#include "fonts.h"
#include "lowLevel.h"
#include "prototypes.h"
#ifdef __AVR__
#include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
TextNode text_nodes[MAX_TEXT_NODES_COUNT];
MultiColorTextNode multi_color_text_node[MAX_TEXT_NODES_COUNT];
Cursor cursor;
unsigned char global_node_id = 0;
bool main_animation_started = false;
unsigned char bottom_text_state = 0;
unsigned char scroll_node_global_id = 0;
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] = {
{},{}
};
const RGB animation_data[MAX_ANIMATIONS_COUNT][MAX_ANIMATION_FRAME_COUNT][PANEL_PIXEL_COUNT][DISPLAY_MAX_X + 1] = {};
Animation animations[MAX_ANIMATIONS_COUNT];
bool vehichles[13][4][8] =
{
{
{0},
{0,0,1,1,1,1,1,0},
{1,1,1,1,1,1,1,1},
{0,1,0,0,0,0,1,0}
},
{
{0,0,1,1,0,0,0,0},
{0,1,1,1,0,0,0,0},
{1,1,1,1,1,1,1,1},
{0,1,0,0,0,0,1,0}
},
{
{0,0,1,1,0,1,1,1},
{0,1,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{0,1,0,0,0,0,1,0}
},
{
{0,0,0,0,1,1,0,0},
{0,0,1,1,1,1,1,0},
{0,1,1,1,1,1,1,1},
{0,0,1,0,0,0,1,0}
},
{
{0},
{0,0,0,1,1,1,1,1},
{0,0,1,1,1,1,1,1},
{0,0,0,1,0,0,1,0}
},
{
{1,1,1,1,1,1,1,1},
{0,1,0,1,0,1,0,1},
{0,1,1,1,1,1,1,1},
{0,0,1,0,0,0,1,0}
},
{
{0},
{0,0,1,1,0,0,0,0},
{1,1,1,1,1,1,1,1},
{0,1,0,0,0,0,1,0},
},
{
{1,1,0,1,1,1,1,1},
{1,1,0,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{0,1,0,0,0,0,1,0}
},
{
{0,0,0,1,1,1,1,1},
{0,0,1,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{0,1,0,0,0,0,1,0}
},
{
{0},
{0,1,1,1,0,0,0,1},
{1,1,1,1,1,1,1,0},
{0,1,0,0,0,1,0,0}
},
{
{0,1,0,1,1,1,1,1},
{1,1,0,1,1,1,1,1},
{1,1,1,1,1,1,1,1},
{0,0,1,0,0,0,1,0}
},
{
{0},
{0,0,0,0,0,1,1,0},
{0,0,0,1,1,1,1,1},
{0,0,0,0,1,0,1,0}
}
};
short getTextNodeY2(TextNode *node)
{
return node->pos_y + node->characterSize.height - 1;
}
short getTextNodeX2(TextNode *node)
{
if (node->character_count == 0) return node->pos_x;
return node->pos_x + (node->characterSize.width * node->character_count) + (node->character_count - 1) - 1;
}
TextNode* addNewTextNode
(
char text[TEXT_MAX_LENGTH + 1], uint32_t color, bool handle_pos_via_cursor, short pos_x, short pos_y,
unsigned char scroll_slowness, bool is_scrolling, bool is_small, short disappear_time, bool is_repeating
)
{
unsigned char text_length = strlen(text);
if (text_length == 0){return nullptr;}
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].disappear_time == 0)
{
strncpy(text_nodes[i].content, text, TEXT_MAX_LENGTH);
text_nodes[i].color = color;
if (handle_pos_via_cursor)
{
text_nodes[i].pos_x = cursor.x;
text_nodes[i].pos_y = cursor.y;
}
else
{
text_nodes[i].pos_x = pos_x;
text_nodes[i].pos_y = pos_y;
}
text_nodes[i].characterSize.height = is_small ? SMALL_TEXT_HEIGHT : MEDIUM_TEXT_HEIGHT;
text_nodes[i].characterSize.width = is_small ? SMALL_TEXT_WIDTH : MEDIUM_TEXT_WIDTH;
text_nodes[i].character_count = text_length;
text_nodes[i].scroll_slowness = scroll_slowness;
text_nodes[i].is_scrolling = is_scrolling;
text_nodes[i].is_repeating = is_repeating;
text_nodes[i].disappear_time = disappear_time;
text_nodes[i].global_id = global_node_id;
global_node_id++;
if (handle_pos_via_cursor)
{
cursor.x += (text_nodes[i].characterSize.width + 1) * text_length;
}
return &text_nodes[i];
}
}
return nullptr;
}
void modifyTextNodeByGlobalId(unsigned char global_id, uint32_t new_color, char new_text[TEXT_MAX_LENGTH + 1], unsigned char new_slowness)
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].global_id == global_id && text_nodes[i].disappear_time != 0)
{
text_nodes[i].color = new_color;
strncpy(text_nodes[i].content, new_text, TEXT_MAX_LENGTH);
text_nodes[i].character_count = strlen(new_text);
text_nodes[i].scroll_slowness = new_slowness;
break;
}
}
}
void modifyMultiColorTextNodeByGlobalId(unsigned char global_id, char new_text[TEXT_MAX_LENGTH + 1], RGBWithIndex new_colors[4], unsigned char new_color_count)
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (multi_color_text_node[i].global_id == global_id && multi_color_text_node[i].disappear_time != 0)
{
strncpy(multi_color_text_node[i].content, new_text, TEXT_MAX_LENGTH);
multi_color_text_node[i].character_count = strlen(new_text);
multi_color_text_node[i].color_count = new_color_count;
for(int j = 0; j < new_color_count; j++)
{
multi_color_text_node[i].colors[j] = new_colors[j];
}
break;
}
}
}
void scrollAllScrollableTexts(bool split_scroll_mode = false)
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].disappear_time == 0) {continue;}
if (text_nodes[i].is_scrolling)
{
if (text_nodes[i].scroll_slowness > text_nodes[i].scroll_progress)
{
text_nodes[i].scroll_progress++;
}
else
{
text_nodes[i].scroll_progress = 0;
short x1 = text_nodes[i].pos_x;
short x2 = getTextNodeX2(&text_nodes[i]);
if (split_scroll_mode || text_nodes[i].pos_y >= 7)
{
if (x2 < 0)
{
if (text_nodes[i].is_repeating)
{
text_nodes[i].pos_x = DISPLAY_MAX_X;
}
else
{
text_nodes[i].disappear_time = 1;
}
continue;
}
text_nodes[i].pos_x--;
}
else
{
if (x1 > DISPLAY_MAX_X)
{
if (text_nodes[i].is_repeating)
{
text_nodes[i].pos_x = -getTextNodeX2(&text_nodes[i]) + text_nodes[i].pos_x;
}
else
{
text_nodes[i].disappear_time = 1;
}
continue;
}
text_nodes[i].pos_x++;
}
}
}
cursor.x = text_nodes[i].pos_x;
cursor.y = text_nodes[i].pos_y;
for (unsigned char j = 0; j < text_nodes[i].character_count; j++)
{
char ch = text_nodes[i].content[j];
if (ch < '!' || ch > '~')
{
ch = ' ';
}
drawCharacter(font7x5[ch - ' '], text_nodes[i].characterSize.height, text_nodes[i].characterSize.width, text_nodes[i].color, &cursor);
}
}
}
MultiColorTextNode* addNewMultiColor
(
char text[TEXT_MAX_LENGTH + 1], RGBWithIndex colors[4], unsigned char color_count, bool handle_pos_via_cursor, short pos_x, short pos_y,
unsigned char scroll_slowness, bool is_scrolling, bool is_small, short disappear_time, bool is_repeating
)
{
unsigned char text_length = strlen(text);
if (text_length == 0){return nullptr;}
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (multi_color_text_node[i].disappear_time == 0)
{
strncpy(multi_color_text_node[i].content, text, TEXT_MAX_LENGTH);
multi_color_text_node[i].color_count = color_count;
for(int j = 0; j < color_count; j++)
{
multi_color_text_node[i].colors[j] = colors[j];
}
if (handle_pos_via_cursor)
{
multi_color_text_node[i].pos_x = cursor.x;
multi_color_text_node[i].pos_y = cursor.y;
}
else
{
multi_color_text_node[i].pos_x = pos_x;
multi_color_text_node[i].pos_y = pos_y;
}
multi_color_text_node[i].characterSize.height = is_small ? SMALL_TEXT_HEIGHT : MEDIUM_TEXT_HEIGHT;
multi_color_text_node[i].characterSize.width = is_small ? SMALL_TEXT_WIDTH : MEDIUM_TEXT_WIDTH;
multi_color_text_node[i].character_count = text_length;
multi_color_text_node[i].scroll_slowness = scroll_slowness;
multi_color_text_node[i].is_scrolling = is_scrolling;
multi_color_text_node[i].is_repeating = is_repeating;
multi_color_text_node[i].disappear_time = disappear_time;
multi_color_text_node[i].global_id = global_node_id;
global_node_id++;
if (handle_pos_via_cursor)
{
cursor.x += (multi_color_text_node[i].characterSize.width + 1) * text_length;
}
return &multi_color_text_node[i];
}
}
return nullptr;
}
short getMultiColorTextNodeX2(MultiColorTextNode *node)
{
if (node->character_count == 0) return node->pos_x;
return node->pos_x + (node->characterSize.width * node->character_count) + (node->character_count - 1) - 1;
}
void scrollAllMultiColorTexts(bool split_scroll_mode = false)
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (multi_color_text_node[i].disappear_time == 0) {continue;}
if (multi_color_text_node[i].is_scrolling)
{
if (multi_color_text_node[i].scroll_slowness > multi_color_text_node[i].scroll_progress)
{
multi_color_text_node[i].scroll_progress++;
}
else
{
multi_color_text_node[i].scroll_progress = 0;
short x1 = multi_color_text_node[i].pos_x;
short x2 = getMultiColorTextNodeX2(&multi_color_text_node[i]);
if (split_scroll_mode || multi_color_text_node[i].pos_y >= 7)
{
if (x2 < 0)
{
if (multi_color_text_node[i].is_repeating)
{
multi_color_text_node[i].pos_x = DISPLAY_MAX_X;
}
else
{
multi_color_text_node[i].disappear_time = 1;
}
continue;
}
multi_color_text_node[i].pos_x--;
}
else
{
if (x1 > DISPLAY_MAX_X)
{
if (multi_color_text_node[i].is_repeating)
{
multi_color_text_node[i].pos_x = -getMultiColorTextNodeX2(&multi_color_text_node[i]) + multi_color_text_node[i].pos_x;
}
else
{
multi_color_text_node[i].disappear_time = 1;
}
continue;
}
multi_color_text_node[i].pos_x++;
}
}
}
cursor.x = multi_color_text_node[i].pos_x;
cursor.y = multi_color_text_node[i].pos_y;
uint32_t current_color = pixels.Color(multi_color_text_node[i].colors[0].r, multi_color_text_node[i].colors[0].g, multi_color_text_node[i].colors[0].b);
unsigned char color_index = 0;
for (unsigned char j = 0; j < multi_color_text_node[i].character_count; j++)
{
if (color_index < multi_color_text_node[i].color_count - 1 && multi_color_text_node[i].colors[color_index+1].start_index == j)
{
color_index++;
current_color = pixels.Color(multi_color_text_node[i].colors[color_index].r, multi_color_text_node[i].colors[color_index].g, multi_color_text_node[i].colors[color_index].b);
}
char ch = multi_color_text_node[i].content[j];
if (ch < '!' || ch > '~')
{
ch = ' ';
}
drawCharacter(font7x5[ch - ' '], multi_color_text_node[i].characterSize.height, multi_color_text_node[i].characterSize.width, current_color, &cursor);
}
}
}
void drawImageFromMemoryByIndex(unsigned char image_index, short pos_x, short pos_y, unsigned char brightness)
{
Image* img = &saved_images[image_index];
auto dimBy = [brightness](unsigned char color)
{
return color * brightness / 100;
};
for (unsigned char y = 0; y < img->height; y++)
{
for (unsigned char x = 0; x < img->width; x++)
{
setPixel(x + pos_x, y + pos_y, pixels.Color(dimBy(img->pixels[y][x].r), dimBy(img->pixels[y][x].g), dimBy(img->pixels[y][x].b)));
}
}
}
void displayAnimationFrame(unsigned char animation_index)
{
unsigned short frame = animations[animation_index].current_frame;
for (unsigned char y = 0; y < PANEL_PIXEL_COUNT; y++)
{
for (unsigned char x = 0; x <= DISPLAY_MAX_X; x++)
{
const RGB& px = animation_data[animation_index][frame][y][x];
setPixel(x, y, pixels.Color(px.r * brightness / 100, px.g * brightness / 100, px.b * brightness / 100));
}
}
}
void tickAnimations()
{
for (unsigned char i = 0; i < MAX_ANIMATIONS_COUNT; i++)
{
if (!animations[i].is_playing) continue;
animations[i].frame_progress++;
if (animations[i].frame_progress > animations[i].frame_delay)
{
animations[i].frame_progress = 0;
animations[i].current_frame++;
if (animations[i].current_frame >= animations[i].frame_count)
{
if (animations[i].is_looping)
{
animations[i].current_frame = 0;
}
else
{
animations[i].current_frame = animations[i].frame_count - 1;
animations[i].is_playing = false;
}
}
}
}
}
void playAnimation(unsigned char index, unsigned char frame_delay, bool loop)
{
animations[index].current_frame = 0;
animations[index].frame_progress = 0;
animations[index].frame_delay = frame_delay;
animations[index].is_playing = true;
animations[index].is_looping = loop;
}
void stopAnimation(unsigned char index)
{
animations[index].is_playing = false;
}
void handleDisappearTimers()
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].disappear_time > 0)
{
text_nodes[i].disappear_time--;
if (text_nodes[i].disappear_time == 0)
{
fillPixels(text_nodes[i].pos_x, text_nodes[i].pos_y, getTextNodeX2(&text_nodes[i]), getTextNodeY2(&text_nodes[i]), 0);
}
}
}
}
void handleMultiColorDisappearTimers()
{
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--;
if (multi_color_text_node[i].disappear_time == 0)
{
fillPixels(multi_color_text_node[i].pos_x, multi_color_text_node[i].pos_y, getMultiColorTextNodeX2(&multi_color_text_node[i]), multi_color_text_node[i].pos_y + multi_color_text_node[i].characterSize.height - 1, 0);
}
}
}
}
bool isNodeExistingByGlobal(unsigned char global_id)
{
for (unsigned char i = 0; i < MAX_TEXT_NODES_COUNT; i++)
{
if (text_nodes[i].global_id == global_id && text_nodes[i].disappear_time != 0)
{
return true;
}
if (multi_color_text_node[i].global_id == global_id && multi_color_text_node[i].disappear_time != 0)
{
return true;
}
}
return false;
}
void resetGlobalIdToAfterIntroValue()
{
global_node_id = 2;
}
void handleProgram1()
{
// --- 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))
{
main_animation_started = true;
// Top row: "Witamy w PTI" — slow scroll, repeating, white + blue PTI
addNewMultiColor("Witamy w PTI",
(RGBWithIndex[4]){
{40, 40, 40, 0}, // white (dimmed) from char 0
{0, 0, 50, 9} // blue (dimmed) from char 9 ("PTI")
}, 2, false, 0, 0, 3, true, true, -1, true);
}
}
// --- 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
if ((main_animation_started || !program_top_text_enabled) &&
(!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;
if (bottom_text_state == 0)
{
node = addNewTextNode("Technik informatyk + ai, robotyka",
pixels.Color(0, 0, 50),
false, DISPLAY_MAX_X, 9, 1, true, true, -1, false);
}
else if (bottom_text_state == 1)
{
node = addNewTextNode("Technik programista",
pixels.Color(50, 40, 0),
false, DISPLAY_MAX_X, 9, 1, true, true, -1, false);
}
else if (bottom_text_state == 2)
{
node = addNewTextNode("Technik poligrafii i grafiki komputerowej",
pixels.Color(0, 45, 0),
false, DISPLAY_MAX_X, 9, 1, true, true, -1, false);
}
else
{
node = addNewTextNode("Technik reklamy",
pixels.Color(60, 20, 0),
false, DISPLAY_MAX_X, 9, 1, true, true, -1, false);
}
if (node != nullptr)
{
scroll_node_global_id = node->global_id;
scroll_node_active = true;
bottom_text_state = (bottom_text_state + 1) % 4;
}
}
}
void drawVehichle(unsigned char index = 0, short pos_x = 0, short pos_y = 0)
{
for (unsigned char i = 0; i < 4; i++)
{
for (unsigned char j = 0; j < 8; j++)
{
if (vehichles[index][i][j])
{
setPixel(pos_x + j, pos_y + i, VEHICHLE_COLOR);
}
}
}
}
unsigned char program2_scroll_progress = 0;
short program2_spacing_counter = 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)
{
program2_scroll_progress++;
return;
}
program2_scroll_progress = 0;
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--;
if (program2_spacing_counter <= 0)
{
drawVehichle(random(0, 11), DISPLAY_MAX_X - 7, y);
program2_spacing_counter = 8 + spacing;
if (max_vehicles > 0)
{
program2_vehicles_spawned++;
}
}
}
void setup()
{
Serial.begin(115200);
Serial.println("----------------------------------------------------");
pixels.begin();
pixels.clear();
scrollAllScrollableTexts(true);
pixels.show();
start_server();
}
void loop()
{
handle_server();
// 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.show();
}