move into firmware subfolder
Signed-off-by: Peter Siegmund <developer@mars3142.org>
This commit is contained in:
35
firmware/components/insa/src/common/InactivityTracker.cpp
Normal file
35
firmware/components/insa/src/common/InactivityTracker.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#include "common/InactivityTracker.h"
|
||||
|
||||
InactivityTracker::InactivityTracker(const uint64_t timeoutMs, const std::function<void()> &onTimeout)
|
||||
: m_timeoutMs(timeoutMs), m_elapsedTime(0), m_enabled(true), m_onTimeout(onTimeout)
|
||||
{
|
||||
}
|
||||
|
||||
void InactivityTracker::update(uint64_t dt)
|
||||
{
|
||||
if (!m_enabled)
|
||||
return;
|
||||
|
||||
m_elapsedTime += dt;
|
||||
|
||||
if (m_elapsedTime >= m_timeoutMs && m_onTimeout)
|
||||
{
|
||||
m_onTimeout();
|
||||
m_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void InactivityTracker::reset()
|
||||
{
|
||||
m_elapsedTime = 0;
|
||||
m_enabled = true;
|
||||
}
|
||||
|
||||
void InactivityTracker::setEnabled(bool enabled)
|
||||
{
|
||||
m_enabled = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
}
|
386
firmware/components/insa/src/common/Menu.cpp
Normal file
386
firmware/components/insa/src/common/Menu.cpp
Normal file
@@ -0,0 +1,386 @@
|
||||
#include "common/Menu.h"
|
||||
|
||||
#include "common/ScrollBar.h"
|
||||
#include "u8g2.h"
|
||||
|
||||
// Menu item type constants for better readability
|
||||
namespace MenuItemTypes
|
||||
{
|
||||
constexpr uint8_t TEXT = 0;
|
||||
constexpr uint8_t SELECTION = 1;
|
||||
constexpr uint8_t TOGGLE = 2;
|
||||
constexpr uint8_t TEXT_COUNTER = 3;
|
||||
} // namespace MenuItemTypes
|
||||
|
||||
// UI layout constants
|
||||
namespace UIConstants
|
||||
{
|
||||
constexpr int LEFT_MARGIN = 8;
|
||||
constexpr int RIGHT_PADDING = 8;
|
||||
constexpr int SCROLLBAR_WIDTH = 3;
|
||||
constexpr int FRAME_BOX_SIZE = 14;
|
||||
constexpr int FRAME_OFFSET = 11;
|
||||
constexpr int SELECTION_MARGIN = 10;
|
||||
constexpr int CORNER_RADIUS = 3;
|
||||
constexpr int LINE_SPACING = 14;
|
||||
constexpr int BOTTOM_OFFSET = 10;
|
||||
} // namespace UIConstants
|
||||
|
||||
Menu::Menu(menu_options_t *options) : Widget(options->u8g2), m_options(options)
|
||||
{
|
||||
// Set up button callback using lambda to forward to member function
|
||||
m_options->onButtonClicked = [this](const ButtonType button) { onButtonClicked(button); };
|
||||
}
|
||||
|
||||
Menu::~Menu()
|
||||
{
|
||||
// Clean up callback to prevent dangling pointer
|
||||
if (m_options)
|
||||
{
|
||||
m_options->onButtonClicked = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem Menu::getItem(const int index)
|
||||
{
|
||||
return m_items.at(index);
|
||||
}
|
||||
|
||||
size_t Menu::getItemCount() const
|
||||
{
|
||||
return m_items.size();
|
||||
}
|
||||
|
||||
void Menu::setItemSize(const size_t size)
|
||||
{
|
||||
if ((m_items.size() - 1) < size)
|
||||
{
|
||||
for (size_t i = m_items.size() - 1; i < size; i++)
|
||||
{
|
||||
auto caption = std::string("Bereich ") + std::to_string(i + 1);
|
||||
auto index = 0;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
constexpr int key_length = 20;
|
||||
char key[key_length] = "";
|
||||
snprintf(key, key_length, "section_%zu", i + 1);
|
||||
index = m_options->persistenceManager->GetValue(key, index);
|
||||
}
|
||||
addSelection(i + 1, caption, m_items.at(0).getValues(), index);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_items.erase(m_items.begin() + static_cast<int>(size + 1), m_items.end());
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::toggle(const MenuItem &menuItem)
|
||||
{
|
||||
const auto item =
|
||||
menuItem.copyWith(menuItem.getValue() == std::to_string(true) ? std::to_string(false) : std::to_string(true));
|
||||
replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
|
||||
MenuItem Menu::switchValue(const MenuItem &menuItem, ButtonType button)
|
||||
{
|
||||
MenuItem result = menuItem;
|
||||
switch (button)
|
||||
{
|
||||
case ButtonType::LEFT:
|
||||
if (menuItem.getIndex() > 0)
|
||||
{
|
||||
const auto item = menuItem.copyWith(menuItem.getIndex() - 1);
|
||||
result = replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto item = menuItem.copyWith(menuItem.getItemCount() - 1);
|
||||
result = replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
break;
|
||||
|
||||
case ButtonType::RIGHT:
|
||||
if (menuItem.getIndex() < menuItem.getItemCount() - 1)
|
||||
{
|
||||
const auto item = menuItem.copyWith(menuItem.getIndex() + 1);
|
||||
result = replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto item = menuItem.copyWith(0);
|
||||
result = replaceItem(menuItem.getId(), item);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MenuItem Menu::replaceItem(const int index, const MenuItem &item)
|
||||
{
|
||||
m_items.at(index) = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
void Menu::render()
|
||||
{
|
||||
// Initialize selection if not set
|
||||
if (m_selected_item >= m_items.size() && !m_items.empty())
|
||||
{
|
||||
m_selected_item = 0;
|
||||
}
|
||||
|
||||
// Early return if no items to render
|
||||
if (m_items.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear screen with black background
|
||||
u8g2_SetDrawColor(u8g2, 0);
|
||||
u8g2_DrawBox(u8g2, 0, 0, u8g2->width, u8g2->height);
|
||||
|
||||
// Set white foreground color for drawing
|
||||
u8g2_SetDrawColor(u8g2, 1);
|
||||
|
||||
// Draw UI components
|
||||
drawScrollBar();
|
||||
drawSelectionBox();
|
||||
|
||||
// Calculate center position for main item
|
||||
const int centerY = u8g2->height / 2 + 3;
|
||||
|
||||
// Render the currently selected item (main/center item)
|
||||
const auto &selectedItem = m_items[m_selected_item];
|
||||
renderWidget(&selectedItem, u8g2_font_helvB08_tr, UIConstants::LEFT_MARGIN, centerY);
|
||||
|
||||
// Render previous item (above) if available
|
||||
if (m_selected_item > 0)
|
||||
{
|
||||
const auto &prevItem = m_items[m_selected_item - 1];
|
||||
renderWidget(&prevItem, u8g2_font_haxrcorp4089_tr, UIConstants::LEFT_MARGIN, UIConstants::LINE_SPACING);
|
||||
}
|
||||
|
||||
// Render next item (below) if available
|
||||
if (m_selected_item < m_items.size() - 1)
|
||||
{
|
||||
const auto &nextItem = m_items[m_selected_item + 1];
|
||||
renderWidget(&nextItem, u8g2_font_haxrcorp4089_tr, UIConstants::LEFT_MARGIN,
|
||||
u8g2->height - UIConstants::BOTTOM_OFFSET);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::renderWidget(const MenuItem *item, const uint8_t *font, const int x, const int y) const
|
||||
{
|
||||
// Set font and draw main text
|
||||
u8g2_SetFont(u8g2, font);
|
||||
u8g2_DrawStr(u8g2, x, y, item->getText().c_str());
|
||||
|
||||
// Render type-specific elements
|
||||
switch (item->getType())
|
||||
{
|
||||
case MenuItemTypes::TEXT: {
|
||||
const std::string formattedValue = ">";
|
||||
const u8g2_uint_t textWidth = u8g2_GetStrWidth(u8g2, formattedValue.c_str());
|
||||
u8g2_DrawStr(u8g2, u8g2->width - textWidth - UIConstants::SELECTION_MARGIN, y, formattedValue.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuItemTypes::TEXT_COUNTER: {
|
||||
const std::string formattedValue = "(" + item->getValue() + ") >";
|
||||
const u8g2_uint_t textWidth = u8g2_GetStrWidth(u8g2, formattedValue.c_str());
|
||||
u8g2_DrawStr(u8g2, u8g2->width - textWidth - UIConstants::SELECTION_MARGIN, y, formattedValue.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuItemTypes::SELECTION: {
|
||||
// Format selection value with angle brackets
|
||||
const std::string formattedValue = "< " + item->getValue() + " >";
|
||||
const u8g2_uint_t textWidth = u8g2_GetStrWidth(u8g2, formattedValue.c_str());
|
||||
u8g2_DrawStr(u8g2, u8g2->width - textWidth - UIConstants::SELECTION_MARGIN, y, formattedValue.c_str());
|
||||
break;
|
||||
}
|
||||
|
||||
case MenuItemTypes::TOGGLE: {
|
||||
// Draw checkbox frame
|
||||
const int frameX = u8g2->width - UIConstants::FRAME_BOX_SIZE - UIConstants::SELECTION_MARGIN;
|
||||
const int frameY = y - UIConstants::FRAME_OFFSET;
|
||||
u8g2_DrawFrame(u8g2, frameX, frameY, UIConstants::FRAME_BOX_SIZE, UIConstants::FRAME_BOX_SIZE);
|
||||
|
||||
// Draw checkmark (X) if toggle is true
|
||||
if (item->getValue() == std::to_string(true))
|
||||
{
|
||||
const int checkX1 = frameX + 2;
|
||||
const int checkY1 = frameY + 2;
|
||||
const int checkX2 = frameX + UIConstants::FRAME_BOX_SIZE - 3;
|
||||
const int checkY2 = frameY + UIConstants::FRAME_BOX_SIZE - 3;
|
||||
|
||||
// Draw X pattern for checked state
|
||||
u8g2_DrawLine(u8g2, checkX1, checkY1, checkX2, checkY2);
|
||||
u8g2_DrawLine(u8g2, checkX1, checkY2, checkX2, checkY1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// No additional rendering needed for text and number types
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::onButtonClicked(const ButtonType button)
|
||||
{
|
||||
// Map button input to navigation functions
|
||||
switch (button)
|
||||
{
|
||||
case ButtonType::UP:
|
||||
onPressedUp();
|
||||
break;
|
||||
|
||||
case ButtonType::DOWN:
|
||||
onPressedDown();
|
||||
break;
|
||||
|
||||
case ButtonType::LEFT:
|
||||
onPressedLeft();
|
||||
break;
|
||||
|
||||
case ButtonType::RIGHT:
|
||||
onPressedRight();
|
||||
break;
|
||||
|
||||
case ButtonType::SELECT:
|
||||
onPressedSelect();
|
||||
break;
|
||||
|
||||
case ButtonType::BACK:
|
||||
onPressedBack();
|
||||
break;
|
||||
|
||||
default:
|
||||
// Ignore unknown button inputs
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::onPressedDown()
|
||||
{
|
||||
if (m_items.empty())
|
||||
return;
|
||||
|
||||
// Wrap around to first item when at the end
|
||||
m_selected_item = (m_selected_item + 1) % m_items.size();
|
||||
}
|
||||
|
||||
void Menu::onPressedUp()
|
||||
{
|
||||
if (m_items.empty())
|
||||
return;
|
||||
|
||||
// Wrap around to last item when at the beginning
|
||||
m_selected_item = (m_selected_item == 0) ? m_items.size() - 1 : m_selected_item - 1;
|
||||
}
|
||||
|
||||
void Menu::onPressedLeft() const
|
||||
{
|
||||
if (m_selected_item < m_items.size())
|
||||
{
|
||||
const auto &item = m_items.at(m_selected_item);
|
||||
item.onButtonPressed(ButtonType::LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::onPressedRight() const
|
||||
{
|
||||
if (m_selected_item < m_items.size())
|
||||
{
|
||||
const auto &item = m_items.at(m_selected_item);
|
||||
item.onButtonPressed(ButtonType::RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::onPressedSelect() const
|
||||
{
|
||||
if (m_selected_item < m_items.size())
|
||||
{
|
||||
const auto &item = m_items.at(m_selected_item);
|
||||
item.onButtonPressed(ButtonType::SELECT);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::onPressedBack() const
|
||||
{
|
||||
// Navigate back to previous screen if callback is available
|
||||
if (m_options && m_options->popScreen)
|
||||
{
|
||||
m_options->popScreen();
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::addText(uint8_t id, const std::string &text)
|
||||
{
|
||||
addTextCounter(id, text, 0);
|
||||
}
|
||||
|
||||
void Menu::addTextCounter(uint8_t id, const std::string &text, const uint8_t value)
|
||||
{
|
||||
auto callback = [this](const MenuItem &menuItem, const ButtonType button) -> void {
|
||||
onButtonPressed(menuItem, button);
|
||||
};
|
||||
if (value > 0)
|
||||
{
|
||||
m_items.emplace_back(id, MenuItemTypes::TEXT_COUNTER, text, std::to_string(value), callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_items.emplace_back(id, MenuItemTypes::TEXT, text, callback);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::addSelection(uint8_t id, const std::string &text, const std::vector<std::string> &values, const int index)
|
||||
{
|
||||
auto callback = [this](const MenuItem &menuItem, const ButtonType button) -> void {
|
||||
onButtonPressed(menuItem, button);
|
||||
};
|
||||
m_items.emplace_back(id, MenuItemTypes::SELECTION, text, values, index, callback);
|
||||
}
|
||||
|
||||
void Menu::addToggle(uint8_t id, const std::string &text, const bool selected)
|
||||
{
|
||||
auto callback = [this](const MenuItem &menuItem, const ButtonType button) -> void {
|
||||
onButtonPressed(menuItem, button);
|
||||
};
|
||||
m_items.emplace_back(id, MenuItemTypes::TOGGLE, text, std::to_string(selected), callback);
|
||||
}
|
||||
|
||||
void Menu::drawScrollBar() const
|
||||
{
|
||||
// Create scrollbar instance
|
||||
ScrollBar scrollBar(m_options, u8g2->width - UIConstants::SCROLLBAR_WIDTH, 3, 1, u8g2->height - 6);
|
||||
scrollBar.refresh(m_selected_item, m_items.size());
|
||||
scrollBar.render();
|
||||
}
|
||||
|
||||
void Menu::drawSelectionBox() const
|
||||
{
|
||||
// Calculate dimensions for the selection box
|
||||
const auto displayHeight = u8g2->height;
|
||||
const auto displayWidth = u8g2->width;
|
||||
const auto boxHeight = displayHeight / 3;
|
||||
const auto y = boxHeight * 2 - 2;
|
||||
const auto x = displayWidth - UIConstants::RIGHT_PADDING;
|
||||
|
||||
// Draw the rounded frame for the selection box
|
||||
u8g2_DrawRFrame(u8g2, 2, boxHeight, displayWidth - UIConstants::RIGHT_PADDING, boxHeight,
|
||||
UIConstants::CORNER_RADIUS);
|
||||
|
||||
// Draw horizontal line separator
|
||||
u8g2_DrawLine(u8g2, 4, y, displayWidth - UIConstants::RIGHT_PADDING, y);
|
||||
|
||||
// Draw vertical line on the right side
|
||||
u8g2_DrawLine(u8g2, x, y - boxHeight + 3, x, y - 1);
|
||||
}
|
41
firmware/components/insa/src/common/ScrollBar.cpp
Normal file
41
firmware/components/insa/src/common/ScrollBar.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "common/ScrollBar.h"
|
||||
|
||||
ScrollBar::ScrollBar(const menu_options_t *options, const size_t x, const size_t y, const size_t width,
|
||||
const size_t height)
|
||||
: Widget(options->u8g2), m_x(x), m_y(y), m_width(width), m_height(height), m_value(0), m_max(0), m_min(0),
|
||||
m_thumbHeight(0), m_thumbY(0)
|
||||
{
|
||||
}
|
||||
|
||||
void ScrollBar::render()
|
||||
{
|
||||
if (m_max <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t y = m_y; y < m_y + m_height; y += 2)
|
||||
{
|
||||
u8g2_DrawPixel(u8g2, m_x, y);
|
||||
}
|
||||
|
||||
u8g2_DrawBox(u8g2, u8g2->width - 4, m_thumbY, 3, m_thumbHeight);
|
||||
}
|
||||
|
||||
void ScrollBar::refresh(const size_t value, const size_t max, const size_t min)
|
||||
{
|
||||
m_value = value;
|
||||
m_max = max;
|
||||
m_min = min;
|
||||
|
||||
if (m_max <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_thumbHeight = std::max(m_height / 4, static_cast<size_t>(3));
|
||||
|
||||
const size_t trackLength = m_height - m_thumbHeight;
|
||||
|
||||
m_thumbY = m_y + (m_value * trackLength) / (m_max - 1);
|
||||
}
|
33
firmware/components/insa/src/common/Widget.cpp
Normal file
33
firmware/components/insa/src/common/Widget.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "common/Widget.h"
|
||||
|
||||
Widget::Widget(u8g2_t *u8g2) : u8g2(u8g2)
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::enter()
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::pause()
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::resume()
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::exit()
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::update(uint64_t dt)
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::render()
|
||||
{
|
||||
}
|
||||
|
||||
void Widget::onButtonClicked(ButtonType button)
|
||||
{
|
||||
}
|
99
firmware/components/insa/src/data/MenuItem.cpp
Normal file
99
firmware/components/insa/src/data/MenuItem.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "data/MenuItem.h"
|
||||
|
||||
// Constructor for basic menu items (text buttons)
|
||||
MenuItem::MenuItem(const uint8_t id, const uint8_t type, std::string text, ButtonCallback callback)
|
||||
: m_id(id), m_type(type), m_text(std::move(text)), m_callback(std::move(callback))
|
||||
{
|
||||
}
|
||||
|
||||
// Constructor for menu items with a single value (toggles)
|
||||
MenuItem::MenuItem(const uint8_t id, const uint8_t type, std::string text, std::string value, ButtonCallback callback)
|
||||
: m_id(id), m_type(type), m_text(std::move(text)), m_value(std::move(value)), m_callback(std::move(callback))
|
||||
{
|
||||
}
|
||||
|
||||
// Constructor for menu items with multiple values (selections)
|
||||
MenuItem::MenuItem(const uint8_t id, const uint8_t type, std::string text, std::vector<std::string> values, int index,
|
||||
ButtonCallback callback)
|
||||
: m_id(id), m_type(type), m_text(std::move(text)), m_values(std::move(values)), m_index(index),
|
||||
m_callback(std::move(callback))
|
||||
{
|
||||
}
|
||||
|
||||
uint8_t MenuItem::getId() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
uint8_t MenuItem::getType() const
|
||||
{
|
||||
return m_type;
|
||||
}
|
||||
|
||||
const std::string &MenuItem::getText() const
|
||||
{
|
||||
return m_text;
|
||||
}
|
||||
|
||||
const std::string &MenuItem::getValue() const
|
||||
{
|
||||
// Return the selected value from values array if available and index is valid
|
||||
if (!m_values.empty() && m_index >= 0 && m_index < m_values.size())
|
||||
{
|
||||
return m_values.at(m_index);
|
||||
}
|
||||
// Otherwise return the direct value
|
||||
return m_value;
|
||||
}
|
||||
|
||||
void MenuItem::setValue(const std::string &value)
|
||||
{
|
||||
m_value = value;
|
||||
}
|
||||
|
||||
void MenuItem::onButtonPressed(const ButtonType button) const
|
||||
{
|
||||
// Execute the callback function if one is registered
|
||||
if (m_callback)
|
||||
{
|
||||
m_callback(*this, button);
|
||||
}
|
||||
}
|
||||
|
||||
bool MenuItem::hasCallback() const
|
||||
{
|
||||
return (m_callback != nullptr);
|
||||
}
|
||||
|
||||
int MenuItem::getIndex() const
|
||||
{
|
||||
return m_index;
|
||||
}
|
||||
|
||||
std::vector<std::string> MenuItem::getValues() const
|
||||
{
|
||||
return m_values;
|
||||
}
|
||||
|
||||
size_t MenuItem::getItemCount() const
|
||||
{
|
||||
return m_values.size();
|
||||
}
|
||||
|
||||
MenuItem MenuItem::copyWith(const std::string &value) const
|
||||
{
|
||||
// Create a copy of this menu item with a new value
|
||||
MenuItem copy = *this;
|
||||
copy.m_value = value;
|
||||
return copy;
|
||||
}
|
||||
|
||||
MenuItem MenuItem::copyWith(const size_t index) const
|
||||
{
|
||||
// Create a copy of this menu item with a new selected index
|
||||
MenuItem copy = *this;
|
||||
|
||||
// Check for potential overflow when converting size_t to int
|
||||
copy.m_index = static_cast<int>(index);
|
||||
return copy;
|
||||
}
|
114
firmware/components/insa/src/ui/LightMenu.cpp
Normal file
114
firmware/components/insa/src/ui/LightMenu.cpp
Normal file
@@ -0,0 +1,114 @@
|
||||
#include "ui/LightMenu.h"
|
||||
|
||||
#include "led_manager.h"
|
||||
#include "ui/LightSettingsMenu.h"
|
||||
|
||||
/**
|
||||
* @namespace LightMenuItem
|
||||
* @brief Constants for light menu item identifiers
|
||||
*/
|
||||
namespace LightMenuItem
|
||||
{
|
||||
constexpr uint8_t ACTIVATE = 0; ///< ID for the light activation toggle
|
||||
constexpr uint8_t MODE = 1; ///< ID for the light mode selection
|
||||
constexpr uint8_t LED_SETTINGS = 2; ///< ID for the LED settings menu item
|
||||
} // namespace LightMenuItem
|
||||
|
||||
namespace LightMenuOptions
|
||||
{
|
||||
constexpr std::string LIGHT_ACTIVE = "light_active";
|
||||
constexpr std::string LIGHT_MODE = "light_mode";
|
||||
} // namespace LightMenuOptions
|
||||
|
||||
LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options)
|
||||
{
|
||||
// Add toggle for enabling/disabling the light system
|
||||
bool active = false;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
active = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_ACTIVE, active);
|
||||
}
|
||||
addToggle(LightMenuItem::ACTIVATE, "Einschalten", active);
|
||||
|
||||
// Create mode selection options (Day/Night modes)
|
||||
std::vector<std::string> values;
|
||||
values.emplace_back("Tag"); // Day mode
|
||||
values.emplace_back("Nacht"); // Night mode
|
||||
int mode_value = 0;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
mode_value = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_MODE, mode_value);
|
||||
}
|
||||
addSelection(LightMenuItem::MODE, "Modus", values, mode_value);
|
||||
|
||||
// Add menu item for accessing LED settings submenu
|
||||
addText(LightMenuItem::LED_SETTINGS, "Einstellungen");
|
||||
}
|
||||
|
||||
void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType button)
|
||||
{
|
||||
std::shared_ptr<Widget> widget;
|
||||
|
||||
// Handle different menu items based on their ID
|
||||
switch (menuItem.getId())
|
||||
{
|
||||
case LightMenuItem::ACTIVATE: {
|
||||
// Toggle the light activation state when SELECT is pressed
|
||||
if (button == ButtonType::SELECT)
|
||||
{
|
||||
toggle(menuItem);
|
||||
if (getItem(menuItem.getId()).getValue() == "1")
|
||||
{
|
||||
led_event_data_t payload = {.value = 42};
|
||||
send_event(EVENT_LED_ON, &payload);
|
||||
}
|
||||
else
|
||||
{
|
||||
led_event_data_t payload = {.value = 0};
|
||||
send_event(EVENT_LED_OFF, &payload);
|
||||
}
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
const auto value = getItem(menuItem.getId()).getValue() == "1";
|
||||
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_ACTIVE, value);
|
||||
m_options->persistenceManager->Save();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case LightMenuItem::MODE: {
|
||||
// Switch between day/night modes using left/right buttons
|
||||
const auto item = switchValue(menuItem, button);
|
||||
if (button == ButtonType::LEFT || button == ButtonType::RIGHT)
|
||||
{
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
const auto value = getItem(item.getId()).getIndex();
|
||||
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_MODE, value);
|
||||
m_options->persistenceManager->Save();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case LightMenuItem::LED_SETTINGS: {
|
||||
// Open the LED settings submenu when SELECT is pressed
|
||||
if (button == ButtonType::SELECT)
|
||||
{
|
||||
widget = std::make_shared<LightSettingsMenu>(m_options);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Handle unknown menu items (no action required)
|
||||
break;
|
||||
}
|
||||
|
||||
// Push the new widget to the screen stack if one was created
|
||||
if (m_options && m_options->pushScreen)
|
||||
{
|
||||
m_options->pushScreen(widget);
|
||||
}
|
||||
}
|
57
firmware/components/insa/src/ui/LightSettingsMenu.cpp
Normal file
57
firmware/components/insa/src/ui/LightSettingsMenu.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "ui/LightSettingsMenu.h"
|
||||
|
||||
/**
|
||||
* @namespace LightSettingsMenuItem
|
||||
* @brief Constants for light settings menu item identifiers
|
||||
*/
|
||||
namespace LightSettingsMenuItem
|
||||
{
|
||||
constexpr uint8_t SECTION_COUNTER = 0; ///< ID for the section counter menu item
|
||||
}
|
||||
|
||||
std::string LightSettingsMenu::CreateKey(const int index)
|
||||
{
|
||||
constexpr int key_length = 20;
|
||||
char key[key_length] = "";
|
||||
snprintf(key, key_length, "section_%d", index);
|
||||
return key;
|
||||
}
|
||||
|
||||
LightSettingsMenu::LightSettingsMenu(menu_options_t *options) : Menu(options), m_options(options)
|
||||
{
|
||||
// Create values vector for section counts (1-99)
|
||||
std::vector<std::string> values;
|
||||
for (size_t i = 1; i <= 99; i++)
|
||||
{
|
||||
values.emplace_back(std::to_string(i));
|
||||
}
|
||||
|
||||
// Add section counter selection (allows choosing number of sections)
|
||||
auto value = 7;
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
value = m_options->persistenceManager->GetValue(CreateKey(0), value);
|
||||
}
|
||||
addSelection(LightSettingsMenuItem::SECTION_COUNTER, "Sektionen", values, value);
|
||||
|
||||
setItemSize(std::stoull(getItem(0).getValue()));
|
||||
}
|
||||
|
||||
void LightSettingsMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType button)
|
||||
{
|
||||
// Handle value switching for the current menu item
|
||||
switchValue(menuItem, button);
|
||||
|
||||
// Update the section list size based on the section counter value
|
||||
if (menuItem.getId() == 0)
|
||||
{
|
||||
setItemSize(std::stoull(getItem(0).getValue()));
|
||||
}
|
||||
|
||||
// Persist the changed section values if persistence is available
|
||||
if (m_options && m_options->persistenceManager)
|
||||
{
|
||||
const auto value = getItem(menuItem.getId()).getIndex();
|
||||
m_options->persistenceManager->SetValue(CreateKey(menuItem.getId()), value);
|
||||
}
|
||||
}
|
44
firmware/components/insa/src/ui/MainMenu.cpp
Normal file
44
firmware/components/insa/src/ui/MainMenu.cpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#include "ui/MainMenu.h"
|
||||
|
||||
#include "common/Widget.h"
|
||||
#include "ui/LightMenu.h"
|
||||
#include "ui/SettingsMenu.h"
|
||||
|
||||
namespace MainMenuItem
|
||||
{
|
||||
constexpr uint8_t LIGHT = 0;
|
||||
constexpr uint8_t EXTERNAL_DEVICES = 1;
|
||||
constexpr uint8_t SETTINGS = 2;
|
||||
} // namespace MainMenuItem
|
||||
|
||||
MainMenu::MainMenu(menu_options_t *options) : Menu(options), m_options(options)
|
||||
{
|
||||
addText(MainMenuItem::LIGHT, "Lichtsteuerung");
|
||||
addTextCounter(MainMenuItem::EXTERNAL_DEVICES, "ext. Geraete", 0);
|
||||
addText(MainMenuItem::SETTINGS, "Einstellungen");
|
||||
}
|
||||
|
||||
void MainMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType button)
|
||||
{
|
||||
if (button == ButtonType::SELECT)
|
||||
{
|
||||
std::shared_ptr<Widget> widget;
|
||||
switch (menuItem.getId())
|
||||
{
|
||||
case MainMenuItem::LIGHT:
|
||||
widget = std::make_shared<LightMenu>(m_options);
|
||||
break;
|
||||
|
||||
case MainMenuItem::SETTINGS:
|
||||
widget = std::make_shared<SettingsMenu>(m_options);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (m_options && m_options->pushScreen)
|
||||
{
|
||||
m_options->pushScreen(widget);
|
||||
}
|
||||
}
|
||||
}
|
329
firmware/components/insa/src/ui/ScreenSaver.cpp
Normal file
329
firmware/components/insa/src/ui/ScreenSaver.cpp
Normal file
@@ -0,0 +1,329 @@
|
||||
#include "ui/ScreenSaver.h"
|
||||
#include "data/roads.h"
|
||||
#include "data/vehicles.h"
|
||||
#include <cstdlib>
|
||||
|
||||
ScreenSaver::ScreenSaver(menu_options_t *options)
|
||||
: Widget(options->u8g2), m_options(options), m_animationCounter(0), m_lastSpawnTime(0), m_leftVehicleCount(0),
|
||||
m_rightVehicleCount(0)
|
||||
{
|
||||
initVehicles();
|
||||
}
|
||||
|
||||
void ScreenSaver::initVehicles()
|
||||
{
|
||||
m_vehicles.resize(MAX_VEHICLES);
|
||||
|
||||
for (auto &vehicle : m_vehicles)
|
||||
{
|
||||
vehicle.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenSaver::update(const uint64_t dt)
|
||||
{
|
||||
m_animationCounter += dt;
|
||||
m_lastSpawnTime += dt;
|
||||
m_sceneShiftTimer += dt;
|
||||
|
||||
// Shift entire scene every 30 seconds
|
||||
if (m_sceneShiftTimer > 30000)
|
||||
{
|
||||
m_sceneOffsetX = (random() % 7) - 3; // -3 to +3 pixels
|
||||
m_sceneOffsetY = (random() % 7) - 3; // -3 to +3 pixels
|
||||
m_sceneShiftTimer = 0;
|
||||
}
|
||||
|
||||
// Try to spawn a new vehicle every few seconds
|
||||
if (m_lastSpawnTime > VEHICLE_SPAWN_DELAY)
|
||||
{
|
||||
trySpawnVehicle();
|
||||
m_lastSpawnTime = 0;
|
||||
}
|
||||
|
||||
// Update vehicle positions
|
||||
if (m_animationCounter > 16) // ~60 FPS
|
||||
{
|
||||
m_animationCounter = 0;
|
||||
|
||||
for (auto &vehicle : m_vehicles)
|
||||
{
|
||||
if (!vehicle.active)
|
||||
continue;
|
||||
|
||||
// Move vehicle
|
||||
if (vehicle.direction == Direction::LEFT)
|
||||
{
|
||||
vehicle.x -= static_cast<int>(vehicle.speed);
|
||||
|
||||
// Remove the vehicle if it goes off-screen
|
||||
if (vehicle.x <= -32) // Allow for largest vehicle width
|
||||
{
|
||||
vehicle.active = false;
|
||||
m_leftVehicleCount--;
|
||||
}
|
||||
}
|
||||
else // Direction::RIGHT
|
||||
{
|
||||
vehicle.x += static_cast<int>(vehicle.speed);
|
||||
|
||||
// Remove the vehicle if it goes off-screen
|
||||
if (vehicle.x >= (u8g2->width + 32)) // Allow for largest vehicle width
|
||||
{
|
||||
vehicle.active = false;
|
||||
m_rightVehicleCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenSaver::canSpawnInDirection(Direction direction) const
|
||||
{
|
||||
// Minimalen Abstand zwischen 48 und 64 Pixel zufällig wählen
|
||||
int requiredDistance =
|
||||
MIN_SAME_DIRECTION_DISTANCE + (random() % (MAX_SAME_DIRECTION_DISTANCE - MIN_SAME_DIRECTION_DISTANCE + 1));
|
||||
|
||||
for (const auto &vehicle : m_vehicles)
|
||||
{
|
||||
if (!vehicle.active || vehicle.direction != direction)
|
||||
continue;
|
||||
|
||||
// Abstand zum nächsten Fahrzeug in gleicher Richtung prüfen
|
||||
if (direction == Direction::LEFT)
|
||||
{
|
||||
// Fahrzeuge fahren von rechts nach links
|
||||
// Neues Fahrzeug würde bei u8g2->width + 16 starten
|
||||
int newVehicleX = u8g2->width + 16;
|
||||
|
||||
// Prüfen ob genug Abstand zum existierenden Fahrzeug
|
||||
if (newVehicleX - vehicle.x < requiredDistance)
|
||||
return false;
|
||||
}
|
||||
else // Direction::RIGHT
|
||||
{
|
||||
// Fahrzeuge fahren von links nach rechts
|
||||
// Neues Fahrzeug würde bei -32 starten
|
||||
int newVehicleX = -32;
|
||||
|
||||
// Prüfen ob genug Abstand zum existierenden Fahrzeug
|
||||
if (vehicle.x - newVehicleX < requiredDistance)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenSaver::trySpawnVehicle()
|
||||
{
|
||||
// Check if we can spawn a new vehicle
|
||||
int activeVehicles = 0;
|
||||
int availableSlot = -1;
|
||||
|
||||
for (int i = 0; i < MAX_VEHICLES; i++)
|
||||
{
|
||||
if (m_vehicles[i].active)
|
||||
{
|
||||
activeVehicles++;
|
||||
}
|
||||
else if (availableSlot == -1)
|
||||
{
|
||||
availableSlot = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't spawn if we're at max capacity or no slot available
|
||||
if (activeVehicles >= MAX_VEHICLES || availableSlot == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Direction direction = getRandomDirection();
|
||||
|
||||
// Check direction constraints
|
||||
if ((direction == Direction::LEFT && m_leftVehicleCount >= MAX_LEFT_VEHICLES) ||
|
||||
(direction == Direction::RIGHT && m_rightVehicleCount >= MAX_RIGHT_VEHICLES))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canSpawnInDirection(direction))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new vehicle
|
||||
Vehicle &newVehicle = m_vehicles[availableSlot];
|
||||
newVehicle.type = getRandomVehicleType();
|
||||
newVehicle.direction = direction;
|
||||
newVehicle.speed = MIN_SPEED + (static_cast<float>(random()) / RAND_MAX) * (MAX_SPEED - MIN_SPEED);
|
||||
|
||||
// Set Y position based on a direction (simulate opposing traffic lanes)
|
||||
const int halfHeight = u8g2->height / 2;
|
||||
if (direction == Direction::RIGHT)
|
||||
{
|
||||
// Vehicles going LEFT use bottom half of screen
|
||||
newVehicle.y = halfHeight + 8 + (random() % (halfHeight - 24));
|
||||
m_rightVehicleCount++;
|
||||
}
|
||||
else // Direction::RIGHT
|
||||
{
|
||||
// Vehicles going RIGHT use top half of screen
|
||||
newVehicle.y = 8 + (random() % (halfHeight - 24));
|
||||
m_leftVehicleCount++;
|
||||
}
|
||||
|
||||
// Set the starting X position based on the direction
|
||||
if (direction == Direction::LEFT)
|
||||
{
|
||||
// Vehicles going LEFT (from right to left) start from RIGHT side of screen
|
||||
newVehicle.x = u8g2->width + 16;
|
||||
}
|
||||
else // Direction::RIGHT
|
||||
{
|
||||
// Vehicles going RIGHT (from left to right) start from LEFT side of screen
|
||||
newVehicle.x = -32; // Account for the largest vehicle width
|
||||
}
|
||||
|
||||
newVehicle.active = true;
|
||||
}
|
||||
|
||||
ScreenSaver::VehicleType ScreenSaver::getRandomVehicleType()
|
||||
{
|
||||
switch (random() % 5)
|
||||
{
|
||||
case 0:
|
||||
return VehicleType::CAR;
|
||||
case 1:
|
||||
return VehicleType::CONVERTABLE;
|
||||
case 2:
|
||||
return VehicleType::SUV;
|
||||
case 3:
|
||||
return VehicleType::LORRY;
|
||||
case 4:
|
||||
return VehicleType::TRUCK;
|
||||
default:
|
||||
return VehicleType::CAR;
|
||||
}
|
||||
}
|
||||
|
||||
ScreenSaver::Direction ScreenSaver::getRandomDirection()
|
||||
{
|
||||
// Simple 50/50 chance for each direction
|
||||
return (random() % 2 == 0) ? Direction::LEFT : Direction::RIGHT;
|
||||
}
|
||||
|
||||
void ScreenSaver::render()
|
||||
{
|
||||
// Clear screen with a black background
|
||||
u8g2_SetDrawColor(u8g2, 0);
|
||||
u8g2_DrawBox(u8g2, 0, 0, u8g2->width, u8g2->height);
|
||||
u8g2_SetDrawColor(u8g2, 1);
|
||||
|
||||
// Calculate offsets
|
||||
const int roadOffset = (m_animationCounter / 100) % road_horizontal_width;
|
||||
|
||||
// Draw all active vehicles with a scene offset
|
||||
for (const auto &vehicle : m_vehicles)
|
||||
{
|
||||
if (vehicle.active)
|
||||
{
|
||||
Vehicle offsetVehicle = vehicle;
|
||||
offsetVehicle.x += m_sceneOffsetX;
|
||||
offsetVehicle.y += m_sceneOffsetY;
|
||||
drawVehicle(offsetVehicle);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw road with offsets
|
||||
const int y = u8g2->height / 2 - road_horizontal_height / 2 + m_sceneOffsetY;
|
||||
for (int x = -road_horizontal_width + roadOffset + m_sceneOffsetX; x <= u8g2->width; x += road_horizontal_width)
|
||||
{
|
||||
drawTransparentBitmap(x, y, road_horizontal_width, road_horizontal_height, road_horizontal_bits);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenSaver::drawVehicle(const Vehicle &vehicle) const
|
||||
{
|
||||
int width, height;
|
||||
|
||||
if (const unsigned char *bitmap = getVehicleBitmap(vehicle.type, vehicle.direction, width, height))
|
||||
{
|
||||
drawTransparentBitmap(vehicle.x, vehicle.y, width, height, bitmap);
|
||||
// u8g2_DrawXBM(u8g2, vehicle.x, vehicle.y, width, height, bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenSaver::drawTransparentBitmap(const int x, const int y, const int width, const int height,
|
||||
const unsigned char *bitmap) const
|
||||
{
|
||||
for (int py = 0; py < height; py++)
|
||||
{
|
||||
for (int px = 0; px < width; px++)
|
||||
{
|
||||
// Calculate byte and a bit of position in bitmap
|
||||
const int byteIndex = (py * ((width + 7) / 8)) + (px / 8);
|
||||
|
||||
// Check if the pixel is set (white)
|
||||
if (const int bitIndex = px % 8; bitmap[byteIndex] & (1 << bitIndex))
|
||||
{
|
||||
// Only draw white pixels, skip black (transparent) pixels
|
||||
const int screenX = x + px;
|
||||
|
||||
// Bounds checking
|
||||
if (const int screenY = y + py;
|
||||
screenX >= 0 && screenX < u8g2->width && screenY >= 0 && screenY < u8g2->height)
|
||||
{
|
||||
u8g2_DrawPixel(u8g2, screenX, screenY);
|
||||
}
|
||||
}
|
||||
// Black pixels are simply not drawn (transparent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unsigned char *ScreenSaver::getVehicleBitmap(const VehicleType type, const Direction direction, int &width,
|
||||
int &height)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case VehicleType::CAR:
|
||||
width = car_width;
|
||||
height = car_height;
|
||||
return (direction == Direction::LEFT) ? car_left_bits : car_right_bits;
|
||||
|
||||
case VehicleType::CONVERTABLE:
|
||||
width = convertable_width;
|
||||
height = convertable_height;
|
||||
return (direction == Direction::LEFT) ? convertable_left_bits : convertable_right_bits;
|
||||
|
||||
case VehicleType::SUV:
|
||||
width = suv_width;
|
||||
height = suv_height;
|
||||
return (direction == Direction::LEFT) ? suv_left_bits : suv_right_bits;
|
||||
|
||||
case VehicleType::LORRY:
|
||||
width = lorry_width;
|
||||
height = lorry_height;
|
||||
return (direction == Direction::LEFT) ? lorry_left_bits : lorry_right_bits;
|
||||
|
||||
case VehicleType::TRUCK:
|
||||
width = truck_width;
|
||||
height = truck_height;
|
||||
return (direction == Direction::LEFT) ? truck_left_bits : truck_right_bits;
|
||||
|
||||
default:
|
||||
width = car_width;
|
||||
height = car_height;
|
||||
return car_left_bits;
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenSaver::onButtonClicked(ButtonType button)
|
||||
{
|
||||
if (m_options && m_options->popScreen)
|
||||
{
|
||||
m_options->popScreen();
|
||||
}
|
||||
}
|
11
firmware/components/insa/src/ui/SettingsMenu.cpp
Normal file
11
firmware/components/insa/src/ui/SettingsMenu.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "ui/SettingsMenu.h"
|
||||
|
||||
namespace SettingsMenuItem
|
||||
{
|
||||
constexpr uint8_t OTA_UPLOAD = 0;
|
||||
}
|
||||
|
||||
SettingsMenu::SettingsMenu(menu_options_t *options) : Menu(options)
|
||||
{
|
||||
addText(SettingsMenuItem::OTA_UPLOAD, "OTA Einspielen");
|
||||
}
|
39
firmware/components/insa/src/ui/SplashScreen.cpp
Normal file
39
firmware/components/insa/src/ui/SplashScreen.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "ui/SplashScreen.h"
|
||||
|
||||
#include "ui/MainMenu.h"
|
||||
|
||||
#ifndef ESP32
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#endif
|
||||
|
||||
uint64_t counter = 0;
|
||||
|
||||
SplashScreen::SplashScreen(menu_options_t *options) : Widget(options->u8g2), m_options(options)
|
||||
{
|
||||
}
|
||||
|
||||
void SplashScreen::update(const uint64_t dt)
|
||||
{
|
||||
counter += dt;
|
||||
if (counter >= 3000)
|
||||
{
|
||||
counter = 0;
|
||||
if (m_options && m_options->setScreen)
|
||||
{
|
||||
m_options->setScreen(std::make_shared<MainMenu>(m_options));
|
||||
}
|
||||
}
|
||||
#ifndef ESP32
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
#endif
|
||||
}
|
||||
|
||||
void SplashScreen::render()
|
||||
{
|
||||
u8g2_SetFont(u8g2, u8g2_font_DigitalDisco_tr);
|
||||
u8g2_DrawStr(u8g2, 28, u8g2->height / 2 - 10, "HO Anlage");
|
||||
u8g2_DrawStr(u8g2, 30, u8g2->height / 2 + 5, "Axel Janz");
|
||||
u8g2_SetFont(u8g2, u8g2_font_haxrcorp4089_tr);
|
||||
u8g2_DrawStr(u8g2, 35, 50, "Initialisierung...");
|
||||
}
|
Reference in New Issue
Block a user