Рефакторинг меню и добавление кучи всего:

- иерархическая и универсальная структура меню, которую относительно удобно расширять и добавлять
- заготовик меню для таймера и секундомера
- работающие игры (выбить 1.00 сек, тест реакции, кликер)
This commit is contained in:
2026-04-21 18:29:44 +03:00
parent 7b7ab6f11d
commit 71fc59d34d
9 changed files with 999 additions and 413 deletions

View File

@@ -1,5 +1,6 @@
#include "melody.h"
MelodyHandle melody;
#define FIXED_ARR 1000 // фиксированный период таймера
#define FIXED_VOLUME 2 // фиксированный период таймера

View File

@@ -3,7 +3,7 @@
#include "stm32f1xx_hal.h"
#include "songs.h"
//#include "sounds.h"
#include "sounds.h"
typedef struct {
TIM_HandleTypeDef* htim;
@@ -15,6 +15,7 @@ typedef struct {
uint8_t is_playing;
uint16_t bpm;
} MelodyHandle;
extern MelodyHandle melody;
void Melody_Init(MelodyHandle* mh, TIM_HandleTypeDef* htim, uint32_t channel, uint32_t timer_clock_hz);
void Melody_SetBPM(MelodyHandle* mh, uint16_t bpm);

View File

@@ -1,426 +1,195 @@
#include "menu.h"
#include "segment.h"
#include "clock_manager.h"
#include <string.h>
#include <stdio.h>
// Время удержания кнопки (мс), после которого нажатие считается "длинным"
// Используется для входа в меню из режима часов
#define LONG_PRESS_MS 500
#define LONG_PRESS_MS 500
// Задержка перед началом автоповтора (мс)
// При удержании кнопки первые 500 мс ничего не происходит
#define REPEAT_DELAY_MS 500
MenuContext g_ctx;
uint8_t g_sound_enabled = 1;
// Интервал между автоповторами (мс)
// После начала повтора каждые 100 мс кнопка срабатывает заново
#define REPEAT_INTERVAL_MS 100
extern bool ReadButton(Button_Type btn);
extern MenuNode* Menu_GetRootMenu(void);
// Интервал мигания редактируемого разряда (мс)
// Моргает каждые 500 мс (0.5 сек вкл / 0.5 сек выкл)
#define BLINK_INTERVAL_MS 500
// Задержка антидребезга (мс)
// Изменение состояния кнопки фиксируется только если оно стабильно в течение 30 мс
#define DEBOUNCE_MS 30
typedef enum {
MAIN_MENU_SET_TIME,
MAIN_MENU_SET_DUTY,
MAIN_MENU_RESET,
MAIN_MENU_COUNT
} MainMenuItem;
typedef struct {
// Состояние
SystemState state;
MainMenuItem selectedMenuItem;
void Menu_Init(MenuNode* startNode) {
g_ctx.currentNode = startNode;
g_ctx.needsRedraw = true;
g_ctx.lastTick = 0;
// Для SET_TIME
time_t originalTime;
time_t editTime;
uint8_t editStep;
bool blinkState;
uint32_t lastBlinkTime;
// Для SET_DUTY
uint8_t originalDuty;
uint8_t editDuty;
// Для кнопок
bool buttonPrevState[BUTTON_COUNT];
uint32_t buttonPressTime[BUTTON_COUNT];
bool longPressSent[BUTTON_COUNT];
bool repeatActive[BUTTON_COUNT];
uint32_t lastRepeatTime[BUTTON_COUNT];
uint32_t lastDebounceTime[BUTTON_COUNT];
bool buttonStableState[BUTTON_COUNT];
} MenuContext;
MenuContext menu;
// ==================== Отображение ====================
static void FormatTime(char* buf, const time_t* t) {
buf[0] = '0' + t->hour / 10;
buf[1] = '0' + t->hour % 10;
buf[2] = '0' + t->min / 10;
buf[3] = '0' + t->min % 10;
buf[4] = '0' + t->sec / 10;
buf[5] = '0' + t->sec % 10;
}
static void UpdateDisplay(void) {
switch (menu.state) {
case STATE_CLOCK: {
time_t now = ClockManager_GetTime(1);
char buf[7];
FormatTime(buf, &now);
Segment_SetString(buf);
break;
}
case STATE_MAIN_MENU: {
switch (menu.selectedMenuItem) {
case MAIN_MENU_SET_TIME:
Segment_SetString("SET T ");
break;
case MAIN_MENU_SET_DUTY:
Segment_SetString("SET D ");
break;
case MAIN_MENU_RESET:
Segment_SetString("RESET ");
break;
default:
break;
}
break;
}
case STATE_SET_TIME: {
char buf[7];
FormatTime(buf, &menu.editTime);
if (menu.blinkState && menu.editStep < 6) {
buf[menu.editStep] = ' ';
}
Segment_SetString(buf);
break;
}
case STATE_SET_DUTY: {
char buf[6] = {'D', 'U', 'T', 'Y', ' ' , ' '};
if (menu.editDuty == 10) {
buf[4] = '1';
buf[5] = '0';
} else {
buf[5] = '0' + menu.editDuty;
}
Segment_SetString(buf);
break;
}
case STATE_RESET_CONFIRM:
Segment_SetString("RESET ");
break;
if (startNode && startNode->onEnter) {
startNode->onEnter();
}
}
// ==================== Логика SET_TIME ====================
static void IncreaseTimeDigit(void) {
uint8_t tens, units;
switch (menu.editStep) {
case 0: // десятки часов (0-2)
tens = menu.editTime.hour / 10;
units = menu.editTime.hour % 10;
tens++;
if (tens > 2) tens = 0;
menu.editTime.hour = tens * 10 + units;
break;
case 1: // единицы часов (0-9, но с учетом десятков)
tens = menu.editTime.hour / 10;
units = menu.editTime.hour % 10;
units++;
if (tens == 2 && units > 3) units = 0; // 23 → 20
if (units > 9) units = 0;
menu.editTime.hour = tens * 10 + units;
break;
case 2: // десятки минут (0-5)
tens = menu.editTime.min / 10;
units = menu.editTime.min % 10;
tens = (tens + 1) % 6;
menu.editTime.min = tens * 10 + units;
break;
case 3: // единицы минут (0-9)
units = (menu.editTime.min % 10 + 1) % 10;
menu.editTime.min = (menu.editTime.min / 10) * 10 + units;
break;
case 4: // десятки секунд (0-5)
tens = menu.editTime.sec / 10;
units = menu.editTime.sec % 10;
tens = (tens + 1) % 6;
menu.editTime.sec = tens * 10 + units;
break;
case 5: // единицы секунд (0-9)
units = (menu.editTime.sec % 10 + 1) % 10;
menu.editTime.sec = (menu.editTime.sec / 10) * 10 + units;
break;
// НОВАЯ ФУНКЦИЯ - открыть меню
void Menu_OpenMenu(MenuNode* menu) {
if (!menu) return;
g_ctx.currentNode = menu;
g_ctx.needsRedraw = true;
if (menu->onEnter) {
menu->onEnter();
}
UpdateDisplay();
}
static void DecreaseTimeDigit(void) {
uint8_t tens, units;
switch (menu.editStep) {
case 0: // десятки часов (0-2)
tens = menu.editTime.hour / 10;
units = menu.editTime.hour % 10;
if (tens == 0) tens = 2;
else tens--;
menu.editTime.hour = tens * 10 + units;
break;
case 1: // единицы часов
tens = menu.editTime.hour / 10;
units = menu.editTime.hour % 10;
if (units == 0) {
units = (tens == 2) ? 3 : 9;
} else {
units--;
}
menu.editTime.hour = tens * 10 + units;
break;
case 2: // десятки минут (0-5)
tens = menu.editTime.min / 10;
units = menu.editTime.min % 10;
tens = (tens == 0) ? 5 : tens - 1;
menu.editTime.min = tens * 10 + units;
break;
case 3: // единицы минут
units = (menu.editTime.min % 10 == 0) ? 9 : (menu.editTime.min % 10) - 1;
menu.editTime.min = (menu.editTime.min / 10) * 10 + units;
break;
case 4: // десятки секунд (0-5)
tens = menu.editTime.sec / 10;
units = menu.editTime.sec % 10;
tens = (tens == 0) ? 5 : tens - 1;
menu.editTime.sec = tens * 10 + units;
break;
case 5: // единицы секунд
units = (menu.editTime.sec % 10 == 0) ? 9 : (menu.editTime.sec % 10) - 1;
menu.editTime.sec = (menu.editTime.sec / 10) * 10 + units;
break;
}
UpdateDisplay();
}
// ==================== Обработка кнопок ====================
static void ProcessButton(Button_Type btn, bool longPress) {
// Длинное нажатие SELECT в режиме часов - вход в меню
if (menu.state == STATE_CLOCK && longPress && btn == BUTTON_SELECT) {
menu.state = STATE_MAIN_MENU;
menu.selectedMenuItem = MAIN_MENU_SET_TIME;
UpdateDisplay();
void Menu_HandleButton(Button_Type btn, bool longPress) {
if (!g_ctx.currentNode) return;
// Длинное нажатие SELECT на корневом уровне (часы) - вход в меню
if (longPress && btn == BUTTON_SELECT && g_ctx.currentNode->parent == NULL) {
SOUND_DOUBLE;
MenuNode* rootMenu = Menu_GetRootMenu();
if (rootMenu) {
Menu_OpenMenu(rootMenu);
}
return;
}
switch (menu.state) {
case STATE_MAIN_MENU:
if (btn == BUTTON_UP) {
menu.selectedMenuItem++;
if (menu.selectedMenuItem >= MAIN_MENU_COUNT) {
menu.selectedMenuItem = 0;
}
UpdateDisplay();
}
else if (btn == BUTTON_DOWN) {
if (menu.selectedMenuItem == 0) {
menu.selectedMenuItem = MAIN_MENU_COUNT - 1;
} else {
menu.selectedMenuItem--;
}
UpdateDisplay();
}
else if (btn == BUTTON_SELECT && !longPress) {
switch (menu.selectedMenuItem) {
case MAIN_MENU_SET_TIME:
menu.state = STATE_SET_TIME;
menu.originalTime = ClockManager_GetTime(0);
menu.editTime = menu.originalTime;
menu.editStep = 0;
menu.blinkState = true;
menu.lastBlinkTime = HAL_GetTick();
break;
case MAIN_MENU_SET_DUTY:
menu.state = STATE_SET_DUTY;
menu.originalDuty = ClockManager_GetDuty();
menu.editDuty = menu.originalDuty;
break;
case MAIN_MENU_RESET:
menu.state = STATE_RESET_CONFIRM;
break;
default: break;
}
UpdateDisplay();
}
else if (btn == BUTTON_BACK) {
menu.state = STATE_CLOCK;
UpdateDisplay();
}
break;
case STATE_SET_TIME:
if (btn == BUTTON_UP) {
IncreaseTimeDigit();
}
else if (btn == BUTTON_DOWN) {
DecreaseTimeDigit();
}
else if (btn == BUTTON_SELECT) {
menu.editStep++;
if (menu.editStep >= 6) {
ClockManager_SetTime(menu.editTime.hour, menu.editTime.min, menu.editTime.sec);
menu.state = STATE_CLOCK;
}
UpdateDisplay();
}
else if (btn == BUTTON_BACK) {
menu.state = STATE_CLOCK;
UpdateDisplay();
}
break;
case STATE_SET_DUTY:
if (btn == BUTTON_UP && menu.editDuty < 10) {
menu.editDuty++;
Segment_SetBrightness(menu.editDuty * 10);
UpdateDisplay();
}
else if (btn == BUTTON_DOWN && menu.editDuty > 0) {
menu.editDuty--;
Segment_SetBrightness(menu.editDuty * 10);
UpdateDisplay();
}
else if (btn == BUTTON_SELECT) {
ClockManager_SetDuty(menu.editDuty);
menu.state = STATE_CLOCK;
UpdateDisplay();
}
else if (btn == BUTTON_BACK) {
menu.state = STATE_CLOCK;
Segment_SetBrightness(menu.originalDuty * 10);
UpdateDisplay();
}
break;
case STATE_RESET_CONFIRM:
if (btn == BUTTON_SELECT) {
ClockManager_ResetTime();
ClockManager_SetDuty(8);
menu.state = STATE_CLOCK;
UpdateDisplay();
}
else if (btn == BUTTON_BACK) {
menu.state = STATE_CLOCK;
UpdateDisplay();
}
break;
default:
break;
if (g_ctx.currentNode->onButton) {
if(btn == BUTTON_SELECT)
{
SOUND_DOUBLE;
}
else
{
SOUND_CLICK;
}
g_ctx.currentNode->onButton(btn, longPress);
return;
}
}
// ==================== Публичные функции ====================
void Menu_Init(void) {
menu.state = STATE_CLOCK;
menu.selectedMenuItem = MAIN_MENU_SET_TIME;
menu.editStep = 0;
menu.blinkState = false;
menu.lastBlinkTime = 0;
for (int i = 0; i < BUTTON_COUNT; i++) {
menu.buttonPrevState[i] = false;
menu.buttonPressTime[i] = 0;
menu.longPressSent[i] = false;
menu.repeatActive[i] = false;
menu.lastDebounceTime[i] = 0;
menu.buttonStableState[i] = false;
if (g_ctx.currentNode->parent != NULL) {
switch (btn) {
case BUTTON_UP:
SOUND_CLICK;
if (g_ctx.currentNode->selectedChild > 0) {
g_ctx.currentNode->selectedChild--;
g_ctx.needsRedraw = true;
}
break;
case BUTTON_DOWN:
SOUND_CLICK;
if (g_ctx.currentNode->selectedChild < g_ctx.currentNode->childCount - 1) {
g_ctx.currentNode->selectedChild++;
g_ctx.needsRedraw = true;
}
break;
case BUTTON_SELECT: {
SOUND_DOUBLE;
if (g_ctx.currentNode->children) {
MenuNode* selected = g_ctx.currentNode->children[g_ctx.currentNode->selectedChild];
if (selected) {
g_ctx.currentNode = selected;
g_ctx.needsRedraw = true;
if (selected->onEnter) {
selected->onEnter();
}
}
}
break;
}
case BUTTON_BACK:
SOUND_CLICK;
Menu_GoBack();
break;
default:
break;
}
}
}
void Menu_Process(void) {
uint32_t tick = HAL_GetTick();
if (menu.state == STATE_SET_TIME) {
if (tick - menu.lastBlinkTime >= BLINK_INTERVAL_MS) {
menu.lastBlinkTime = tick;
menu.blinkState = !menu.blinkState;
UpdateDisplay();
}
}
// ОБРАБОТКА КНОПОК С ДЕБАУНСОМ
for (int i = 0; i < BUTTON_COUNT; i++) {
bool rawPressed = ReadButton(i);
bool pressed;
// Антидребезг
if (rawPressed != menu.buttonStableState[i]) {
menu.lastDebounceTime[i] = tick;
menu.buttonStableState[i] = rawPressed;
}
if ((tick - menu.lastDebounceTime[i]) >= DEBOUNCE_MS) {
pressed = menu.buttonStableState[i];
} else {
pressed = menu.buttonPrevState[i]; // старое значение пока не стабилизировалось
}
bool wasPressed = menu.buttonPrevState[i];
bool reading = ReadButton((Button_Type)i);
if (pressed && !wasPressed) {
menu.buttonPressTime[i] = tick;
menu.longPressSent[i] = false;
menu.repeatActive[i] = false;
}
else if (pressed && wasPressed) {
if (!menu.longPressSent[i] && (tick - menu.buttonPressTime[i] >= LONG_PRESS_MS)) {
menu.longPressSent[i] = true;
ProcessButton((Button_Type)i, true);
}
else if (menu.longPressSent[i] && !menu.repeatActive[i] &&
(tick - menu.buttonPressTime[i] >= REPEAT_DELAY_MS)) {
menu.repeatActive[i] = true;
menu.lastRepeatTime[i] = tick;
ProcessButton((Button_Type)i, true);
}
else if (menu.repeatActive[i] && (tick - menu.lastRepeatTime[i] >= REPEAT_INTERVAL_MS)) {
menu.lastRepeatTime[i] = tick;
ProcessButton((Button_Type)i, true);
}
}
else if (!pressed && wasPressed) {
if (!menu.longPressSent[i]) {
ProcessButton((Button_Type)i, false);
}
menu.repeatActive[i] = false;
if (reading != g_ctx.lastButtonState[i]) {
g_ctx.lastDebounceTime[i] = tick;
}
menu.buttonPrevState[i] = pressed;
if ((tick - g_ctx.lastDebounceTime[i]) > 30) { // DEBOUNCE_MS = 30
if (reading != g_ctx.buttonState[i]) {
g_ctx.buttonState[i] = reading;
if (g_ctx.buttonState[i]) {
g_ctx.pressStartTime[i] = tick;
g_ctx.longPressTriggered[i] = false;
} else {
if (!g_ctx.longPressTriggered[i]) {
Menu_HandleButton((Button_Type)i, false);
}
}
}
}
if (g_ctx.buttonState[i] && !g_ctx.longPressTriggered[i]) {
if ((tick - g_ctx.pressStartTime[i]) >= LONG_PRESS_MS) {
g_ctx.longPressTriggered[i] = true;
Menu_HandleButton((Button_Type)i, true);
}
}
g_ctx.lastButtonState[i] = reading;
}
static uint32_t lastClockUpdate = 0;
if (menu.state == STATE_CLOCK && (tick - lastClockUpdate >= 200)) {
lastClockUpdate = tick;
UpdateDisplay();
if (g_ctx.currentNode && g_ctx.currentNode->onUpdate) {
g_ctx.currentNode->onUpdate();
}
if (tick - g_ctx.lastTick >= 200) {
g_ctx.lastTick = tick;
g_ctx.needsRedraw = true;
}
if ((g_ctx.needsRedraw && g_ctx.currentNode && g_ctx.currentNode->display) ||
(g_ctx.currentNode && g_ctx.currentNode->display && g_ctx.currentNode->needsRedraw)) {
g_ctx.currentNode->display();
g_ctx.needsRedraw = false;
}
}
SystemState Menu_GetState(void) {
return menu.state;
void Menu_GoBack(void) {
if (g_ctx.currentNode && g_ctx.currentNode->parent) {
g_ctx.currentNode = g_ctx.currentNode->parent;
g_ctx.needsRedraw = true;
if (g_ctx.currentNode->onEnter) {
g_ctx.currentNode->onEnter();
}
}
}
void Menu_Refresh(void) {
g_ctx.needsRedraw = true;
}
MenuNode* Menu_GetCurrentNode(void) {
return g_ctx.currentNode;
}
void Menu_Sound_On(void) {
g_sound_enabled = 1;
SOUND_SUCCESS;
}
void Menu_Sound_Off(void) {
g_sound_enabled = 0;
}
void Menu_Sound_Toggle(void) {
if (g_sound_enabled) {
Menu_Sound_Off();
} else {
Menu_Sound_On();
}
}
uint8_t Menu_Sound_IsEnabled(void) {
return g_sound_enabled;
}

View File

@@ -4,7 +4,15 @@
#include <stdint.h>
#include <stdbool.h>
#include "main.h"
#include "melody.h"
// Макросы звуковых эффектов
#define SFX_BPM 480
#define SOUND_CLICK if(g_sound_enabled) Melody_Play(&melody, &SFX_Click, SFX_BPM)
#define SOUND_DOUBLE if(g_sound_enabled) Melody_Play(&melody, &SFX_DoubleBeep, SFX_BPM)
#define SOUND_ERROR if(g_sound_enabled) Melody_Play(&melody, &SFX_Error, SFX_BPM)
#define SOUND_SUCCESS if(g_sound_enabled) Melody_Play(&melody, &SFX_Success, SFX_BPM)
typedef enum {
BUTTON_UP,
BUTTON_DOWN,
@@ -13,18 +21,50 @@ typedef enum {
BUTTON_COUNT
} Button_Type;
typedef enum {
STATE_CLOCK, // режим часов
STATE_MAIN_MENU, // главное меню (выбор пункта)
STATE_SET_TIME, // настройка времени
STATE_SET_DUTY, // настройка яркости
STATE_RESET_CONFIRM // подтверждение сброса
} SystemState;
typedef struct MenuNode {
const char* name;
struct MenuNode* parent;
struct MenuNode** children;
uint8_t childCount;
uint8_t selectedChild;
uint8_t needsRedraw;
void (*display)(void);
void (*onEnter)(void);
void (*onUpdate)(void);
void (*onButton)(Button_Type btn, bool longPress);
void* data;
} MenuNode;
// Чтение кнопки (реализуется в main.c или hal)
int ReadButton(int btn);
typedef struct {
MenuNode* currentNode;
uint32_t lastTick;
bool needsRedraw;
uint32_t lastDebounceTime[BUTTON_COUNT];
uint8_t lastButtonState[BUTTON_COUNT];
uint8_t buttonState[BUTTON_COUNT];
uint32_t pressStartTime[BUTTON_COUNT];
uint8_t longPressTriggered[BUTTON_COUNT];
} MenuContext;
void Menu_Init(void);
// Глобальные переменные
extern uint8_t g_sound_enabled;
extern MenuContext g_ctx;
// Функции
void Menu_Init(MenuNode* startNode);
void Menu_Process(void);
void Menu_HandleButton(Button_Type btn, bool longPress);
void Menu_GoBack(void);
void Menu_Refresh(void);
MenuNode* Menu_GetCurrentNode(void);
void Menu_OpenMenu(MenuNode* menu);
void Menu_Sound_On(void);
void Menu_Sound_Off(void);
void Menu_Sound_Toggle(void);
uint8_t Menu_Sound_IsEnabled(void);
#endif

695
Core/Clock/menu_items.c Normal file
View File

@@ -0,0 +1,695 @@
#include "menu_items.h"
#include "segment.h"
#include "clock_manager.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
static MenuNode g_rootMenu;
// Сначала объявляем узлы активностей
MenuNode g_clockNode;
MenuNode g_timerNode;
MenuNode g_stopwatchNode;
MenuNode g_ZeroMillisNode;
MenuNode g_ReactionTimeNode;
MenuNode g_ClickerTimeNode;
// Потом объявляем узлы меню
MenuNode g_gamesNode;
MenuNode g_settingsNode;
MenuNode g_timeEditNode;
MenuNode g_dutyEditNode;
MenuNode g_LEDEditNode;
MenuNode g_MenuSoundNode;
MenuNode g_PowerOnSongNode;
MenuNode g_SongNode;
MenuNode g_resetNode;
// ==================== Данные ====================
typedef struct {
time_t editTime;
uint8_t editStep;
bool blinkState;
uint32_t lastBlink;
} TimeEditData;
typedef struct {
uint32_t startTime;
uint32_t pressTime;
bool celebrating;
bool result;
int32_t diff_ms;
uint8_t state;
} Game1SecData;
typedef struct {
uint32_t waitStart;
uint32_t ledOnTime;
uint32_t reactionTime;
uint8_t state;
} GameReactionData;
typedef struct {
uint32_t endTime;
uint16_t clicks;
bool active;
bool finished;
} GameClickerData;
static TimeEditData g_timeData;
static uint8_t g_originalDuty;
static uint8_t g_editDuty;
static Game1SecData g_game1sec;
static GameReactionData g_gameReaction;
static GameClickerData g_gameClicker;
// Текущая активность в корне
static MenuNode* g_currentActivity = NULL;
// ==================== Функции отображения активностей ====================
static void Display_Clock(void) {
time_t now = ClockManager_GetTime(1);
char buf[7];
sprintf(buf, "%02d%02d%02d", now.hour, now.min, now.sec);
Segment_SetString(buf);
}
static void Display_Timer(void) {
Segment_SetString("TIMER ");
}
static void Display_Stopwatch(void) {
Segment_SetString("SECOND ");
}
static void Display_ZeroMillis(void) {
char buf[7];
switch (g_game1sec.state) {
case 0:
g_game1sec.celebrating = 0;
sprintf(buf, "START"); break;
case 1:
g_game1sec.diff_ms = (HAL_GetTick() - g_game1sec.startTime);
case 2:
sprintf(buf, " %4d", g_game1sec.diff_ms/10);
break;
default: sprintf(buf, "ERROR"); break;
}
if(g_game1sec.state == 2)
{
if((g_game1sec.diff_ms/10)%100 == 0)
{
if(!g_game1sec.celebrating)
{
g_game1sec.celebrating = 1;
Melody_Play(&melody, &Polyphia_PlayingGod, 134);
}
}
}
Segment_SetString(buf);
}
static void Display_Reaction(void) {
char buf[7];
switch (g_gameReaction.state) {
case 0: sprintf(buf, "START"); break;
case 1: sprintf(buf, " "); break;
case 2: sprintf(buf, "888888"); break;
case 3: sprintf(buf, "%6d", g_gameReaction.reactionTime); break;
default: sprintf(buf, "ERROR"); break;
}
Segment_SetString(buf);
}
static void Display_Clicker(void) {
char buf[7];
if (!g_gameClicker.active && !g_gameClicker.finished) {
sprintf(buf, "START");
} else if (g_gameClicker.active) {
uint32_t remaining = (g_gameClicker.endTime - HAL_GetTick()) / 1000;
sprintf(buf, "%2d %3d", remaining, g_gameClicker.clicks);
} else {
sprintf(buf, " %3d", g_gameClicker.clicks);
}
Segment_SetString(buf);
}
static void Display_MenuItem(void) {
MenuNode* current = Menu_GetCurrentNode();
if (current && current->selectedChild < current->childCount) {
const char* name = current->children[current->selectedChild]->name;
char buf[7];
sprintf(buf, "%-6.6s", name);
Segment_SetString(buf);
}
}
static void Display_TimeEdit(void) {
char buf[7];
sprintf(buf, "%02d%02d%02d",
g_timeData.editTime.hour,
g_timeData.editTime.min,
g_timeData.editTime.sec);
if (g_timeData.blinkState && g_timeData.editStep < 6) {
buf[g_timeData.editStep] = ' ';
}
Segment_SetString(buf);
}
static void Display_DutyEdit(void) {
char buf[7] = "DUTY ";
if (g_editDuty == 10) {
buf[4] = '1';
buf[5] = '0';
} else {
buf[5] = '0' + g_editDuty;
}
Segment_SetString(buf);
}
static void Display_Reset(void) {
Segment_SetString("RESET ");
}
// ==================== Вход в активности ====================
static void OnEnter_Clock(void) {
g_currentActivity = &g_clockNode;
g_rootMenu.parent = &g_clockNode;
}
static void OnEnter_Timer(void) {
g_currentActivity = &g_timerNode;
g_rootMenu.parent = &g_timerNode;
}
static void OnEnter_Stopwatch(void) {
g_currentActivity = &g_stopwatchNode;
g_rootMenu.parent = &g_stopwatchNode;
}
static void OnEnter_ZeroMillis(void) {
g_game1sec.state = 0;
Menu_Refresh();
}
static void OnEnter_Reaction(void) {
g_gameReaction.state = 0;
Menu_Refresh();
}
static void OnEnter_Clicker(void) {
g_gameClicker.active = false;
g_gameClicker.finished = false;
g_gameClicker.clicks = 0;
Menu_Refresh();
}
static void OnEnter_TimeEdit(void) {
g_timeData.editTime = ClockManager_GetTime(0);
g_timeData.editStep = 0;
g_timeData.blinkState = true;
g_timeData.lastBlink = HAL_GetTick();
}
static void OnEnter_DutyEdit(void) {
g_originalDuty = ClockManager_GetDuty();
g_editDuty = g_originalDuty;
Segment_SetBrightness(g_editDuty * 10);
}
static void OnEnter_Reset(void) {}
// ==================== Обновления ====================
static void OnUpdate_TimeEdit(void) {
uint32_t tick = HAL_GetTick();
if (tick - g_timeData.lastBlink >= 500) {
g_timeData.lastBlink = tick;
g_timeData.blinkState = !g_timeData.blinkState;
Menu_Refresh();
}
}
static void OnUpdate_Reaction(void) {
uint32_t tick = HAL_GetTick();
if (g_gameReaction.state == 1) {
if (tick - g_gameReaction.waitStart >= g_gameReaction.ledOnTime) {
g_gameReaction.state = 2;
g_gameReaction.waitStart = tick;
Menu_Refresh();
}
}
else if (g_gameReaction.state == 2) {
if (tick - g_gameReaction.waitStart >= 2000) {
g_gameReaction.state = 3;
g_gameReaction.reactionTime = 999;
Menu_Refresh();
}
}
}
static void OnUpdate_Clicker(void) {
if (g_gameClicker.active && HAL_GetTick() >= g_gameClicker.endTime) {
g_gameClicker.active = false;
g_gameClicker.finished = true;
Menu_Refresh();
}
}
// ==================== Обработчики кнопок активностей ====================
static void Clock_OnButton(Button_Type btn, bool longPress) {
if (longPress && btn == BUTTON_SELECT) {
Menu_OpenMenu(&g_rootMenu);
}
}
static void Timer_OnButton(Button_Type btn, bool longPress) {
if (longPress && btn == BUTTON_SELECT) {
Menu_OpenMenu(&g_rootMenu);
}
}
static void Stopwatch_OnButton(Button_Type btn, bool longPress) {
if (longPress && btn == BUTTON_SELECT) {
Menu_OpenMenu(&g_rootMenu);
}
}
static void ZeroMillis_OnButton(Button_Type btn, bool longPress) {
if (longPress && btn == BUTTON_SELECT) {
Menu_OpenMenu(&g_rootMenu);
return;
}
if (btn == BUTTON_BACK) {
Menu_GoBack();
}
uint32_t tick = HAL_GetTick();
if (g_game1sec.state == 0 && btn == BUTTON_SELECT) {
g_game1sec.state = 1;
g_game1sec.startTime = tick;
Menu_Refresh();
}
else if ((btn == BUTTON_SELECT) && (g_game1sec.state == 1)) {
g_game1sec.state = 2;
g_game1sec.pressTime = HAL_GetTick();
g_game1sec.diff_ms = (g_game1sec.pressTime - g_game1sec.startTime);
if (g_game1sec.diff_ms < 0) g_game1sec.diff_ms = -g_game1sec.diff_ms;
Menu_Refresh();
}
else if ((g_game1sec.state == 2) && (btn == BUTTON_SELECT))
{
g_game1sec.state = 0;
}
}
static void Reaction_OnButton(Button_Type btn, bool longPress) {
if (longPress && btn == BUTTON_SELECT) {
Menu_OpenMenu(&g_rootMenu);
return;
}
if (btn == BUTTON_BACK) {
Menu_GoBack();
}
uint32_t tick = HAL_GetTick();
if (g_gameReaction.state == 0 && btn == BUTTON_SELECT) {
g_gameReaction.state = 1;
g_gameReaction.waitStart = tick;
g_gameReaction.ledOnTime = 1000 + (rand() % 4000);
Menu_Refresh();
}
else if (g_gameReaction.state == 2 && btn == BUTTON_SELECT) {
g_gameReaction.reactionTime = tick - g_gameReaction.waitStart;
g_gameReaction.state = 3;
Menu_Refresh();
}
else if ((g_gameReaction.state == 3) && (btn == BUTTON_SELECT))
{
g_gameReaction.state = 0;
}
}
static void Clicker_OnButton(Button_Type btn, bool longPress) {
if (longPress && btn == BUTTON_SELECT) {
Menu_OpenMenu(&g_rootMenu);
return;
}
if (btn == BUTTON_BACK) {
Menu_GoBack();
}
if (!g_gameClicker.active && !g_gameClicker.finished && btn == BUTTON_SELECT) {
g_gameClicker.active = true;
g_gameClicker.endTime = HAL_GetTick() + 10000;
g_gameClicker.clicks = 0;
Menu_Refresh();
}
else if (g_gameClicker.active && btn == BUTTON_UP) {
g_gameClicker.clicks++;
Menu_Refresh();
}
else if (g_gameClicker.finished && (btn == BUTTON_SELECT))
{
g_gameClicker.finished = 0;
g_gameClicker.active = 0;
}
}
static void TimeEdit_OnButton(Button_Type btn, bool longPress) {
(void)longPress;
switch (btn) {
case BUTTON_UP: {
uint8_t tens, units;
switch (g_timeData.editStep) {
case 0:
tens = g_timeData.editTime.hour / 10;
units = g_timeData.editTime.hour % 10;
tens = (tens + 1) % 3;
g_timeData.editTime.hour = tens * 10 + units;
break;
case 1:
tens = g_timeData.editTime.hour / 10;
units = (g_timeData.editTime.hour % 10 + 1) % 10;
if (tens == 2 && units > 3) units = 0;
g_timeData.editTime.hour = tens * 10 + units;
break;
case 2:
tens = (g_timeData.editTime.min / 10 + 1) % 6;
g_timeData.editTime.min = tens * 10 + (g_timeData.editTime.min % 10);
break;
case 3:
units = (g_timeData.editTime.min % 10 + 1) % 10;
g_timeData.editTime.min = (g_timeData.editTime.min / 10) * 10 + units;
break;
case 4:
tens = (g_timeData.editTime.sec / 10 + 1) % 6;
g_timeData.editTime.sec = tens * 10 + (g_timeData.editTime.sec % 10);
break;
case 5:
units = (g_timeData.editTime.sec % 10 + 1) % 10;
g_timeData.editTime.sec = (g_timeData.editTime.sec / 10) * 10 + units;
break;
}
Menu_Refresh();
break;
}
case BUTTON_DOWN: {
uint8_t tens, units;
switch (g_timeData.editStep) {
case 0:
tens = g_timeData.editTime.hour / 10;
units = g_timeData.editTime.hour % 10;
tens = (tens == 0) ? 2 : tens - 1;
g_timeData.editTime.hour = tens * 10 + units;
break;
case 1:
tens = g_timeData.editTime.hour / 10;
units = g_timeData.editTime.hour % 10;
if (units == 0) units = (tens == 2) ? 3 : 9;
else units--;
g_timeData.editTime.hour = tens * 10 + units;
break;
case 2:
tens = g_timeData.editTime.min / 10;
tens = (tens == 0) ? 5 : tens - 1;
g_timeData.editTime.min = tens * 10 + (g_timeData.editTime.min % 10);
break;
case 3:
units = g_timeData.editTime.min % 10;
units = (units == 0) ? 9 : units - 1;
g_timeData.editTime.min = (g_timeData.editTime.min / 10) * 10 + units;
break;
case 4:
tens = g_timeData.editTime.sec / 10;
tens = (tens == 0) ? 5 : tens - 1;
g_timeData.editTime.sec = tens * 10 + (g_timeData.editTime.sec % 10);
break;
case 5:
units = g_timeData.editTime.sec % 10;
units = (units == 0) ? 9 : units - 1;
g_timeData.editTime.sec = (g_timeData.editTime.sec / 10) * 10 + units;
break;
}
Menu_Refresh();
break;
}
case BUTTON_SELECT:
g_timeData.editStep++;
if (g_timeData.editStep >= 6) {
ClockManager_SetTime(g_timeData.editTime.hour,
g_timeData.editTime.min,
g_timeData.editTime.sec);
Menu_GoBack();
}
Menu_Refresh();
break;
case BUTTON_BACK:
Menu_GoBack();
default:
break;
}
}
static void DutyEdit_OnButton(Button_Type btn, bool longPress) {
(void)longPress;
switch (btn) {
case BUTTON_UP:
if (g_editDuty < 10) {
g_editDuty++;
Segment_SetBrightness(g_editDuty * 10);
Menu_Refresh();
}
break;
case BUTTON_DOWN:
if (g_editDuty > 0) {
g_editDuty--;
Segment_SetBrightness(g_editDuty * 10);
Menu_Refresh();
}
break;
case BUTTON_SELECT:
ClockManager_SetDuty(g_editDuty);
Menu_GoBack();
break;
case BUTTON_BACK:
ClockManager_SetDuty(g_originalDuty);
Menu_GoBack();
default:
break;
}
}
static void Reset_OnButton(Button_Type btn, bool longPress) {
if ((btn == BUTTON_SELECT) && longPress) {
ClockManager_ResetTime();
ClockManager_SetDuty(5);
Menu_GoBack();
}
}
// ==================== Узлы активностей (корень) ====================
MenuNode g_clockNode = {
.name = "CLOC",
.parent = NULL,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_Clock,
.onEnter = OnEnter_Clock,
.onUpdate = NULL,
.onButton = Clock_OnButton,
.data = NULL
};
MenuNode g_timerNode = {
.name = "TIMER",
.parent = NULL,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_Timer,
.onEnter = OnEnter_Timer,
.onUpdate = NULL,
.onButton = Timer_OnButton,
.data = NULL
};
MenuNode g_stopwatchNode = {
.name = "SECOND",
.parent = NULL,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_Stopwatch,
.onEnter = OnEnter_Stopwatch,
.onUpdate = NULL,
.onButton = Stopwatch_OnButton,
.data = NULL
};
MenuNode g_ZeroMillisNode = {
.name = "00 SEC",
.parent = &g_gamesNode,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.needsRedraw = 1,
.display = Display_ZeroMillis,
.onEnter = OnEnter_ZeroMillis,
.onUpdate = NULL,
.onButton = ZeroMillis_OnButton,
.data = &g_game1sec
};
MenuNode g_ReactionTimeNode = {
.name = "CSTEST",
.parent = &g_gamesNode,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_Reaction,
.onEnter = OnEnter_Reaction,
.onUpdate = OnUpdate_Reaction,
.onButton = Reaction_OnButton,
.data = &g_gameReaction
};
MenuNode g_ClickerTimeNode = {
.name = "CLICER",
.parent = &g_gamesNode,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_Clicker,
.onEnter = OnEnter_Clicker,
.onUpdate = OnUpdate_Clicker,
.onButton = Clicker_OnButton,
.data = &g_gameClicker
};
// ==================== Узлы меню ====================
MenuNode g_gamesNode;
MenuNode g_settingsNode;
MenuNode g_timeEditNode;
MenuNode g_dutyEditNode;
MenuNode g_LEDEditNode;
MenuNode g_MenuSoundNode;
MenuNode g_PowerOnSongNode;
MenuNode g_SongNode;
MenuNode g_resetNode;
static MenuNode* g_gamesChildren[] = {
&g_ZeroMillisNode,
&g_ReactionTimeNode,
&g_ClickerTimeNode
};
static MenuNode* g_settingsChildren[] = {
&g_timeEditNode,
&g_dutyEditNode,
&g_LEDEditNode,
&g_MenuSoundNode,
&g_PowerOnSongNode,
&g_SongNode,
&g_resetNode
};
static MenuNode* g_mainMenuChildren[] = {
&g_clockNode,
&g_timerNode,
&g_stopwatchNode,
&g_gamesNode,
&g_settingsNode
};
static MenuNode g_rootMenu = {
.name = "MAIN",
.parent = NULL,
.children = g_mainMenuChildren,
.childCount = 5,
.selectedChild = 0,
.display = Display_MenuItem,
.onEnter = NULL,
.onUpdate = NULL,
.onButton = NULL,
.data = NULL
};
void MenuItems_Init(void) {
g_gamesNode = (MenuNode){
.name = "PLAY",
.parent = &g_rootMenu,
.children = g_gamesChildren,
.childCount = 3,
.selectedChild = 0,
.display = Display_MenuItem,
.onEnter = NULL,
.onUpdate = NULL,
.onButton = NULL,
.data = NULL
};
g_settingsNode = (MenuNode){
.name = "SETUP",
.parent = &g_rootMenu,
.children = g_settingsChildren,
.childCount = 7,
.selectedChild = 0,
.display = Display_MenuItem,
.onEnter = NULL,
.onUpdate = NULL,
.onButton = NULL,
.data = NULL
};
g_timeEditNode = (MenuNode){
.name = "SET T",
.parent = &g_settingsNode,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_TimeEdit,
.onEnter = OnEnter_TimeEdit,
.onUpdate = OnUpdate_TimeEdit,
.onButton = TimeEdit_OnButton,
.data = &g_timeData
};
g_dutyEditNode = (MenuNode){
.name = "SET D",
.parent = &g_settingsNode,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_DutyEdit,
.onEnter = OnEnter_DutyEdit,
.onUpdate = NULL,
.onButton = DutyEdit_OnButton,
.data = &g_editDuty
};
g_resetNode = (MenuNode){
.name = "RESET",
.parent = &g_settingsNode,
.children = NULL,
.childCount = 0,
.selectedChild = 0,
.display = Display_Reset,
.onEnter = OnEnter_Reset,
.onUpdate = NULL,
.onButton = Reset_OnButton,
.data = NULL
};
}
MenuNode* Menu_GetRootMenu(void) {
return &g_rootMenu;
}
MenuNode* Menu_GetCurrentActivity(void) {
return g_currentActivity;
}

19
Core/Clock/menu_items.h Normal file
View File

@@ -0,0 +1,19 @@
#ifndef MENU_ITEMS_H
#define MENU_ITEMS_H
#include "menu.h"
// Глобальные узлы меню (для доступа из main.c)
extern MenuNode g_clockNode;
extern MenuNode g_timerNode;
extern MenuNode g_stopwatchNode;
extern MenuNode g_gamesNode;
extern MenuNode g_settingsNode;
extern MenuNode g_timeEditNode;
extern MenuNode g_dutyEditNode;
extern MenuNode g_resetNode;
// Инициализация всех пунктов меню
void MenuItems_Init(void);
#endif

View File

@@ -25,7 +25,7 @@
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "clock_manager.h"
#include "menu.h"
#include "menu_items.h"
#include "segment.h"
#include "melody.h"
#include "songs.h"
@@ -81,7 +81,6 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
}
}
MelodyHandle melody;
#define curr_song Polyphia_OD
Melody_t *mySong = &curr_song;
@@ -127,12 +126,13 @@ int main(void)
/* USER CODE BEGIN WHILE */
Segment_Init();
ClockManager_Init();
Menu_Init();
Menu_Init(&g_clockNode);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_Base_Start_IT(&htim2);
MenuItems_Init();
Melody_Init(&melody, &htim1, TIM_CHANNEL_1, 72000000);
Melody_Play(&melody, mySong, 134);
// Melody_Play(&melody, mySong, 134);
while (1)
{
Menu_Process();