// ============================================================================ // ТРЕНАЖЕР БЕСКОНЕЧНАЯ ЛЕСТНИЦА - Complete Implementation // ============================================================================ #include #include #include #include #include #include // ============================================================================ // PIN DEFINITIONS // ============================================================================ // Motor Control #define PWM_OUT 9 #define ENABLE_OUT 19 // Local Control Panel Buttons #define BTN_MODE_SELECT 22 #define BTN_INCREASE 23 #define BTN_DECREASE 24 #define BTN_RESET_PARAMS 25 #define BTN_START 26 #define BTN_STOP 27 #define BTN_SPEED_DOWN 30 #define BTN_SPEED_UP 31 #define BTN_EMERGENCY_MUSHROOM 29 #define EMERGENCY_LASER_SENSORS 32 // Radio Remote Control (7 buttons) #define RADIO_SPEED_UP 40 #define RADIO_SPEED_DOWN 41 #define RADIO_START_STOP 42 #define RADIO_MODE_SELECT 43 #define RADIO_RESET 44 #define RADIO_DECREASE 45 #define RADIO_INCREASE 46 // Sensors #define ENCODER_A 2 // Interrupt capable (INT0) #define ENCODER_B 3 // Interrupt capable (INT1) #define LIMIT_SW_SIGNAL 33 // Display Systems #define LCD_SDA 20 #define LCD_SCL 21 #define MATRIX_DIN 51 // SPI MOSI #define MATRIX_CLK 52 // SPI SCK #define MATRIX_CS 53 // SPI SS // Status LEDs #define LED_GREEN 35 #define LED_RED 36 #define LED_EMERGENCY 37 // ============================================================================ // CONSTANTS // ============================================================================ const int SPEED_MIN = 0; const int SPEED_MAX = 100; const int SPEED_STEP = 1; const unsigned long DEBOUNCE_DELAY = 50; const unsigned long REPEAT_DELAY = 500; const unsigned long REPEAT_INTERVAL = 100; const unsigned long EMERGENCY_BLINK_INTERVAL = 500; const unsigned long COUNTDOWN_INTERVAL = 1000; // 1 second per countdown step // Encoder: LPD3806-360BM-G5-24C // Specifications: 360 pulses per revolution, quadrature decoding gives 4x = 1440 counts per revolution const int ENCODER_COUNTS_PER_REVOLUTION = 1440; // Quadrature decoding: 360 pulses * 4 edges = 1440 counts const float ENCODER_METERS_PER_REVOLUTION = 0.350f; // 350mm = 0.35m per revolution (mechanical) const float ENCODER_METERS_PER_COUNT = ENCODER_METERS_PER_REVOLUTION / ENCODER_COUNTS_PER_REVOLUTION; // ~0.000243m per count // LCD I2C address (try common addresses) #define LCD_ADDRESS 0x27 #define LCD_COLS 16 #define LCD_ROWS 2 // LED Matrix configuration #define MATRIX_MODULES 8 // Number of MAX7219 modules in series (1x8 configuration) // EEPROM addresses for storing settings #define EEPROM_TIME_MINUTES_ADDR 0 #define EEPROM_TIME_SECONDS_ADDR 1 #define EEPROM_DISTANCE_ADDR 2 // 2 bytes for distance (0-65535 meters) #define EEPROM_SPEED_ADDR 4 // Speed percentage (0-100) #define EEPROM_MODE_ADDR 5 // Exercise mode (MODE_TIME=1, MODE_DISTANCE=2) #define EEPROM_MAGIC_BYTE 6 // Magic byte to check if EEPROM is initialized #define EEPROM_MAGIC_VALUE 0xAA // Default values #define DEFAULT_TIME_MINUTES 5 #define DEFAULT_TIME_SECONDS 0 #define DEFAULT_DISTANCE_METERS 100 // ============================================================================ // ENUMS AND DATA STRUCTURES // ============================================================================ enum ExerciseMode { MODE_NONE, MODE_TIME, MODE_DISTANCE }; enum ExerciseState { STATE_IDLE, STATE_MODE_SELECTED, STATE_READY, STATE_COUNTDOWN, STATE_RUNNING, STATE_PAUSED, STATE_EMERGENCY }; struct ExerciseParams { ExerciseMode mode; int timeMinutes; int timeSeconds; int distanceMeters; // in meters, increments of 100 }; struct ExerciseProgress { unsigned long startTime; unsigned long pausedTime; unsigned long totalPausedTime; volatile long encoderCount; float distanceTraveled; bool isPaused; }; // ============================================================================ // GLOBAL VARIABLES // ============================================================================ // System state ExerciseState exerciseState = STATE_IDLE; ExerciseMode exerciseMode = MODE_TIME; // Default to time mode bool emergency = false; int speedPercent = 50; // Exercise parameters ExerciseParams exerciseParams; ExerciseProgress exerciseProgress; // Countdown int countdownValue = 0; unsigned long countdownStartTime = 0; // Button states bool lastStartBtnState = HIGH; bool lastStopBtnState = HIGH; bool lastSpeedUpBtnState = HIGH; bool lastSpeedDownBtnState = HIGH; bool lastModeSelectBtnState = HIGH; unsigned long modeSelectButtonPressTime = 0; // Track when mode button was pressed (for debouncing momentary button) bool lastIncreaseBtnState = HIGH; bool lastDecreaseBtnState = HIGH; bool lastResetParamsBtnState = HIGH; bool lastEmergencyBtnState = HIGH; // Radio button states bool lastRadioSpeedUpBtnState = HIGH; bool lastRadioSpeedDownBtnState = HIGH; bool lastRadioStartStopBtnState = HIGH; bool lastRadioModeSelectBtnState = HIGH; bool lastRadioResetBtnState = HIGH; bool lastRadioDecreaseBtnState = HIGH; bool lastRadioIncreaseBtnState = HIGH; // Button press timers unsigned long speedUpPressTime = 0; unsigned long speedDownPressTime = 0; unsigned long radioSpeedUpPressTime = 0; unsigned long radioSpeedDownPressTime = 0; unsigned long increasePressTime = 0; unsigned long decreasePressTime = 0; unsigned long radioIncreasePressTime = 0; unsigned long radioDecreasePressTime = 0; // Button repeat timers (for auto-repeat on hold) unsigned long lastSpeedUpRepeat = 0; unsigned long lastSpeedDownRepeat = 0; unsigned long lastIncreaseRepeat = 0; unsigned long lastDecreaseRepeat = 0; // Button processed flags (to prevent multiple triggers) bool modeSelectButtonProcessed = false; bool increaseButtonProcessed = false; bool decreaseButtonProcessed = false; // Emergency bool emergencyBlinkState = false; unsigned long lastEmergencyBlink = 0; bool emergencyMatrixFlashState = false; unsigned long lastEmergencyMatrixFlash = 0; const unsigned long EMERGENCY_MATRIX_FLASH_INTERVAL = 1000; // 1 second on, 1 second off = 2 second cycle // Display update timing unsigned long lastDisplayUpdate = 0; const unsigned long DISPLAY_UPDATE_INTERVAL = 250; // Update display every 250ms (reduced flashing) // Cache last displayed values to prevent unnecessary updates unsigned long lastDisplayedMinutes = 999; unsigned long lastDisplayedSeconds = 999; int lastDisplayedDistance = -1; int lastDisplayedSpeed = -1; // Display mode for matrix (what to show) // Note: Remaining time/distance is always shown when running (per requirements) bool showSpeedOnMatrix = true; // Whether to show speed on matrix (uses last 3 modules) // Flags to force reset of display caches (set to true when pausing/resuming) bool resetTimeDisplayCacheFlag = false; bool resetSpeedDisplayCacheFlag = false; bool resetDistanceDisplayCacheFlag = false; // Encoder state (for quadrature decoding) volatile int encoderLastState = 0; // Display objects - using standard I2C LCD library with hybrid Russian/Latin support LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLS, LCD_ROWS); LedControl matrix = LedControl(MATRIX_DIN, MATRIX_CLK, MATRIX_CS, MATRIX_MODULES); // ============================================================================ // HYBRID RUSSIAN/LATIN LCD SOLUTION // ============================================================================ // HD44780 LCD supports 8 custom characters (CGRAM slots 0-7) // Strategy: Use Latin characters where Russian and Latin look alike, // create 8 custom characters for unique Russian letters // Custom character definitions (5x8 pixels, stored as 8 bytes) // Each byte represents one row (top to bottom) // HD44780 uses only bits 0-4 (lower 5 bits) for the 5-pixel width // Format: B0B1B2B3B4000 where B0-B4 are the pixel columns (left to right) // Character slot 0: Б (Be) byte char_BE[8] = { B01110, // ### B10000, // # B11110, // ##### B10001, // # # B10001, // # # B10001, // ##### B11110, // B00000 // }; // Character slot 1: И (I) byte char_I[8] = { B10001, // # # B10001, // # ## B10011, // # # # B10101, // ## # B11001, // # # B10001, // # # B10001, // B00000 // }; // Character slot 2: Ц (Tse) byte char_TS[8] = { B10010, // # # B10010, // # # B10010, // # # B10010, // # # B10010, // # # B10010, // # # B11111, // ##### B00001 // # }; // Character slot 3: Ч (Che) byte char_CH[8] = { B10001, // # # B10001, // # # B10001, // # # B10001, // # # B01111, // #### B00001, // # B00001, // # B00000 // }; // Character slot 4: Я (Ya) byte char_YA[8] = { B01111, // #### B10001, // # # B10001, // # # B01111, // #### B00101, // # # B01001, // # # B10001, // # # B00000 // }; // Character slot 5: Л (El) byte char_L[8] = { B00111, // ### B01101, // ## # B11001, // ## # B10001, // # # B10001, // # # B10001, // # # B10001, // B00000 // }; // Character slot 6: Д (De) byte char_D[8] = { B00100, // ## B01010, // # # B01010, // # # B01010, // # # B11111, // ##### B10001, // # # B10001, // # # B00000 // }; // Character slot 7: ь (Soft sign) byte char_soft_sign[8] = { B10000, // # B10000, // # B10000, // # B11110, // # B10010, // ### B10010, // # # B11110, // ### B00000 // }; // UTF-8 to character mapping for Russian Cyrillic // Russian Cyrillic characters in UTF-8 are 2 bytes // Uppercase А-Я: 0xD0 0x90-0xAF // Lowercase а-я: 0xD0 0xB0-0xBF and 0xD1 0x80-0x8F byte mapRussianUTF8(const unsigned char* utf8Bytes, int& bytesConsumed) { bytesConsumed = 1; // Default: consume 1 byte // Check if this is a UTF-8 sequence for Cyrillic (starts with 0xD0 or 0xD1) if (utf8Bytes[0] == 0xD0 && utf8Bytes[1] >= 0x90 && utf8Bytes[1] <= 0xAF) { // Cyrillic uppercase (А-Я) range: 0xD0 0x90-0xAF bytesConsumed = 2; unsigned char code = utf8Bytes[1]; // Map Cyrillic uppercase to Latin/custom switch (code) { case 0x90: return 'A'; // А case 0x91: return 0; // Б -> custom 0 case 0x92: return 'B'; // В case 0x93: return 'r'; // Г case 0x94: return 6; // Д -> custom 6 case 0x95: return 'E'; // Е case 0x96: return 'b'; // Ж -> use 'b' as approximation (not used) case 0x97: return '3'; // З case 0x98: return 1; // И -> custom 1 case 0x99: return 1; // Й (same as И for display) case 0x9A: return 'K'; // К case 0x9B: return 5; // Л -> custom 5 case 0x9C: return 'M'; // М case 0x9D: return 'H'; // Н case 0x9E: return 'O'; // О case 0x9F: return 'n'; // П case 0xA0: return 'P'; // Р case 0xA1: return 'C'; // С case 0xA2: return 'T'; // Т case 0xA3: return 'Y'; // У case 0xA4: return 'F'; // Ф case 0xA5: return 'X'; // Х case 0xA6: return 2; // Ц -> custom 2 case 0xA7: return 3; // Ч -> custom 3 case 0xA8: return 'W'; // Ш case 0xA9: return 'W'; // Щ case 0xAA: return 'B'; // Ъ case 0xAB: return 'b'; // Ы case 0xAC: return 7; // Ь -> custom 7 case 0xAD: return 'E'; // Э case 0xAE: return 'U'; // Ю case 0xAF: return 4; // Я -> custom 4 default: return utf8Bytes[0]; // Return first byte if unknown } } else if (utf8Bytes[0] == 0xD0 && utf8Bytes[1] >= 0xB0 && utf8Bytes[1] <= 0xBF) { // Cyrillic lowercase а-п: 0xD0 0xB0-0xBF bytesConsumed = 2; unsigned char code = utf8Bytes[1]; switch (code) { case 0xB0: return 'A'; // а case 0xB1: return 0; // б -> custom 0 case 0xB2: return 'B'; // в case 0xB3: return 'r'; // г case 0xB4: return 6; // д -> custom 6 case 0xB5: return 'E'; // е case 0xB6: return 'b'; // ж -> use 'b' as approximation (not used) case 0xB7: return '3'; // з case 0xB8: return 1; // и -> custom 1 case 0xB9: return 1; // й (same as и) case 0xBA: return 'K'; // к case 0xBB: return 5; // л -> custom 5 case 0xBC: return 'M'; // м case 0xBD: return 'H'; // н case 0xBE: return 'O'; // о case 0xBF: return 'n'; // п default: return utf8Bytes[0]; } } else if (utf8Bytes[0] == 0xD1 && utf8Bytes[1] >= 0x80 && utf8Bytes[1] <= 0x8F) { // Cyrillic lowercase р-я: 0xD1 0x80-0x8F bytesConsumed = 2; unsigned char code = utf8Bytes[1]; switch (code) { case 0x80: return 'P'; // р case 0x81: return 'C'; // с case 0x82: return 'T'; // т case 0x83: return 'Y'; // у case 0x84: return 'F'; // ф case 0x85: return 'X'; // х case 0x86: return 2; // ц -> custom 2 case 0x87: return 3; // ч -> custom 3 case 0x88: return 'W'; // ш case 0x89: return 'W'; // щ case 0x8A: return 'B'; // ъ case 0x8B: return 'b'; // ы case 0x8C: return 7; // ь -> custom 7 case 0x8D: return 'E'; // э case 0x8E: return 'U'; // ю case 0x8F: return 4; // я -> custom 4 default: return utf8Bytes[0]; } } // Not a Russian character, return as-is (ASCII) return utf8Bytes[0]; } // Initialize custom characters in LCD CGRAM void initCustomChars() { // Create custom characters - HD44780 supports 8 custom characters (0-7) // Each character is 8 bytes (one per row), only lower 5 bits are used lcd.createChar(0, char_BE); // Б delay(2); // Small delay between character creation lcd.createChar(1, char_I); // И delay(2); lcd.createChar(2, char_TS); // Ц delay(2); lcd.createChar(3, char_CH); // Ч delay(2); lcd.createChar(4, char_YA); // Я delay(2); lcd.createChar(5, char_L); // Л delay(2); lcd.createChar(6, char_D); // Д delay(2); lcd.createChar(7, char_soft_sign); // ь (soft sign) delay(2); } // Function to print Russian text on LCD using hybrid approach // Handles UTF-8 encoded Russian characters properly void lcdPrintRussianText(const char* text) { if (text == NULL) return; int i = 0; while (text[i] != '\0') { int bytesConsumed = 1; byte mappedChar = mapRussianUTF8((const unsigned char*)&text[i], bytesConsumed); // If mapped to custom character (0-7), write it directly using write() // Custom characters are stored in CGRAM slots 0-7 if (mappedChar <= 7) { lcd.write((uint8_t)mappedChar); } else { // Otherwise, print as regular character (Latin equivalent or original) lcd.print((char)mappedChar); } // Advance by the number of bytes consumed (1 for ASCII, 2 for UTF-8 Cyrillic) i += bytesConsumed; } } // ============================================================================ // FONT DATA FOR LED MATRIX (8x8 Russian/Numeric Font) // ============================================================================ // Font data: Each character is 8 bytes (8 rows, 1 byte per row) // Pre-mirrored both vertically and horizontally for MAX7219 // Bit pattern: LSB is left, MSB is right (reversed from original) const byte font[][8] = { // Numbers 0-9 (mirrored) {0x7C, 0xC6, 0xE6, 0xF6, 0xDE, 0xCE, 0xC6, 0x7C}, // 0 {0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x38, 0x18}, // 1 {0xFE, 0xC6, 0xC0, 0x70, 0x1C, 0x06, 0xC6, 0x7C}, // 2 {0x7C, 0xC6, 0x06, 0x06, 0x3C, 0x06, 0xC6, 0x7C}, // 3 {0x0C, 0x0C, 0xFE, 0xCC, 0x6C, 0x3C, 0x1C, 0x0C}, // 4 {0x7C, 0xC6, 0x06, 0x06, 0xFC, 0xC0, 0xC0, 0xFE}, // 5 {0x7C, 0xC6, 0xC6, 0xC6, 0xFC, 0xC0, 0xC6, 0x7C}, // 6 {0x30, 0x30, 0x30, 0x18, 0x0C, 0x06, 0xC6, 0xFE}, // 7 {0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0x7C}, // 8 {0x7C, 0xC6, 0x06, 0x7E, 0xC6, 0xC6, 0xC6, 0x7C}, // 9 // Russian letters (Cyrillic) - mirrored {0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6}, // А (corrected) {0xFC, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x66, 0xFC}, // Б {0x3C, 0x66, 0xC0, 0xC0, 0xC0, 0xC0, 0x66, 0x3C}, // В {0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF8}, // Г {0xFE, 0x62, 0x60, 0x68, 0x78, 0x68, 0x62, 0xFE}, // Д {0xFE, 0xC0, 0xC0, 0xFC, 0xC0, 0xC0, 0xC0, 0xFE}, // Е {0xC0, 0xC0, 0xC0, 0xFC, 0xC0, 0xC0, 0xC0, 0xFE}, // Ж {0x7C, 0xC6, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xFE}, // З (corrected - looks like 3) {0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6}, // И {0xC6, 0xC6, 0x6C, 0x38, 0x38, 0x6C, 0xC6, 0xC6}, // К {0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0xC6, 0xC6, 0xC6}, // Л {0xC6, 0xEE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0xC6}, // М {0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E}, // Н (corrected) {0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C}, // О {0xFC, 0xC6, 0xC6, 0xC6, 0xFC, 0xC0, 0xC0, 0xC0}, // П (corrected - should look like П) {0x76, 0x7E, 0xDE, 0xD6, 0xC6, 0xC6, 0xC6, 0x7C}, // Р {0x66, 0x66, 0x6C, 0x78, 0x7C, 0x66, 0x66, 0xFC}, // С {0xFC, 0xC6, 0xC6, 0xC6, 0xFC, 0xC0, 0xC0, 0xC0}, // Т {0x7C, 0xC6, 0x06, 0x3C, 0x60, 0xC6, 0xC6, 0x7C}, // У (corrected - should look like Y shape) {0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF8}, // Ф {0xFE, 0x86, 0x06, 0x3E, 0x06, 0x86, 0xC6, 0xFE}, // Х {0x06, 0x06, 0x06, 0x3E, 0x06, 0x86, 0xC6, 0xFE}, // Ц {0xFC, 0xC6, 0xC6, 0xE6, 0x06, 0x06, 0xC6, 0x7C}, // Ч {0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6}, // Ш {0xDB, 0xDB, 0x7E, 0x18, 0xD8, 0xD8, 0x78, 0x78}, // Щ {0xFE, 0xC6, 0x60, 0x30, 0x18, 0x0C, 0xC6, 0xFE}, // Ъ {0xEE, 0x6C, 0x6C, 0xC6, 0xC6, 0xC6, 0x6C, 0x38}, // Ы {0x3E, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06}, // Ь {0x7C, 0xC6, 0xC6, 0xF6, 0xF6, 0xC6, 0xC6, 0x7C}, // Э {0x66, 0x66, 0x66, 0x7E, 0x66, 0x66, 0x3C, 0x18}, // Ю {0x60, 0x60, 0x60, 0x78, 0x7C, 0x66, 0x66, 0xFC}, // Я // Special characters - mirrored {0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00}, // - (minus/dash) {0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00}, // : (colon) {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // space {0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // _ (underscore) }; // Character mapping function byte getCharIndex(char c) { if (c >= '0' && c <= '9') { return c - '0'; // 0-9 } // Russian letters mapping (simplified - using ASCII-like mapping) // For full Cyrillic support, would need proper Unicode handling switch (c) { case 'А': case 'а': return 10; // А case 'Б': case 'б': return 11; // Б case 'В': case 'в': return 12; // В case 'Г': case 'г': return 13; // Г case 'Д': case 'д': return 14; // Д case 'Е': case 'е': return 15; // Е case 'Ж': case 'ж': return 16; // Ж case 'З': case 'з': return 17; // З case 'И': case 'и': return 18; // И case 'К': case 'к': return 19; // К case 'Л': case 'л': return 20; // Л case 'М': case 'м': return 21; // М case 'Н': case 'н': return 22; // Н case 'О': case 'о': return 23; // О case 'П': case 'п': return 24; // П case 'Р': case 'р': return 25; // Р case 'С': case 'с': return 26; // С case 'Т': case 'т': return 27; // Т case 'У': case 'у': return 28; // У case 'Ф': case 'ф': return 29; // Ф case 'Х': case 'х': return 30; // Х case 'Ц': case 'ц': return 31; // Ц case 'Ч': case 'ч': return 32; // Ч case 'Ш': case 'ш': return 33; // Ш case 'Щ': case 'щ': return 34; // Щ case 'Ъ': case 'ъ': return 35; // Ъ case 'Ы': case 'ы': return 36; // Ы case 'Ь': case 'ь': return 37; // Ь case 'Э': case 'э': return 38; // Э case 'Ю': case 'ю': return 39; // Ю case 'Я': case 'я': return 40; // Я case '-': return 41; // minus case ':': return 42; // colon case ' ': return 43; // space case '_': return 44; // underscore default: return 43; // space for unknown } } // Clear all matrix modules void clearAllMatrix() { for (int i = 0; i < MATRIX_MODULES; i++) { matrix.clearDisplay(i); } } // Bit reversal lookup table for faster horizontal mirroring const byte REVERSE_BITS_TABLE[256] PROGMEM = { 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF }; // Fast bit reversal using lookup table inline byte reverseBits(byte b) { return pgm_read_byte(&REVERSE_BITS_TABLE[b]); } // Display a single character on a specific matrix module // Font data is vertically mirrored, apply horizontal mirroring at runtime void displayChar(char c, int moduleIndex) { if (moduleIndex < 0 || moduleIndex >= MATRIX_MODULES) return; byte charIndex = getCharIndex(c); if (charIndex >= sizeof(font) / sizeof(font[0])) { charIndex = 43; // space } // Font is vertically mirrored, apply horizontal mirroring (bit reversal) at runtime // Use lookup table for fast bit reversal for (int row = 0; row < 8; row++) { byte mirroredData = reverseBits(font[charIndex][row]); matrix.setRow(moduleIndex, row, mirroredData); } } // Display a number (0-99999999) on the matrix across multiple modules void displayNumber(int number) { clearAllMatrix(); if (number < 0) number = 0; // Extract digits (up to 8 digits for 8 modules) int digits[8]; int digitCount = 0; if (number == 0) { digits[0] = 0; digitCount = 1; } else { int temp = number; while (temp > 0 && digitCount < 8) { digits[digitCount] = temp % 10; temp /= 10; digitCount++; } } // Display digits left to right (most significant on left) // digits[0] is least significant, digits[digitCount-1] is most significant for (int i = 0; i < digitCount && i < MATRIX_MODULES; i++) { int digitIndex = digitCount - 1 - i; // Get digit from most to least significant int moduleIndex = i; // Display from left to right displayChar('0' + digits[digitIndex], moduleIndex); } } // Display time MM:SS format across multiple modules void displayTimeMMSS(unsigned long minutes, unsigned long seconds) { // Only clear the modules we'll use, not all modules (prevents flashing) // Limit to 99:59 for display if (minutes > 99) minutes = 99; if (seconds > 59) seconds = 59; // Extract digits int minTens = (minutes / 10) % 10; int minOnes = minutes % 10; int secTens = (seconds / 10) % 10; int secOnes = seconds % 10; // Only update if values changed static int lastMinTens = -1, lastMinOnes = -1, lastSecTens = -1, lastSecOnes = -1; static bool colonDisplayed = false; // Reset cache if flag is set (called from pause/resume) if (resetTimeDisplayCacheFlag) { lastMinTens = -1; lastMinOnes = -1; lastSecTens = -1; lastSecOnes = -1; colonDisplayed = false; resetTimeDisplayCacheFlag = false; // Clear all modules to force full redraw for (int i = 0; i < 5 && i < MATRIX_MODULES; i++) { matrix.clearDisplay(i); } } // Display format: MM:SS (5 characters + colon = 6 positions) // Modules: [0] [1] [2] [3] [4] [5] [6] [7] // Display: M M : S S // Position: 0 1 2 3 4 if (MATRIX_MODULES >= 5) { // Only update modules that changed - don't clear if same character if (minTens != lastMinTens) { if (lastMinTens != -1) matrix.clearDisplay(0); // Only clear if not first time displayChar('0' + minTens, 0); lastMinTens = minTens; } if (minOnes != lastMinOnes) { if (lastMinOnes != -1) matrix.clearDisplay(1); displayChar('0' + minOnes, 1); lastMinOnes = minOnes; } // Colon doesn't change, but display it once if (!colonDisplayed) { displayChar(':', 2); colonDisplayed = true; } if (secTens != lastSecTens) { if (lastSecTens != -1) matrix.clearDisplay(3); displayChar('0' + secTens, 3); lastSecTens = secTens; } if (secOnes != lastSecOnes) { if (lastSecOnes != -1) matrix.clearDisplay(4); displayChar('0' + secOnes, 4); lastSecOnes = secOnes; } } else { // Fallback for fewer modules - show just seconds if (MATRIX_MODULES >= 2) { if (secTens != lastSecTens) { matrix.clearDisplay(MATRIX_MODULES - 2); displayChar('0' + secTens, MATRIX_MODULES - 2); lastSecTens = secTens; } if (secOnes != lastSecOnes) { matrix.clearDisplay(MATRIX_MODULES - 1); displayChar('0' + secOnes, MATRIX_MODULES - 1); lastSecOnes = secOnes; } } else if (MATRIX_MODULES >= 1) { if (secOnes != lastSecOnes) { matrix.clearDisplay(0); displayChar('0' + secOnes, 0); lastSecOnes = secOnes; } } } } // Display text across multiple modules (up to MATRIX_MODULES characters) void displayText(const char* text) { if (text == NULL || strlen(text) == 0) { clearAllMatrix(); return; } clearAllMatrix(); // Display up to MATRIX_MODULES characters int len = strlen(text); int charsToShow = (len < MATRIX_MODULES) ? len : MATRIX_MODULES; for (int i = 0; i < charsToShow; i++) { displayChar(text[i], i); } } // Display "ПАУЗА" (PAUSE) on the matrix // Using character indices directly to avoid encoding issues void displayPause() { clearAllMatrix(); // ПАУЗА = 5 characters // Character indices in font array: 24 (П), 10 (А), 28 (У), 17 (З), 10 (А) byte pauseChars[5] = {24, 10, 28, 17, 10}; // П, А, У, З, А int charsToShow = (MATRIX_MODULES < 5) ? MATRIX_MODULES : 5; for (int i = 0; i < charsToShow; i++) { byte charIndex = pauseChars[i]; if (charIndex >= sizeof(font) / sizeof(font[0])) { charIndex = 43; // space if invalid } // Display character directly from font array for (int row = 0; row < 8; row++) { byte mirroredData = reverseBits(font[charIndex][row]); matrix.setRow(i, row, mirroredData); } } } // ============================================================================ // ENCODER INTERRUPT HANDLERS // ============================================================================ void encoderInterruptA() { int stateA = digitalRead(ENCODER_A); int stateB = digitalRead(ENCODER_B); // Quadrature decoding int currentState = (stateA << 1) | stateB; int lastState = encoderLastState; // Determine direction and count if (lastState == 0 && currentState == 2) exerciseProgress.encoderCount++; else if (lastState == 0 && currentState == 1) exerciseProgress.encoderCount--; else if (lastState == 2 && currentState == 3) exerciseProgress.encoderCount++; else if (lastState == 2 && currentState == 0) exerciseProgress.encoderCount--; else if (lastState == 3 && currentState == 1) exerciseProgress.encoderCount++; else if (lastState == 3 && currentState == 2) exerciseProgress.encoderCount--; else if (lastState == 1 && currentState == 0) exerciseProgress.encoderCount++; else if (lastState == 1 && currentState == 3) exerciseProgress.encoderCount--; encoderLastState = currentState; } void encoderInterruptB() { encoderInterruptA(); // Same logic for both interrupts } // ============================================================================ // EEPROM FUNCTIONS // ============================================================================ // Save exercise parameters to EEPROM void saveSettingsToEEPROM() { EEPROM.write(EEPROM_TIME_MINUTES_ADDR, exerciseParams.timeMinutes); EEPROM.write(EEPROM_TIME_SECONDS_ADDR, exerciseParams.timeSeconds); // Save distance as 2 bytes (high byte, low byte) EEPROM.write(EEPROM_DISTANCE_ADDR, (exerciseParams.distanceMeters >> 8) & 0xFF); EEPROM.write(EEPROM_DISTANCE_ADDR + 1, exerciseParams.distanceMeters & 0xFF); // Save speed (0-100) EEPROM.write(EEPROM_SPEED_ADDR, speedPercent); // Save mode (MODE_TIME=1, MODE_DISTANCE=2) EEPROM.write(EEPROM_MODE_ADDR, (byte)exerciseMode); // Write magic byte to indicate EEPROM is initialized EEPROM.write(EEPROM_MAGIC_BYTE, EEPROM_MAGIC_VALUE); } // Load exercise parameters from EEPROM void loadSettingsFromEEPROM() { // Check if EEPROM has been initialized (magic byte check) byte magic = EEPROM.read(EEPROM_MAGIC_BYTE); if (magic == EEPROM_MAGIC_VALUE) { // EEPROM is initialized, load values exerciseParams.timeMinutes = EEPROM.read(EEPROM_TIME_MINUTES_ADDR); exerciseParams.timeSeconds = EEPROM.read(EEPROM_TIME_SECONDS_ADDR); // Load distance as 2 bytes byte distanceHigh = EEPROM.read(EEPROM_DISTANCE_ADDR); byte distanceLow = EEPROM.read(EEPROM_DISTANCE_ADDR + 1); exerciseParams.distanceMeters = (distanceHigh << 8) | distanceLow; // Load speed speedPercent = EEPROM.read(EEPROM_SPEED_ADDR); // Load mode byte modeByte = EEPROM.read(EEPROM_MODE_ADDR); if (modeByte == MODE_TIME || modeByte == MODE_DISTANCE) { exerciseMode = (ExerciseMode)modeByte; } else { exerciseMode = MODE_TIME; // Default to time mode if invalid } // Validate loaded values if (exerciseParams.timeMinutes > 99) exerciseParams.timeMinutes = DEFAULT_TIME_MINUTES; if (exerciseParams.timeSeconds > 59) exerciseParams.timeSeconds = DEFAULT_TIME_SECONDS; if (exerciseParams.distanceMeters > 9999) exerciseParams.distanceMeters = DEFAULT_DISTANCE_METERS; if (speedPercent < 0 || speedPercent > 100) speedPercent = 50; Serial.print("Загружено из EEPROM: Режим="); Serial.print(exerciseMode == MODE_TIME ? "ВРЕМЯ" : "ДИСТАНЦИЯ"); Serial.print(", Время="); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.print(exerciseParams.timeSeconds); Serial.print(", Дистанция="); Serial.print(exerciseParams.distanceMeters); Serial.print("м, Скорость="); Serial.print(speedPercent); Serial.println("%"); } else { // EEPROM not initialized, use defaults exerciseParams.timeMinutes = DEFAULT_TIME_MINUTES; exerciseParams.timeSeconds = DEFAULT_TIME_SECONDS; exerciseParams.distanceMeters = DEFAULT_DISTANCE_METERS; speedPercent = 50; exerciseMode = MODE_TIME; // Default to time mode // Save defaults to EEPROM saveSettingsToEEPROM(); Serial.print("Использованы значения по умолчанию: Режим=ВРЕМЯ, Время="); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.print(exerciseParams.timeSeconds); Serial.print(", Дистанция="); Serial.print(exerciseParams.distanceMeters); Serial.print("м, Скорость="); Serial.print(speedPercent); Serial.println("%"); } // Set state to READY if mode and parameters are valid if (exerciseMode == MODE_TIME) { if (exerciseParams.timeMinutes > 0 || exerciseParams.timeSeconds > 0) { exerciseState = STATE_READY; } else { exerciseState = STATE_MODE_SELECTED; } } else if (exerciseMode == MODE_DISTANCE) { if (exerciseParams.distanceMeters > 0) { exerciseState = STATE_READY; } else { exerciseState = STATE_MODE_SELECTED; } } } // ============================================================================ // SETUP // ============================================================================ void setup() { Serial.begin(115200); Serial.println("=== ТРЕНАЖЕР БЕСКОНЕЧНАЯ ЛЕСТНИЦА ==="); Serial.println("Запуск системы..."); // Initialize pins pinMode(BTN_START, INPUT_PULLUP); pinMode(BTN_STOP, INPUT_PULLUP); pinMode(BTN_SPEED_DOWN, INPUT_PULLUP); pinMode(BTN_SPEED_UP, INPUT_PULLUP); pinMode(BTN_MODE_SELECT, INPUT_PULLUP); pinMode(BTN_INCREASE, INPUT_PULLUP); pinMode(BTN_DECREASE, INPUT_PULLUP); pinMode(BTN_RESET_PARAMS, INPUT_PULLUP); pinMode(BTN_EMERGENCY_MUSHROOM, INPUT_PULLUP); // D29: Normally LOW, emergency HIGH (external pulldown) pinMode(EMERGENCY_LASER_SENSORS, INPUT_PULLUP); // D32: Normally HIGH, emergency LOW (external pullup) pinMode(RADIO_SPEED_UP, INPUT_PULLUP); pinMode(RADIO_SPEED_DOWN, INPUT_PULLUP); pinMode(RADIO_START_STOP, INPUT_PULLUP); pinMode(RADIO_MODE_SELECT, INPUT_PULLUP); pinMode(RADIO_RESET, INPUT_PULLUP); pinMode(RADIO_DECREASE, INPUT_PULLUP); pinMode(RADIO_INCREASE, INPUT_PULLUP); pinMode(LIMIT_SW_SIGNAL, INPUT_PULLUP); // D33: Normally LOW, emergency HIGH (external pulldown) pinMode(ENCODER_A, INPUT_PULLUP); pinMode(ENCODER_B, INPUT_PULLUP); pinMode(PWM_OUT, OUTPUT); analogWrite(PWM_OUT, 0); pinMode(ENABLE_OUT, OUTPUT); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled pinMode(LED_GREEN, OUTPUT); pinMode(LED_RED, OUTPUT); pinMode(LED_EMERGENCY, OUTPUT); // Initialize encoder interrupts // Encoder: LPD3806-360BM-G5-24C // - 360 pulses per revolution // - Quadrature output (A and B phases) // - NPN open collector (requires INPUT_PULLUP) // - With quadrature decoding: 1440 counts per revolution encoderLastState = (digitalRead(ENCODER_A) << 1) | digitalRead(ENCODER_B); attachInterrupt(digitalPinToInterrupt(ENCODER_A), encoderInterruptA, CHANGE); attachInterrupt(digitalPinToInterrupt(ENCODER_B), encoderInterruptB, CHANGE); // Initialize LCD (try common I2C addresses) Serial.println("Initializing LCD..."); // Try init() first (most common method) lcd.init(); delay(200); // Give LCD time to initialize lcd.backlight(); delay(100); lcd.clear(); delay(100); // Initialize custom Russian characters (8 slots for unique Russian letters) // Must be done after clear() but before first use initCustomChars(); // Reinitialize after clear delay(50); // Display welcome message in Russian (using hybrid Latin/Russian approach) lcd.setCursor(0, 0); lcdPrintRussianText("БЕСКОНЕЧНАЯ"); lcd.setCursor(0, 1); lcdPrintRussianText("ЛЕСТНИЦА"); delay(100); // Give time for characters to display Serial.println("LCD initialization complete"); // Note: If LCD doesn't work, try changing LCD_ADDRESS to 0x3F // Initialize LED matrix (all modules) for (int i = 0; i < MATRIX_MODULES; i++) { matrix.shutdown(i, false); matrix.setIntensity(i, 8); matrix.clearDisplay(i); } // Display welcome message on matrix displayWelcome(); // Initialize button states lastStartBtnState = digitalRead(BTN_START); lastStopBtnState = digitalRead(BTN_STOP); lastSpeedUpBtnState = digitalRead(BTN_SPEED_UP); lastSpeedDownBtnState = digitalRead(BTN_SPEED_DOWN); lastModeSelectBtnState = digitalRead(BTN_MODE_SELECT); lastIncreaseBtnState = digitalRead(BTN_INCREASE); lastDecreaseBtnState = digitalRead(BTN_DECREASE); lastResetParamsBtnState = digitalRead(BTN_RESET_PARAMS); lastEmergencyBtnState = digitalRead(BTN_EMERGENCY_MUSHROOM); lastRadioSpeedUpBtnState = digitalRead(RADIO_SPEED_UP); lastRadioSpeedDownBtnState = digitalRead(RADIO_SPEED_DOWN); lastRadioStartStopBtnState = digitalRead(RADIO_START_STOP); lastRadioModeSelectBtnState = digitalRead(RADIO_MODE_SELECT); lastRadioResetBtnState = digitalRead(RADIO_RESET); lastRadioDecreaseBtnState = digitalRead(RADIO_DECREASE); lastRadioIncreaseBtnState = digitalRead(RADIO_INCREASE); // Initialize exercise parameters exerciseParams.mode = MODE_TIME; // Will be set by loadSettingsFromEEPROM // Load settings from EEPROM (or use defaults if first time) // This will also set exerciseMode and speedPercent loadSettingsFromEEPROM(); exerciseProgress.startTime = 0; exerciseProgress.pausedTime = 0; exerciseProgress.totalPausedTime = 0; exerciseProgress.encoderCount = 0; exerciseProgress.distanceTraveled = 0.0; exerciseProgress.isPaused = false; // Set initial LED states digitalWrite(LED_RED, HIGH); digitalWrite(LED_GREEN, LOW); digitalWrite(LED_EMERGENCY, LOW); Serial.print("Концевик: "); Serial.println(digitalRead(LIMIT_SW_SIGNAL) == HIGH ? "РАЗОМКНУТ (АВАРИЯ)" : "ЗАМКНУТ (НОРМА)"); Serial.print("Стартовая мощность: "); Serial.print(speedPercent); Serial.println("%"); Serial.println("Система готова"); Serial.println("-------------------"); // Update LCD with initial speed updateLCDSettings(); } // ============================================================================ // MAIN LOOP // ============================================================================ void loop() { unsigned long currentMillis = millis(); // Status printing (every 1 second) static unsigned long lastStatusPrint = 0; if (currentMillis - lastStatusPrint > 1000) { printStatus(); lastStatusPrint = currentMillis; } // Safety checks if (!emergency) { checkSafety(); checkEmergencyButton(currentMillis); checkEmergencyLaserSensors(currentMillis); } else { emergencyHandler(currentMillis); } // State machine update if (!emergency) { updateExerciseState(currentMillis); } // Mode and parameter setting (in IDLE, MODE_SELECTED, or READY states) // STATE_READY allows continued parameter adjustment before starting exercise // Mode is always TIME or DISTANCE (no MODE_NONE), so we can always check parameters if (exerciseState == STATE_IDLE || exerciseState == STATE_MODE_SELECTED || exerciseState == STATE_READY) { checkModeButtons(currentMillis); checkParameterButtons(currentMillis); } // Exercise tracking (only in RUNNING state) if (exerciseState == STATE_RUNNING) { trackExercise(currentMillis); checkExerciseCompletion(currentMillis); } // Countdown handling if (exerciseState == STATE_COUNTDOWN) { handleCountdown(currentMillis); } // Display updates if (currentMillis - lastDisplayUpdate >= DISPLAY_UPDATE_INTERVAL) { updateDisplays(currentMillis); lastDisplayUpdate = currentMillis; } // Button checks if (!emergency) { checkButtons(currentMillis); checkRadioButtons(currentMillis); checkSpeedButtons(currentMillis); } } // ============================================================================ // STATE MACHINE // ============================================================================ void updateExerciseState(unsigned long currentMillis) { // State transitions are handled by specific functions // This function can be used for state-specific periodic updates if needed } // ============================================================================ // MODE SELECTION // ============================================================================ void checkModeButtons(unsigned long currentMillis) { // Local mode select button (D22 - momentary button, not toggle/latching) // Toggle between TIME and DISTANCE modes only bool currentModeSelectBtnState = digitalRead(BTN_MODE_SELECT); // Detect button press (falling edge) - record time for debouncing if (currentModeSelectBtnState == LOW && lastModeSelectBtnState == HIGH) { modeSelectButtonPressTime = currentMillis; modeSelectButtonProcessed = false; // Reset processed flag on new press } // Process button press after debounce delay (non-blocking) // Only process once per press (when press time is set, debounce has passed, and not yet processed) if (!modeSelectButtonProcessed && modeSelectButtonPressTime > 0 && (currentMillis - modeSelectButtonPressTime) >= DEBOUNCE_DELAY && currentModeSelectBtnState == LOW) { // Re-read to confirm button is still pressed after debounce if (digitalRead(BTN_MODE_SELECT) == LOW) { // Toggle between TIME and DISTANCE modes if (exerciseMode == MODE_TIME) { exerciseMode = MODE_DISTANCE; exerciseState = STATE_READY; // Ready if distance is already set if (exerciseParams.distanceMeters == 0) { exerciseState = STATE_MODE_SELECTED; } Serial.println("Режим: ДИСТАНЦИЯ"); } else { exerciseMode = MODE_TIME; exerciseState = STATE_READY; // Ready if time is already set if (exerciseParams.timeMinutes == 0 && exerciseParams.timeSeconds == 0) { exerciseState = STATE_MODE_SELECTED; } Serial.println("Режим: ВРЕМЯ"); } saveSettingsToEEPROM(); // Save mode to EEPROM updateLCDSettings(); // Mark as processed to prevent multiple triggers during same press modeSelectButtonProcessed = true; } } // Detect button release (rising edge) - reset everything if (currentModeSelectBtnState == HIGH && lastModeSelectBtnState == LOW) { modeSelectButtonPressTime = 0; modeSelectButtonProcessed = false; } lastModeSelectBtnState = currentModeSelectBtnState; // Radio mode select button (D43 - pulse button, needs debounce) bool currentRadioModeSelectBtnState = digitalRead(RADIO_MODE_SELECT); if (currentRadioModeSelectBtnState == LOW && lastRadioModeSelectBtnState == HIGH) { delay(DEBOUNCE_DELAY); // Toggle between TIME and DISTANCE modes if (exerciseMode == MODE_TIME) { exerciseMode = MODE_DISTANCE; exerciseState = STATE_READY; // Ready if distance is already set if (exerciseParams.distanceMeters == 0) { exerciseState = STATE_MODE_SELECTED; } Serial.println("Радио: Режим ДИСТАНЦИЯ"); } else { exerciseMode = MODE_TIME; exerciseState = STATE_READY; // Ready if time is already set if (exerciseParams.timeMinutes == 0 && exerciseParams.timeSeconds == 0) { exerciseState = STATE_MODE_SELECTED; } Serial.println("Радио: Режим ВРЕМЯ"); } saveSettingsToEEPROM(); // Save mode to EEPROM updateLCDSettings(); } lastRadioModeSelectBtnState = currentRadioModeSelectBtnState; } // ============================================================================ // PARAMETER SETTING // ============================================================================ void checkParameterButtons(unsigned long currentMillis) { if (exerciseMode == MODE_TIME) { // Time mode: Increase = +10 seconds, Decrease = -10 seconds // Increase button (+10 seconds) bool currentIncreaseBtnState = digitalRead(BTN_INCREASE); if (currentIncreaseBtnState == LOW && lastIncreaseBtnState == HIGH) { // Button just pressed - immediate action increasePressTime = currentMillis; lastIncreaseRepeat = currentMillis; increaseButtonProcessed = false; exerciseParams.timeSeconds += 10; if (exerciseParams.timeSeconds >= 60) { exerciseParams.timeMinutes++; exerciseParams.timeSeconds -= 60; } exerciseState = STATE_READY; Serial.print("Время: "); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.println(exerciseParams.timeSeconds); saveSettingsToEEPROM(); updateLCDSettings(); } else if (currentIncreaseBtnState == LOW) { // Button held - check for auto-repeat if (currentMillis - increasePressTime > REPEAT_DELAY) { if (currentMillis - lastIncreaseRepeat >= REPEAT_INTERVAL) { lastIncreaseRepeat = currentMillis; exerciseParams.timeSeconds += 10; if (exerciseParams.timeSeconds >= 60) { exerciseParams.timeMinutes++; exerciseParams.timeSeconds -= 60; } exerciseState = STATE_READY; Serial.print("Время: "); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.println(exerciseParams.timeSeconds); saveSettingsToEEPROM(); updateLCDSettings(); } } } else { // Button released increasePressTime = 0; lastIncreaseRepeat = 0; increaseButtonProcessed = false; } lastIncreaseBtnState = currentIncreaseBtnState; // Decrease button (-10 seconds) bool currentDecreaseBtnState = digitalRead(BTN_DECREASE); if (currentDecreaseBtnState == LOW && lastDecreaseBtnState == HIGH) { // Button just pressed - immediate action decreasePressTime = currentMillis; lastDecreaseRepeat = currentMillis; decreaseButtonProcessed = false; if (exerciseParams.timeSeconds >= 10) { exerciseParams.timeSeconds -= 10; } else { // Need to borrow from minutes if (exerciseParams.timeMinutes > 0) { exerciseParams.timeMinutes--; exerciseParams.timeSeconds += 50; // 60 - 10 = 50 } else { // Can't go below 0:00 exerciseParams.timeSeconds = 0; } } exerciseState = STATE_READY; Serial.print("Время: "); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.println(exerciseParams.timeSeconds); saveSettingsToEEPROM(); updateLCDSettings(); } else if (currentDecreaseBtnState == LOW) { // Button held - check for auto-repeat if (currentMillis - decreasePressTime > REPEAT_DELAY) { if (currentMillis - lastDecreaseRepeat >= REPEAT_INTERVAL) { lastDecreaseRepeat = currentMillis; if (exerciseParams.timeSeconds >= 10) { exerciseParams.timeSeconds -= 10; } else { // Need to borrow from minutes if (exerciseParams.timeMinutes > 0) { exerciseParams.timeMinutes--; exerciseParams.timeSeconds += 50; // 60 - 10 = 50 } else { // Can't go below 0:00 exerciseParams.timeSeconds = 0; } } exerciseState = STATE_READY; Serial.print("Время: "); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.println(exerciseParams.timeSeconds); saveSettingsToEEPROM(); updateLCDSettings(); } } } else { // Button released decreasePressTime = 0; lastDecreaseRepeat = 0; decreaseButtonProcessed = false; } lastDecreaseBtnState = currentDecreaseBtnState; // Radio increase (+10 seconds) bool currentRadioIncreaseBtnState = digitalRead(RADIO_INCREASE); if (currentRadioIncreaseBtnState == LOW && lastRadioIncreaseBtnState == HIGH) { delay(DEBOUNCE_DELAY); exerciseParams.timeSeconds += 10; if (exerciseParams.timeSeconds >= 60) { exerciseParams.timeMinutes++; exerciseParams.timeSeconds -= 60; } exerciseState = STATE_READY; Serial.print("Радио: Время "); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.println(exerciseParams.timeSeconds); saveSettingsToEEPROM(); updateLCDSettings(); } lastRadioIncreaseBtnState = currentRadioIncreaseBtnState; // Radio decrease (-10 seconds) bool currentRadioDecreaseBtnState = digitalRead(RADIO_DECREASE); if (currentRadioDecreaseBtnState == LOW && lastRadioDecreaseBtnState == HIGH) { delay(DEBOUNCE_DELAY); if (exerciseParams.timeSeconds >= 10) { exerciseParams.timeSeconds -= 10; } else { // Need to borrow from minutes if (exerciseParams.timeMinutes > 0) { exerciseParams.timeMinutes--; exerciseParams.timeSeconds += 50; // 60 - 10 = 50 } else { // Can't go below 0:00 exerciseParams.timeSeconds = 0; } } exerciseState = STATE_READY; Serial.print("Радио: Время "); Serial.print(exerciseParams.timeMinutes); Serial.print(":"); if (exerciseParams.timeSeconds < 10) Serial.print("0"); Serial.println(exerciseParams.timeSeconds); saveSettingsToEEPROM(); updateLCDSettings(); } lastRadioDecreaseBtnState = currentRadioDecreaseBtnState; } else if (exerciseMode == MODE_DISTANCE) { // Distance mode: Increase = +5m, Decrease = -5m // Increase button (+5m) bool currentIncreaseBtnState = digitalRead(BTN_INCREASE); if (currentIncreaseBtnState == LOW && lastIncreaseBtnState == HIGH) { // Button just pressed - immediate action increasePressTime = currentMillis; lastIncreaseRepeat = currentMillis; increaseButtonProcessed = false; exerciseParams.distanceMeters += 5; exerciseState = STATE_READY; Serial.print("Дистанция: "); Serial.print(exerciseParams.distanceMeters); Serial.println(" м"); saveSettingsToEEPROM(); updateLCDSettings(); } else if (currentIncreaseBtnState == LOW) { // Button held - check for auto-repeat if (currentMillis - increasePressTime > REPEAT_DELAY) { if (currentMillis - lastIncreaseRepeat >= REPEAT_INTERVAL) { lastIncreaseRepeat = currentMillis; exerciseParams.distanceMeters += 5; exerciseState = STATE_READY; Serial.print("Дистанция: "); Serial.print(exerciseParams.distanceMeters); Serial.println(" м"); saveSettingsToEEPROM(); updateLCDSettings(); } } } else { // Button released increasePressTime = 0; lastIncreaseRepeat = 0; increaseButtonProcessed = false; } lastIncreaseBtnState = currentIncreaseBtnState; // Decrease button (-5m) bool currentDecreaseBtnState = digitalRead(BTN_DECREASE); if (currentDecreaseBtnState == LOW && lastDecreaseBtnState == HIGH) { // Button just pressed - immediate action decreasePressTime = currentMillis; lastDecreaseRepeat = currentMillis; decreaseButtonProcessed = false; exerciseParams.distanceMeters -= 5; if (exerciseParams.distanceMeters < 0) { exerciseParams.distanceMeters = 0; } exerciseState = STATE_READY; Serial.print("Дистанция: "); Serial.print(exerciseParams.distanceMeters); Serial.println(" м"); saveSettingsToEEPROM(); updateLCDSettings(); } else if (currentDecreaseBtnState == LOW) { // Button held - check for auto-repeat if (currentMillis - decreasePressTime > REPEAT_DELAY) { if (currentMillis - lastDecreaseRepeat >= REPEAT_INTERVAL) { lastDecreaseRepeat = currentMillis; exerciseParams.distanceMeters -= 5; if (exerciseParams.distanceMeters < 0) { exerciseParams.distanceMeters = 0; } exerciseState = STATE_READY; Serial.print("Дистанция: "); Serial.print(exerciseParams.distanceMeters); Serial.println(" м"); saveSettingsToEEPROM(); updateLCDSettings(); } } } else { // Button released decreasePressTime = 0; lastDecreaseRepeat = 0; decreaseButtonProcessed = false; } lastDecreaseBtnState = currentDecreaseBtnState; // Radio increase (+100m) bool currentRadioIncreaseBtnState = digitalRead(RADIO_INCREASE); if (currentRadioIncreaseBtnState == LOW && lastRadioIncreaseBtnState == HIGH) { delay(DEBOUNCE_DELAY); exerciseParams.distanceMeters += 5; exerciseState = STATE_READY; Serial.print("Радио: Дистанция "); Serial.print(exerciseParams.distanceMeters); Serial.println(" м"); saveSettingsToEEPROM(); updateLCDSettings(); } lastRadioIncreaseBtnState = currentRadioIncreaseBtnState; // Radio decrease (-100m) bool currentRadioDecreaseBtnState = digitalRead(RADIO_DECREASE); if (currentRadioDecreaseBtnState == LOW && lastRadioDecreaseBtnState == HIGH) { delay(DEBOUNCE_DELAY); exerciseParams.distanceMeters -= 5; if (exerciseParams.distanceMeters < 0) { exerciseParams.distanceMeters = 0; } exerciseState = STATE_READY; Serial.print("Радио: Дистанция "); Serial.print(exerciseParams.distanceMeters); Serial.println(" м"); saveSettingsToEEPROM(); updateLCDSettings(); } lastRadioDecreaseBtnState = currentRadioDecreaseBtnState; } // Reset parameters button bool currentResetParamsBtnState = digitalRead(BTN_RESET_PARAMS); if (currentResetParamsBtnState == LOW && lastResetParamsBtnState == HIGH) { delay(DEBOUNCE_DELAY); if (exerciseMode == MODE_TIME) { exerciseParams.timeMinutes = DEFAULT_TIME_MINUTES; exerciseParams.timeSeconds = DEFAULT_TIME_SECONDS; exerciseState = STATE_READY; // Defaults are valid, ready to start Serial.println("Время сброшено до значений по умолчанию"); saveSettingsToEEPROM(); } else if (exerciseMode == MODE_DISTANCE) { exerciseParams.distanceMeters = DEFAULT_DISTANCE_METERS; exerciseState = STATE_READY; // Defaults are valid, ready to start Serial.println("Дистанция сброшена до значения по умолчанию"); saveSettingsToEEPROM(); } updateLCDSettings(); } lastResetParamsBtnState = currentResetParamsBtnState; // Radio reset button (resets exercise if running/paused, or parameters if in setup) bool currentRadioResetBtnState = digitalRead(RADIO_RESET); if (currentRadioResetBtnState == LOW && lastRadioResetBtnState == HIGH) { delay(DEBOUNCE_DELAY); if (exerciseState == STATE_PAUSED) { resetExercise(); Serial.println("Радио: Упражнение сброшено"); } else if (exerciseMode == MODE_TIME) { exerciseParams.timeMinutes = DEFAULT_TIME_MINUTES; exerciseParams.timeSeconds = DEFAULT_TIME_SECONDS; exerciseState = STATE_READY; // Defaults are valid, ready to start Serial.println("Радио: Время сброшено до значений по умолчанию"); saveSettingsToEEPROM(); } else if (exerciseMode == MODE_DISTANCE) { exerciseParams.distanceMeters = DEFAULT_DISTANCE_METERS; exerciseState = STATE_READY; // Defaults are valid, ready to start Serial.println("Радио: Дистанция сброшена до значения по умолчанию"); saveSettingsToEEPROM(); } updateLCDSettings(); } lastRadioResetBtnState = currentRadioResetBtnState; } // ============================================================================ // EXERCISE CONTROL // ============================================================================ void startExercise() { if (exerciseState != STATE_READY && exerciseState != STATE_IDLE && exerciseState != STATE_MODE_SELECTED) return; if (emergency) return; // Check if parameters are set if (exerciseMode == MODE_TIME) { if (exerciseParams.timeMinutes == 0 && exerciseParams.timeSeconds == 0) { Serial.println("Установите время!"); return; } } else if (exerciseMode == MODE_DISTANCE) { if (exerciseParams.distanceMeters == 0) { Serial.println("Установите дистанцию!"); return; } } else { // If mode is somehow NONE, default to time mode exerciseMode = MODE_TIME; if (exerciseParams.timeMinutes == 0 && exerciseParams.timeSeconds == 0) { exerciseParams.timeMinutes = DEFAULT_TIME_MINUTES; exerciseParams.timeSeconds = DEFAULT_TIME_SECONDS; saveSettingsToEEPROM(); } } // Check limit switch bool limitTripped = (digitalRead(LIMIT_SW_SIGNAL) == HIGH); if (limitTripped) { Serial.println("Не могу стартовать: концевик разомкнут!"); return; } // Reset progress exerciseProgress.startTime = 0; exerciseProgress.pausedTime = 0; exerciseProgress.totalPausedTime = 0; exerciseProgress.encoderCount = 0; exerciseProgress.distanceTraveled = 0.0; exerciseProgress.isPaused = false; // Reset all display caches so displays will redraw properly on start resetTimeDisplayCacheFlag = true; resetSpeedDisplayCacheFlag = true; resetDistanceDisplayCacheFlag = true; // Start countdown exerciseState = STATE_COUNTDOWN; countdownValue = 3; countdownStartTime = millis(); digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_RED, LOW); Serial.println(">>> НАЧАЛО ОТСЧЕТА"); } void pauseExercise() { if (exerciseState != STATE_RUNNING) return; exerciseState = STATE_PAUSED; exerciseProgress.isPaused = true; exerciseProgress.pausedTime = millis(); // Reset all display caches so displays will redraw properly when resumed resetTimeDisplayCacheFlag = true; resetSpeedDisplayCacheFlag = true; resetDistanceDisplayCacheFlag = true; analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, HIGH); Serial.println("<<< УПРАЖНЕНИЕ НА ПАУЗЕ"); } void resumeExercise() { if (exerciseState != STATE_PAUSED) return; if (emergency) return; // Update total paused time if (exerciseProgress.pausedTime > 0) { exerciseProgress.totalPausedTime += (millis() - exerciseProgress.pausedTime); exerciseProgress.pausedTime = 0; } // Reset all display caches so displays will redraw properly (colon, speed, distance, etc.) resetTimeDisplayCacheFlag = true; resetSpeedDisplayCacheFlag = true; resetDistanceDisplayCacheFlag = true; exerciseState = STATE_RUNNING; exerciseProgress.isPaused = false; digitalWrite(ENABLE_OUT, LOW); // Inverted: LOW = enabled digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_RED, LOW); updateMotorSpeed(); Serial.println(">>> УПРАЖНЕНИЕ ВОЗОБНОВЛЕНО"); } void resetExercise() { exerciseState = STATE_IDLE; // Keep current mode, don't reset to MODE_NONE // Reload parameters from EEPROM (restore saved settings) loadSettingsFromEEPROM(); exerciseProgress.startTime = 0; exerciseProgress.pausedTime = 0; exerciseProgress.totalPausedTime = 0; exerciseProgress.encoderCount = 0; exerciseProgress.distanceTraveled = 0.0; exerciseProgress.isPaused = false; // Reset all display caches so displays will redraw properly on next start resetTimeDisplayCacheFlag = true; resetSpeedDisplayCacheFlag = true; resetDistanceDisplayCacheFlag = true; analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, HIGH); updateLCDSettings(); clearAllMatrix(); Serial.println("<<< УПРАЖНЕНИЕ СБРОШЕНО"); } void completeExercise() { // Set state to READY so user can start immediately exerciseState = STATE_READY; // Reset all display caches so displays will redraw properly on next start resetTimeDisplayCacheFlag = true; resetSpeedDisplayCacheFlag = true; resetDistanceDisplayCacheFlag = true; analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, HIGH); Serial.println(">>> УПРАЖНЕНИЕ ЗАВЕРШЕНО"); // Display completion message displayCompletion(); } // ============================================================================ // COUNTDOWN SYSTEM // ============================================================================ void handleCountdown(unsigned long currentMillis) { static unsigned long lastCountdownUpdate = 0; static bool firstCountdownDisplay = true; // Display initial countdown value (3) immediately on first call if (firstCountdownDisplay) { lastCountdownUpdate = currentMillis; firstCountdownDisplay = false; displayCountdown(countdownValue); // Display "3" countdownValue--; // Decrement so next interval shows "2" return; } // Check if it's time for next countdown step if (currentMillis - lastCountdownUpdate >= COUNTDOWN_INTERVAL) { lastCountdownUpdate = currentMillis; if (countdownValue > 0) { displayCountdown(countdownValue); // Display "2" or "1" countdownValue--; } else if (countdownValue == 0) { // Display "ПУСК" and start exercise displayCountdown(0); // Display "ПУСК" // Wait 0.5 seconds for "ПУСК" display delay(500); exerciseState = STATE_RUNNING; exerciseProgress.startTime = millis(); exerciseProgress.isPaused = false; // Reset all display caches so displays will redraw properly when exercise starts resetTimeDisplayCacheFlag = true; resetSpeedDisplayCacheFlag = true; resetDistanceDisplayCacheFlag = true; digitalWrite(ENABLE_OUT, LOW); // Inverted: LOW = enabled updateMotorSpeed(); // Reset static variable for next countdown sequence firstCountdownDisplay = true; Serial.println(">>> УПРАЖНЕНИЕ НАЧАЛОСЬ"); } } } // ============================================================================ // EXERCISE TRACKING // ============================================================================ // Calculate elapsed time accounting for all pause periods (including current pause) unsigned long getElapsedTime(unsigned long currentMillis) { if (exerciseProgress.startTime == 0) { return 0; } unsigned long elapsedTime = currentMillis - exerciseProgress.startTime - exerciseProgress.totalPausedTime; // If currently paused, subtract the current pause period if (exerciseState == STATE_PAUSED && exerciseProgress.pausedTime > 0) { elapsedTime -= (currentMillis - exerciseProgress.pausedTime); } return elapsedTime; } void trackExercise(unsigned long currentMillis) { if (exerciseProgress.startTime == 0) { exerciseProgress.startTime = currentMillis; } // Calculate distance from encoder // LPD3806-360BM-G5-24C: 1440 counts per revolution (quadrature decoding) long currentEncoderCount = exerciseProgress.encoderCount; exerciseProgress.distanceTraveled = abs(currentEncoderCount) * ENCODER_METERS_PER_COUNT; } void checkExerciseCompletion(unsigned long currentMillis) { if (exerciseMode == MODE_TIME) { // Check if time elapsed unsigned long elapsedTime = getElapsedTime(currentMillis); unsigned long targetTime = (exerciseParams.timeMinutes * 60000UL) + (exerciseParams.timeSeconds * 1000UL); if (elapsedTime >= targetTime) { completeExercise(); } } else if (exerciseMode == MODE_DISTANCE) { // Check if distance reached if (exerciseProgress.distanceTraveled >= exerciseParams.distanceMeters) { completeExercise(); } } } // ============================================================================ // BUTTON HANDLERS // ============================================================================ void checkButtons(unsigned long currentMillis) { // Start button (also works as resume when paused) bool currentStartBtnState = digitalRead(BTN_START); if (currentStartBtnState == LOW && lastStartBtnState == HIGH) { delay(DEBOUNCE_DELAY); if (exerciseState == STATE_READY || exerciseState == STATE_IDLE || exerciseState == STATE_MODE_SELECTED) { startExercise(); } else if (exerciseState == STATE_PAUSED) { resumeExercise(); } } lastStartBtnState = currentStartBtnState; // Stop button (pause or reset) bool currentStopBtnState = digitalRead(BTN_STOP); if (currentStopBtnState == LOW && lastStopBtnState == HIGH) { delay(DEBOUNCE_DELAY); if (exerciseState == STATE_RUNNING) { pauseExercise(); } else if (exerciseState == STATE_PAUSED) { // If paused, reset exercise resetExercise(); } } lastStopBtnState = currentStopBtnState; } void checkRadioButtons(unsigned long currentMillis) { // Radio start/stop toggle bool currentRadioStartStopBtnState = digitalRead(RADIO_START_STOP); if (currentRadioStartStopBtnState != lastRadioStartStopBtnState) { delay(DEBOUNCE_DELAY); if (currentRadioStartStopBtnState == LOW && lastRadioStartStopBtnState == HIGH) { // Button pressed if (exerciseState == STATE_READY || exerciseState == STATE_IDLE || exerciseState == STATE_MODE_SELECTED) { startExercise(); } else if (exerciseState == STATE_RUNNING) { pauseExercise(); } else if (exerciseState == STATE_PAUSED) { resumeExercise(); } } } lastRadioStartStopBtnState = currentRadioStartStopBtnState; // Note: Radio reset is handled in checkParameterButtons() for parameter reset // and here for exercise reset bool currentRadioResetBtnState = digitalRead(RADIO_RESET); if (currentRadioResetBtnState == LOW && lastRadioResetBtnState == HIGH) { delay(DEBOUNCE_DELAY); if (exerciseState == STATE_PAUSED) { resetExercise(); Serial.println("Радио: Упражнение сброшено"); } } lastRadioResetBtnState = currentRadioResetBtnState; } void checkSpeedButtons(unsigned long currentMillis) { // Local speed buttons (with auto-repeat on hold) bool currentSpeedUpBtnState = digitalRead(BTN_SPEED_UP); if (currentSpeedUpBtnState == LOW && lastSpeedUpBtnState == HIGH) { // Button just pressed - immediate action speedUpPressTime = currentMillis; lastSpeedUpRepeat = currentMillis; if (!emergency) { increaseSpeed(); } } else if (currentSpeedUpBtnState == LOW) { // Button held - auto-repeat if (currentMillis - speedUpPressTime > REPEAT_DELAY) { // After initial delay, repeat at regular intervals if (currentMillis - lastSpeedUpRepeat >= REPEAT_INTERVAL) { lastSpeedUpRepeat = currentMillis; if (!emergency) { increaseSpeed(); } } } } else { // Button released speedUpPressTime = 0; lastSpeedUpRepeat = 0; } lastSpeedUpBtnState = currentSpeedUpBtnState; bool currentSpeedDownBtnState = digitalRead(BTN_SPEED_DOWN); if (currentSpeedDownBtnState == LOW && lastSpeedDownBtnState == HIGH) { // Button just pressed - immediate action speedDownPressTime = currentMillis; lastSpeedDownRepeat = currentMillis; if (!emergency) { decreaseSpeed(); } } else if (currentSpeedDownBtnState == LOW) { // Button held - auto-repeat if (currentMillis - speedDownPressTime > REPEAT_DELAY) { // After initial delay, repeat at regular intervals if (currentMillis - lastSpeedDownRepeat >= REPEAT_INTERVAL) { lastSpeedDownRepeat = currentMillis; if (!emergency) { decreaseSpeed(); } } } } else { // Button released speedDownPressTime = 0; lastSpeedDownRepeat = 0; } lastSpeedDownBtnState = currentSpeedDownBtnState; // Radio speed buttons (with auto-repeat) bool currentRadioSpeedUpBtnState = digitalRead(RADIO_SPEED_UP); if (currentRadioSpeedUpBtnState == LOW) { if (radioSpeedUpPressTime == 0) { radioSpeedUpPressTime = currentMillis; if (!emergency) { increaseSpeed(); } } else if (currentMillis - radioSpeedUpPressTime > REPEAT_DELAY) { if ((currentMillis - radioSpeedUpPressTime - REPEAT_DELAY) % REPEAT_INTERVAL == 0) { if (!emergency) { increaseSpeed(); } } } } else { radioSpeedUpPressTime = 0; } lastRadioSpeedUpBtnState = currentRadioSpeedUpBtnState; bool currentRadioSpeedDownBtnState = digitalRead(RADIO_SPEED_DOWN); if (currentRadioSpeedDownBtnState == LOW) { if (radioSpeedDownPressTime == 0) { radioSpeedDownPressTime = currentMillis; if (!emergency) { decreaseSpeed(); } } else if (currentMillis - radioSpeedDownPressTime > REPEAT_DELAY) { if ((currentMillis - radioSpeedDownPressTime - REPEAT_DELAY) % REPEAT_INTERVAL == 0) { if (!emergency) { decreaseSpeed(); } } } } else { radioSpeedDownPressTime = 0; } lastRadioSpeedDownBtnState = currentRadioSpeedDownBtnState; } // ============================================================================ // SPEED CONTROL // ============================================================================ void increaseSpeed() { if (emergency || speedPercent >= SPEED_MAX) return; speedPercent += SPEED_STEP; if (speedPercent > SPEED_MAX) speedPercent = SPEED_MAX; // Save speed to EEPROM saveSettingsToEEPROM(); if (exerciseState == STATE_RUNNING) { updateMotorSpeed(); } updateLCDSettings(); } void decreaseSpeed() { if (emergency || speedPercent <= SPEED_MIN) return; speedPercent -= SPEED_STEP; if (speedPercent < SPEED_MIN) speedPercent = SPEED_MIN; // Save speed to EEPROM saveSettingsToEEPROM(); if (exerciseState == STATE_RUNNING) { updateMotorSpeed(); } updateLCDSettings(); } void updateMotorSpeed() { if (exerciseState != STATE_RUNNING || emergency) { analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled return; } int pwmValue = map(speedPercent, 0, 100, 0, 255); analogWrite(PWM_OUT, pwmValue); } // ============================================================================ // SAFETY AND EMERGENCY // ============================================================================ void checkSafety() { // D33: Normally LOW, emergency HIGH (HIGH = triggered) bool limitTripped = (digitalRead(LIMIT_SW_SIGNAL) == HIGH); if (limitTripped && !emergency) { emergency = true; exerciseState = STATE_EMERGENCY; analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, LOW); digitalWrite(LED_EMERGENCY, HIGH); // Initialize matrix flash state and start flashing immediately emergencyMatrixFlashState = true; lastEmergencyMatrixFlash = millis(); // Turn on all LEDs immediately for (int module = 0; module < MATRIX_MODULES; module++) { for (int row = 0; row < 8; row++) { matrix.setRow(module, row, 0xFF); // All pixels ON } } Serial.println("АВАРИЯ: Концевик разомкнут !!!"); Serial.println("Для сброса: замкнуть концевики и нажать СТОП"); } } void checkEmergencyButton(unsigned long currentMillis) { bool currentEmergencyBtnState = digitalRead(BTN_EMERGENCY_MUSHROOM); // D29: Normally LOW, emergency HIGH (HIGH = pressed/triggered) if (currentEmergencyBtnState == HIGH && lastEmergencyBtnState == LOW) { // Emergency button pressed (HIGH = emergency) delay(DEBOUNCE_DELAY); emergency = true; exerciseState = STATE_EMERGENCY; analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, LOW); digitalWrite(LED_EMERGENCY, HIGH); // Initialize matrix flash state and start flashing immediately emergencyMatrixFlashState = true; lastEmergencyMatrixFlash = millis(); // Turn on all LEDs immediately for (int module = 0; module < MATRIX_MODULES; module++) { for (int row = 0; row < 8; row++) { matrix.setRow(module, row, 0xFF); // All pixels ON } } Serial.println("АВАРИЯ: Нажат аварийный выключатель (Грибок) !!!"); } else if (currentEmergencyBtnState == LOW && lastEmergencyBtnState == HIGH) { // Emergency button released (LOW = normal, not pressed) delay(DEBOUNCE_DELAY); bool limitTripped = (digitalRead(LIMIT_SW_SIGNAL) == HIGH); bool laserTripped = (digitalRead(EMERGENCY_LASER_SENSORS) == LOW); if (!limitTripped && !laserTripped) { emergency = false; exerciseState = STATE_IDLE; digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, HIGH); digitalWrite(LED_EMERGENCY, LOW); // Clear matrix when emergency is reset clearAllMatrix(); emergencyMatrixFlashState = false; Serial.println("Авария сброшена (Грибок отпущен)"); Serial.println("Система в режиме СТОП"); } else { if (limitTripped) Serial.println("Концевик все еще разомкнут!"); if (laserTripped) Serial.println("Лазерные датчики все еще сработали!"); } } lastEmergencyBtnState = currentEmergencyBtnState; } void checkEmergencyLaserSensors(unsigned long currentMillis) { // D32: Normally HIGH, emergency LOW (LOW = triggered) bool laserTripped = (digitalRead(EMERGENCY_LASER_SENSORS) == LOW); if (laserTripped && !emergency) { emergency = true; exerciseState = STATE_EMERGENCY; analogWrite(PWM_OUT, 0); digitalWrite(ENABLE_OUT, HIGH); // Inverted: HIGH = disabled digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, LOW); digitalWrite(LED_EMERGENCY, HIGH); // Initialize matrix flash state and start flashing immediately emergencyMatrixFlashState = true; lastEmergencyMatrixFlash = millis(); // Turn on all LEDs immediately for (int module = 0; module < MATRIX_MODULES; module++) { for (int row = 0; row < 8; row++) { matrix.setRow(module, row, 0xFF); // All pixels ON } } Serial.println("АВАРИЯ: Сработали аварийные лазерные датчики !!!"); } } void emergencyHandler(unsigned long currentMillis) { // Blink emergency LED if (currentMillis - lastEmergencyBlink >= EMERGENCY_BLINK_INTERVAL) { emergencyBlinkState = !emergencyBlinkState; digitalWrite(LED_EMERGENCY, emergencyBlinkState); lastEmergencyBlink = currentMillis; } // Flash LED matrix full color in 2-second cycle (1 second on, 1 second off) if (currentMillis - lastEmergencyMatrixFlash >= EMERGENCY_MATRIX_FLASH_INTERVAL) { emergencyMatrixFlashState = !emergencyMatrixFlashState; if (emergencyMatrixFlashState) { // Turn on all LEDs on all modules (full color = all pixels lit) for (int module = 0; module < MATRIX_MODULES; module++) { for (int row = 0; row < 8; row++) { matrix.setRow(module, row, 0xFF); // 0xFF = all 8 pixels in row are ON } } } else { // Turn off all LEDs (clear matrix) clearAllMatrix(); } lastEmergencyMatrixFlash = currentMillis; } // Check for reset (STOP button) bool currentStopBtnState = digitalRead(BTN_STOP); if (currentStopBtnState == LOW && lastStopBtnState == HIGH) { delay(DEBOUNCE_DELAY); bool limitTripped = (digitalRead(LIMIT_SW_SIGNAL) == HIGH); // D33: Normally LOW, emergency HIGH bool emergencyBtnPressed = (digitalRead(BTN_EMERGENCY_MUSHROOM) == HIGH); // D29: Normally LOW, emergency HIGH bool laserTripped = (digitalRead(EMERGENCY_LASER_SENSORS) == LOW); // D32: Normally HIGH, emergency LOW if (!limitTripped && !emergencyBtnPressed && !laserTripped) { emergency = false; exerciseState = STATE_IDLE; digitalWrite(LED_GREEN, LOW); digitalWrite(LED_RED, HIGH); digitalWrite(LED_EMERGENCY, LOW); // Clear matrix when emergency is reset clearAllMatrix(); emergencyMatrixFlashState = false; Serial.println("Авария сброшена"); Serial.println("Система в режиме СТОП"); } else { if (limitTripped) Serial.println("Концевик все еще разомкнут!"); if (emergencyBtnPressed) Serial.println("Грибок все еще нажат!"); if (laserTripped) Serial.println("Лазерные датчики все еще сработали!"); } } lastStopBtnState = currentStopBtnState; } // ============================================================================ // DISPLAY FUNCTIONS // ============================================================================ void updateLCDSettings() { lcd.clear(); // Reinitialize custom characters after clear (some LCDs clear CGRAM on clear()) initCustomChars(); delay(10); // Small delay to ensure characters are loaded lcd.setCursor(0, 0); lcdPrintRussianText("Скорость: "); lcd.print(speedPercent); lcd.print("%"); lcd.setCursor(0, 1); if (exerciseMode == MODE_TIME) { lcdPrintRussianText("Время: "); lcd.print(exerciseParams.timeMinutes); lcd.print(":"); if (exerciseParams.timeSeconds < 10) lcd.print("0"); lcd.print(exerciseParams.timeSeconds); } else if (exerciseMode == MODE_DISTANCE) { lcdPrintRussianText("Дистанция: "); lcd.print(exerciseParams.distanceMeters); lcdPrintRussianText(" м"); } else { // Should not happen since we default to time mode, but just in case lcdPrintRussianText("Режим: ВРЕМЯ"); } } void displayWelcome() { // Display welcome message on matrix clearAllMatrix(); // Display "СТАРТ" (START) or first few letters const char* welcomeText = "СТАРТ"; displayText(welcomeText); delay(2000); clearAllMatrix(); } void displayCountdown(int value) { clearAllMatrix(); if (value > 0) { // Display countdown number (3, 2, 1) - show large in center int centerModule = MATRIX_MODULES / 2; displayChar('0' + value, centerModule); } else { // Display "ПУСК" (GO/START) const char* startText = "ПУСК"; displayText(startText); } } // displayNumber is now defined in font section above // displayText is now defined in font section above void displayCompletion() { clearAllMatrix(); // Display "ГОТОВО" (COMPLETE) or first few letters const char* completeText = "ГОТОВО"; displayText(completeText); delay(1000); // Show all LEDs briefly on all modules for (int module = 0; module < MATRIX_MODULES; module++) { for (int i = 0; i < 8; i++) { matrix.setRow(module, i, 0xFF); } } delay(500); clearAllMatrix(); } void updateDisplays(unsigned long currentMillis) { if (exerciseState == STATE_RUNNING) { // Always show remaining time/distance on matrix (per requirements) if (exerciseMode == MODE_TIME) { // Display remaining time unsigned long elapsedTime = getElapsedTime(currentMillis); unsigned long targetTime = (exerciseParams.timeMinutes * 60000UL) + (exerciseParams.timeSeconds * 1000UL); unsigned long remainingTime = (targetTime > elapsedTime) ? (targetTime - elapsedTime) : 0; unsigned long remainingSeconds = remainingTime / 1000; unsigned long remainingMinutes = remainingSeconds / 60; remainingSeconds = remainingSeconds % 60; // Display remaining time on modules 0-4 (MM:SS format = 5 modules) displayTimeMMSS(remainingMinutes, remainingSeconds); // Display speed on remaining modules (if enabled and we have enough modules) if (showSpeedOnMatrix && MATRIX_MODULES >= 8) { displaySpeedOnModules(speedPercent, 5); } else if (showSpeedOnMatrix && MATRIX_MODULES >= 6) { // If we have 6-7 modules, show speed on last 2-3 modules (compact) int speedStartModule = MATRIX_MODULES - 3; if (speedStartModule >= 5) { displaySpeedOnModules(speedPercent, speedStartModule); } } } else if (exerciseMode == MODE_DISTANCE) { // Display remaining distance float remainingDistance = exerciseParams.distanceMeters - exerciseProgress.distanceTraveled; if (remainingDistance < 0) remainingDistance = 0; // Display remaining distance (will use as many modules as needed) displayDistanceOnModules(remainingDistance, 0); // Display speed on last 3 modules (if enabled and we have enough modules) // Speed will overwrite distance if they overlap, but distance is more important if (showSpeedOnMatrix && MATRIX_MODULES >= 6) { int speedStartModule = MATRIX_MODULES - 3; if (speedStartModule >= 0) { displaySpeedOnModules(speedPercent, speedStartModule); } } } } else if (exerciseState == STATE_COUNTDOWN) { // Countdown is handled separately in handleCountdown() } else if (exerciseState == STATE_IDLE || exerciseState == STATE_MODE_SELECTED || exerciseState == STATE_READY) { clearAllMatrix(); } else if (exerciseState == STATE_PAUSED) { // Show stopped timer/distance when paused (same as running but frozen) if (exerciseMode == MODE_TIME) { // Display remaining time (frozen, timer is stopped) unsigned long elapsedTime = getElapsedTime(currentMillis); unsigned long targetTime = (exerciseParams.timeMinutes * 60000UL) + (exerciseParams.timeSeconds * 1000UL); unsigned long remainingTime = (targetTime > elapsedTime) ? (targetTime - elapsedTime) : 0; unsigned long remainingSeconds = remainingTime / 1000; unsigned long remainingMinutes = remainingSeconds / 60; remainingSeconds = remainingSeconds % 60; // Display remaining time on modules 0-4 (MM:SS format = 5 modules) displayTimeMMSS(remainingMinutes, remainingSeconds); // Display speed on remaining modules (if enabled and we have enough modules) if (showSpeedOnMatrix && MATRIX_MODULES >= 8) { displaySpeedOnModules(speedPercent, 5); } else if (showSpeedOnMatrix && MATRIX_MODULES >= 6) { // If we have 6-7 modules, show speed on last 2-3 modules (compact) int speedStartModule = MATRIX_MODULES - 3; if (speedStartModule >= 5) { displaySpeedOnModules(speedPercent, speedStartModule); } } } else if (exerciseMode == MODE_DISTANCE) { // Display remaining distance (frozen) float remainingDistance = exerciseParams.distanceMeters - exerciseProgress.distanceTraveled; if (remainingDistance < 0) remainingDistance = 0; // Display remaining distance (will use as many modules as needed) displayDistanceOnModules(remainingDistance, 0); // Display speed on last 3 modules (if enabled and we have enough modules) if (showSpeedOnMatrix && MATRIX_MODULES >= 6) { int speedStartModule = MATRIX_MODULES - 3; if (speedStartModule >= 0) { displaySpeedOnModules(speedPercent, speedStartModule); } } } } } void displayDistance(float distance) { // Display distance in meters on matrix using font (uses all available modules) displayDistanceOnModules(distance, 0); } void displayTime(unsigned long minutes, unsigned long seconds) { // Display time MM:SS on matrix using font (uses modules 0-4) displayTimeMMSS(minutes, seconds); } // Display distance starting from a specific module index void displayDistanceOnModules(float distance, int startModule) { if (startModule < 0 || startModule >= MATRIX_MODULES) return; int meters = (int)distance; int availableModules = MATRIX_MODULES - startModule; // Extract digits (up to available modules) int digits[8]; int digitCount = 0; if (meters == 0) { // Show "00" for zero digits[0] = 0; digits[1] = 0; digitCount = 2; } else { int temp = meters; while (temp > 0 && digitCount < availableModules) { digits[digitCount] = temp % 10; temp /= 10; digitCount++; } // Ensure minimum 2 digits (show leading zero for values < 10) if (digitCount == 1 && availableModules >= 2) { digits[1] = 0; // Leading zero digitCount = 2; } } // Cache last displayed digits per module static int lastDigits[8] = {-1, -1, -1, -1, -1, -1, -1, -1}; static int lastDigitCount = 0; // Reset cache if flag is set (called from pause/resume) if (resetDistanceDisplayCacheFlag) { for (int i = 0; i < 8; i++) { lastDigits[i] = -1; } lastDigitCount = 0; resetDistanceDisplayCacheFlag = false; // Clear modules to force full redraw for (int i = 0; i < availableModules && (startModule + i) < MATRIX_MODULES; i++) { matrix.clearDisplay(startModule + i); } } // Only update if digit count changed or digits changed bool needsUpdate = (digitCount != lastDigitCount); if (!needsUpdate) { for (int i = 0; i < digitCount; i++) { int digitIndex = digitCount - 1 - i; if (digits[digitIndex] != lastDigits[i]) { needsUpdate = true; break; } } } if (needsUpdate) { // Clear modules - include any extra modules from previous larger digit count int modulesToClear = (digitCount > lastDigitCount) ? digitCount : lastDigitCount; for (int i = 0; i < modulesToClear && (startModule + i) < MATRIX_MODULES; i++) { matrix.clearDisplay(startModule + i); } // Display digits left to right (most significant on left) for (int i = 0; i < digitCount && (startModule + i) < MATRIX_MODULES; i++) { int digitIndex = digitCount - 1 - i; // Get digit from most to least significant int moduleIndex = startModule + i; // Display from left to right displayChar('0' + digits[digitIndex], moduleIndex); lastDigits[i] = digits[digitIndex]; } lastDigitCount = digitCount; } } // Display speed on specific modules (3 digits for 0-100%) void displaySpeedOnModules(int speed, int startModule) { if (startModule < 0 || startModule >= MATRIX_MODULES) return; if (speed < 0) speed = 0; if (speed > 100) speed = 100; int availableModules = MATRIX_MODULES - startModule; if (availableModules < 3) return; // Need at least 3 modules for speed display // Extract digits (hundreds, tens, ones) int hundreds = speed / 100; int tens = (speed / 10) % 10; int ones = speed % 10; // Cache last displayed values per module position static int lastSpeedHundreds = -1, lastSpeedTens = -1, lastSpeedOnes = -1; static int lastSpeedStartModule = -1; // Reset cache if flag is set (called from pause/resume) or start module changed if (resetSpeedDisplayCacheFlag || lastSpeedStartModule != startModule) { lastSpeedHundreds = -1; lastSpeedTens = -1; lastSpeedOnes = -1; lastSpeedStartModule = startModule; resetSpeedDisplayCacheFlag = false; // Clear modules to force full redraw for (int i = 0; i < 3 && (startModule + i) < MATRIX_MODULES; i++) { matrix.clearDisplay(startModule + i); } } // Only update if values changed if (hundreds > 0) { if (hundreds != lastSpeedHundreds) { matrix.clearDisplay(startModule); displayChar('0' + hundreds, startModule); lastSpeedHundreds = hundreds; } if (tens != lastSpeedTens) { matrix.clearDisplay(startModule + 1); displayChar('0' + tens, startModule + 1); lastSpeedTens = tens; } if (ones != lastSpeedOnes) { matrix.clearDisplay(startModule + 2); displayChar('0' + ones, startModule + 2); lastSpeedOnes = ones; } } else { // Don't show leading zero - show space if (lastSpeedHundreds > 0 || lastSpeedHundreds == -1) { matrix.clearDisplay(startModule); displayChar(' ', startModule); lastSpeedHundreds = 0; } if (tens != lastSpeedTens) { matrix.clearDisplay(startModule + 1); displayChar('0' + tens, startModule + 1); lastSpeedTens = tens; } if (ones != lastSpeedOnes) { matrix.clearDisplay(startModule + 2); displayChar('0' + ones, startModule + 2); lastSpeedOnes = ones; } } } void displaySpeed(int speed) { // Display speed percentage on matrix using font (uses all available modules) displaySpeedOnModules(speed, 0); } // ============================================================================ // STATUS PRINTING // ============================================================================ void printStatus() { Serial.print("Состояние: "); switch (exerciseState) { case STATE_IDLE: Serial.print("ОЖИДАНИЕ"); break; case STATE_MODE_SELECTED: Serial.print("ВЫБОР ПАРАМЕТРОВ"); break; case STATE_READY: Serial.print("ГОТОВ"); break; case STATE_COUNTDOWN: Serial.print("ОТСЧЕТ"); break; case STATE_RUNNING: Serial.print("РАБОТА"); break; case STATE_PAUSED: Serial.print("ПАУЗА"); break; case STATE_EMERGENCY: Serial.print("АВАРИЯ"); break; } Serial.print(" | Режим: "); switch (exerciseMode) { case MODE_NONE: Serial.print("НЕТ"); break; case MODE_TIME: Serial.print("ВРЕМЯ"); break; case MODE_DISTANCE: Serial.print("ДИСТАНЦИЯ"); break; } Serial.print(" | Концевик: "); Serial.print(digitalRead(LIMIT_SW_SIGNAL) == HIGH ? "РАЗОМКНУТ" : "ЗАМКНУТ"); Serial.print(" | Мощность: "); Serial.print(speedPercent); Serial.print("%"); if (exerciseState == STATE_RUNNING) { Serial.print(" | Дистанция: "); Serial.print(exerciseProgress.distanceTraveled, 1); Serial.print("м"); unsigned long elapsedTime = getElapsedTime(millis()); Serial.print(" | Время: "); Serial.print(elapsedTime / 1000); Serial.print("с"); } Serial.println(); }