2577 lines
90 KiB
C++
2577 lines
90 KiB
C++
// ============================================================================
|
||
// ТРЕНАЖЕР БЕСКОНЕЧНАЯ ЛЕСТНИЦА - 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) {
|
||
// 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();
|
||
}
|