Files
InfiniteStairs/stairs_arduino.ino
2026-01-24 15:17:23 +03:00

2569 lines
89 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================================
// ТРЕНАЖЕР БЕСКОНЕЧНАЯ ЛЕСТНИЦА - Complete Implementation
// ============================================================================
#include <SPI.h>
#include <Wire.h>
#include <string.h>
#include <LiquidCrystal_I2C.h>
#include <LedControl.h>
#include <EEPROM.h>
// ============================================================================
// 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) {
digits[0] = 0;
digitCount = 1;
} else {
int temp = meters;
while (temp > 0 && digitCount < availableModules) {
digits[digitCount] = temp % 10;
temp /= 10;
digitCount++;
}
}
// 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 only the modules we'll actually use (prevents flashing)
for (int i = 0; i < digitCount && (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();
}