//-----------------
// part_#01
//
#include <Wire.h>
#include <SSD1306Ascii.h>
#include <SSD1306AsciiWire.h>
#include <TimeLib.h> // БИБЛИОТЕКА ЖИВОГО ВРЕМЕНИ
// --- ПИНЫ ---
#define DATA_PIN 2
#define BUTTON_PIN 3
#define PWR_SWITCH_PIN 4
// Закомментируй эту строку, когда перейдешь на CR1/3N!
#define USE_18650
// ==========================================
// КАСТОМНАЯ ГРАФИКА ПО ЧЕРТЕЖАМ (6 байт заголовка)
// ==========================================
const uint8_t customGlyphs[] PROGMEM = {
0x00, 0x01, // Индексы 0 и 1: Тип шрифта (00 00 = граница 1 px // 00 01 = граница 0 px)
0x05, // Индекс 2: Ширина (5)
0x08, // Индекс 3: Высота (8)
32, // Индекс 4: Стартовый символ (ASCII 32 = 'SPACE')
39, // Индекс 5: Количество символов в массиве (31 шт.)
// --- ДАЛЬШЕ ИДУТ ДАННЫЕ (с индекса 6) ---
// 32 - space
0x00, 0x00, 0x00, 0x00, 0x00,
// Код 33 ('!') - Залитый блок (Кирпич) 5*7
0x7F, 0x7F, 0x7F, 0x7F, 0x7F,
// Код 34 ('"') - Пустая рамка 5*7
0x7F, 0x41, 0x41, 0x41, 0x7F,
// Код 35 ('#') - Дымчатый сегмент (Шахматка) 5*7
0x55, 0x2A, 0x55, 0x2A, 0x55,
// Код 36 ('$') - Граница верх/низ (пустая шкала) 5*7
0x41, 0x41, 0x41, 0x41, 0x41,
// Код 37 ('%') - Граница право (правая граница шкалы) 5*7
0x00, 0x00, 0x00, 0x00, 0x7F,
// Код 38 ('&') - Граница лево (левая граница шкалы) 5*7
0x7F, 0x00, 0x00, 0x00, 0x00,
// глазки 39 - Залитый блок (Кирпич) 5*8
0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// глазки 40 - "зрачёк влево" 5*8
0xFF, 0x80, 0x80, 0x80, 0x80,
// глазки 41 - "зрачёк вправо" 5*8
0x80, 0x80, 0x80, 0x80, 0xFF,
// 42 - Дымчатый сегмент (Шахматка) 5*8
0x55, 0xAA, 0x55, 0xAA, 0x55,
// 43 - Граница верх/низ (пустая шкала) 5*8
0x81, 0x81, 0x81, 0x81, 0x81,
// 44 - Граница лево (левая граница шкалы) 5*8
0xFF, 0x00, 0x00, 0x00, 0x00,
// 45 - QUAL 1/3
0x00, 0x47, 0x7D, 0x47, 0x40,
// 46 - QUAL 2/3
0x47, 0x40, 0x0F, 0x00, 0x1F,
// 47 - QUAL 3/3
0x00, 0x3F, 0x00, 0x7F, 0x00,
// 48 - BATT 1/3
0x00, 0x7F, 0x7F, 0x41, 0x41,
// 49 - BATT 2/3
0x7F, 0x41, 0x41, 0x7F, 0x41,
// 50 - BATT 3/3
0x41, 0x7F, 0x3E, 0x3E, 0x00,
// 51 - RAMF 1/3
0x00, 0x3C, 0xBD, 0x3C, 0xBD,
// 52 - RAMF 2/3
0x3C, 0xBD, 0x3C, 0xBD, 0x3C,
// 53 - RAMF 3/3
0xBD, 0x3C, 0xBD, 0x3C, 0x00,
// 54 - FLOW 1/3
0x08, 0x09, 0x09, 0xFF, 0x88,
// 55 - FLOW 2/3
0x88, 0x88, 0xFF, 0x09, 0x09,
// 56 - FLOW 3/3
0x09, 0xFF, 0x88, 0x88, 0x08,
// 57 - ------------------------
0x08, 0x08, 0x08, 0x08, 0x08,
// 58 - граница лево/верх 5*8
0xFF, 0x01, 0x01, 0x01, 0x01,
// 59 - CEST 1/6
0x80, 0x82, 0x04, 0xC0, 0xF0,
// 60 - CEST 2/6
0xF0, 0xF8, 0xFB, 0xF8, 0xF0,
// 61 - CEST 3/6
0xF0, 0xC0, 0x04, 0x82, 0x80,
// 62 - CEST 4/6
0x00, 0x20, 0x10, 0x01, 0x07,
// 63 - CEST 5/6
0x07, 0x0F, 0x6F, 0x0F, 0x07,
// 64 - CEST 6/6
0x07, 0x01, 0x10, 0x20, 0x00,
// 65 - CET 1/6
0x85, 0xA2, 0xC5, 0xA8, 0x90,
// 66 - CET 2/6
0xAA, 0x44, 0xBF, 0x44, 0xAA,
// 67 - CET 3/6
0x90, 0xA8, 0xC5, 0xA2, 0x85,
// 68 - CET 4/6
0x50, 0x22, 0x51, 0x0A, 0x04,
// 69 - CET 5/6
0x2A, 0x11, 0x7E, 0x11, 0x2A,
// 70 - CET 6/6
0x04, 0x0A, 0x51, 0x22, 0x50
};
const uint8_t customNumber[] PROGMEM = { //big numbers for TIME/DATE
0x00, 0x00,
0x05,
0x08,
45,
14,
// 45 - "-"
0x00, 0x08, 0x08, 0x08, 0x00,
// 46 - "."
0x00, 0x00, 0xC0, 0x00, 0x00,
// 47 - "/"
0x00, 0xC0, 0x3C, 0x03, 0x00,
// 48 - "0"
0x7E, 0x81, 0x81, 0x81, 0x7E,
// 49 - "1"
0x00, 0x81, 0xFF, 0x80, 0x00,
// 50 - "2"
0xF3, 0x91, 0x91, 0x91, 0x9F,
// 51 - "3"
0x42, 0x81, 0x89, 0x89, 0x76,
// 52 - "4"
0x38, 0x24, 0x22, 0x21, 0xFF,
// 53 - "5"
0xCF, 0x89, 0x89, 0x89, 0xF9,
// 54 - "6"
0xFF, 0x89, 0x89, 0x89, 0xF8,
// 55 - "7"
0x03, 0xC1, 0x21, 0x11, 0x0F,
// 56 - "8"
0x76, 0x89, 0x89, 0x89, 0x76,
// 57 - "9"
0x1F, 0x91, 0x91, 0x91, 0xFF,
// 58 - ":"
0x00, 0x00, 0xCC, 0x00, 0x00
};
const uint8_t customMeandr[] PROGMEM = { //Meandr for FLOW
0x00, 0x01,
0x05,
0x08,
48,
13,
// 48 - "0" __
0x10, 0xF0, 0x80, 0x80, 0xF0,
// 49 - "1"_|||
0x10, 0x1E, 0x02, 0x02, 0x1E,
// 50 - "2" error_e_
0x10, 0x10, 0x10, 0x10, 0x10,
// 51 - "3" minutes flag -| |-
0x10, 0xFE, 0x00, 0x00, 0xFE,
//глифы для эквалайзера на service2 5*8
// 52 - space
0x00, 0x00, 0x00, 0x00, 0x00,
// 53 - 1 пиксель снизу
0x00, 0x80, 0x80, 0x80, 0x80,
// 54 - 2 пикселя снизу
0x00, 0xC0, 0xC0, 0xC0, 0xC0,
// 55 - 3 пикселя снизу
0x00, 0xE0, 0xE0, 0xE0, 0xE0,
// 56 - 4 пикселя снизу
0x00, 0xF0, 0xF0, 0xF0, 0xF0,
// 57 - 5 пикселей снизу
0x00, 0xF8, 0xF8, 0xF8, 0xF8,
// 58 - 6 пикселей снизу
0x00, 0xFC, 0xFC, 0xFC, 0xFC,
// 59 - 7 пикселей снизу
0x00, 0xFE, 0xFE, 0xFE, 0xFE,
// 60 - 8 пикселей снизу
0x00, 0xFF, 0xFF, 0xFF, 0xFF
};
SSD1306AsciiWire oled;
// --- ПЕРЕМЕННЫЕ ДЛЯ ПРЕРЫВАНИЙ ---
volatile unsigned long pulseStartTime = 0;
volatile unsigned long pulseDuration = 0;
volatile bool pulseReady = false;
volatile bool lastState = HIGH;
// --- ПЕРЕМЕННЫЕ UI ---
byte currentScreen = 0;
int batteryPercent = 0.0;
float batteryVoltage = 0.00;
float qualityPercent = 0.0;
float sdramPercent = 0.0;
int rawADC = 0;
// --- Буфер для сборки эфира ---
volatile bool dcf_buffer[60];
volatile byte bit_index = 0;
volatile bool minute_ready = false;
// --- Внутренние часы (Software RTC) ---
uint8_t curr_sec = 0, curr_min = 0, curr_hour = 0;
uint8_t curr_day = 0, curr_month = 0;
uint16_t curr_year = 0;
// --- Флаги валидации (что мы уже успешно поймали) ---
bool seconds_synced = false;
bool minutes_synced = false;
bool hours_synced = false;
bool date_synced = false;
bool is_summer_time = false; // Флаг CEST/CET (зимнее/летнее время)
bool dst_synced = false; // Флаг, чтобы не рисовать глифы, пока не поймаем статус
// --- Строки для OLED (изначально с прочерками) ---
char time_str[9] = "--:--:--";
char date_str[11] = "--.--.----";
// --- Таймер для внутренних часов ---
unsigned long last_tick_millis = 0;
//
// --- АНИМАЦИИ ---
const char* robotAnim[] = {"O_o ", "o_O "};
byte RobotAnimIndex = 0;
unsigned long lastRobotAnimTime = 0;
char spinnerAnim[] = {'|', '/', '-', '\\'};
byte SpinnerAnimIndex = 0;
// --- МОНИТОРИНГ FLOW ---
char flowBuf[25] = " ";
// --- ТЕРМИНАЛ ---
char termData[8][22];
char* term[8];
byte termCol = 0;
int getFreeMemory() {
extern int __heap_start, *__brkval;
int v;
return (int)((size_t)&v - (__brkval == 0 ? (size_t)&__heap_start : (size_t)__brkval));
}
void addTerminalChar(char c) {
memmove(flowBuf, flowBuf + 1, 23);
flowBuf[23] = c;
if (termCol >= 21) {
char* temp = term[0];
for (byte i = 0; i < 7; i++) {
term[i] = term[i + 1];
}
term[7] = temp;
memset(term[7], ' ', 21);
termCol = 0;
}
term[7][termCol] = c;
termCol++;
}
void handlePulse() {
unsigned long now = millis();
bool state = !digitalRead(DATA_PIN); //! - инверсия сигнала на D2, после ВС547B для железа
// bool state = digitalRead(DATA_PIN); // нет инверсии сигнала на D2, для Wokwi
digitalWrite(LED_BUILTIN, !state);
if (state != lastState) {
static unsigned long lastChangeTime = 0;
if (now - lastChangeTime > 30) {
if (state == HIGH) { // HIGH - с инверсией, для железа
// if (state == LOW) { // LOW - без инверсии, для Wokwi
pulseStartTime = now;
} else {
pulseDuration = now - pulseStartTime;
pulseReady = true;
}
lastState = state;
lastChangeTime = now;
}
}
}
void setup() {
//Serial.begin(9600); // Включаем отладку для поиска пропавших данных!
//Serial.println("Система стартует...");
// Внутренний ИОН 1.1V. Жизненно важно для батарейного питания:
// Опорное напряжение не "плывет" при разряде аккумулятора (в отличие от VCC).
analogReference(INTERNAL); //reference voltage 1.1V
// СИНХРОНИЗИРУЕМ ВНУТРЕННИЕ ЧАСЫ С ВРЕМЕНЕМ КОМПИЛЯЦИИ
// syncToCompileTime();
for (byte i = 0; i < 8; i++) {
memset(termData[i], ' ', 21);
termData[i][21] = '\0';
term[i] = termData[i];
}
// Настройка пинов
pinMode(PWR_SWITCH_PIN, INPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(DATA_PIN, INPUT_PULLUP);
// Прерывание висит на CHANGE. Сама функция handlePulse() отрабатывает быстро,
// что отлично подходит для съема сигнала с приемника.
attachInterrupt(digitalPinToInterrupt(DATA_PIN), handlePulse, CHANGE);
Wire.begin();
// Fast I2C (400kHz). Обязательно для OLED при работе с точными таймингами.
// Сокращает время блокировки процессора на отправку кадров дисплею.
Wire.setClock(400000L);
oled.begin(&SH1106_128x64, 0x3C); // для 1,3" SH1106, для железа
// oled.begin(&Adafruit128x64, 0x3C); // для 0,96" SSD1306, для Wokwi
oled.setFont(System5x7);
// Расчет свободной памяти
sdramPercent = 100 - ((getFreeMemory() * 100L) / 2048L);
// Глазки
oled.setFont(customGlyphs);
oled.set2X();
delay(1000); oled.clear();
oled.setCursor(0, 2); oled.write(33); oled.write(33); oled.write(33); oled.print(" "); oled.write(33); oled.write(33); oled.write(33);
delay(1000); oled.clear();
oled.setCursor(0, 0); oled.write(39); oled.write(39); oled.write(39); oled.print(" "); oled.write(39); oled.write(39); oled.write(39);
oled.setCursor(0, 2); oled.write(39); oled.write(39); oled.write(39); oled.print(" "); oled.write(39); oled.write(39); oled.write(39);
oled.setCursor(0, 4); oled.write(40); oled.write(39); oled.write(39); oled.print(" "); oled.write(40); oled.write(39); oled.write(39);
delay(1000);
oled.clear();
oled.setCursor(0, 2); oled.write(33); oled.write(33); oled.write(33); oled.print(" "); oled.write(33); oled.write(33); oled.write(33);
delay(1000); oled.clear();
oled.setCursor(0, 0); oled.write(39); oled.write(39); oled.write(39); oled.print(" "); oled.write(39); oled.write(39); oled.write(39);
oled.setCursor(0, 2); oled.write(39); oled.write(39); oled.write(39); oled.print(" "); oled.write(39); oled.write(39); oled.write(39);
oled.setCursor(0, 4); oled.write(39); oled.write(39); oled.write(41); oled.print(" "); oled.write(39); oled.write(39); oled.write(41);
delay(1000); oled.clear(); delay(1000);
oled.setFont(System5x7);
runBootSequence();
updateBatteryStatus();
}
////
// Преобразование BCD (двоично-десятичного кода) в обычное число
uint8_t decodeBCD(byte start_bit, byte end_bit) {
uint8_t ones = 0;
uint8_t tens = 0;
byte weight = 1;
// Считаем единицы (первые 4 бита)
for (byte i = start_bit; i <= min(start_bit + 3, end_bit); i++) {
if (dcf_buffer[i]) ones += weight;
weight *= 2;
}
// Считаем десятки (оставшиеся биты)
weight = 1;
for (byte i = start_bit + 4; i <= end_bit; i++) {
if (dcf_buffer[i]) tens += weight;
weight *= 2;
}
return (tens * 10) + ones;
}
// Проверка на четность (Even Parity)
bool checkParity(byte start_bit, byte end_bit, byte parity_bit) {
byte ones_count = 0;
// Считаем все единички в блоке данных
for (byte i = start_bit; i <= end_bit; i++) {
if (dcf_buffer[i]) ones_count++;
}
// Прибавляем сам контрольный бит
if (dcf_buffer[parity_bit]) ones_count++;
// Если сумма четная (остаток от деления на 2 равен нулю) — данные чистые
return (ones_count % 2 == 0);
}
////
// Обновляем символы в строках, только если данные валидны
void updateDisplayStrings() {
if (hours_synced) {
time_str[0] = '0' + curr_hour / 10;
time_str[1] = '0' + curr_hour % 10;
}
if (minutes_synced) {
time_str[3] = '0' + curr_min / 10;
time_str[4] = '0' + curr_min % 10;
}
if (seconds_synced) {
time_str[6] = '0' + curr_sec / 10;
time_str[7] = '0' + curr_sec % 10;
}
if (date_synced) {
date_str[0] = '0' + curr_day / 10; date_str[1] = '0' + curr_day % 10;
date_str[3] = '0' + curr_month / 10; date_str[4] = '0' + curr_month % 10;
date_str[6] = '2'; date_str[7] = '0'; // Год передается двузначным, жестко пишем 20xx
date_str[8] = '0' + curr_year / 10; date_str[9] = '0' + curr_year % 10;
}
}
// Программные часы (Software RTC)
void tickRTC() {
if (millis() - last_tick_millis >= 1000) {
last_tick_millis += 1000;
// Тикаем, только если поймали хотя бы начало минуты
if (seconds_synced) {
curr_sec++;
if (curr_sec > 59) {
curr_sec = 0;
if (minutes_synced) {
curr_min++;
if (curr_min > 59) {
curr_min = 0;
if (hours_synced) {
curr_hour++;
if (curr_hour > 23) curr_hour = 0;
// Смену дней/месяцев пока оставляем на откуп DCF77 (он сам обновит раз в сутки)
}
}
}
}
updateDisplayStrings(); // Обновляем текст на экране каждую секунду
}
}
}
////
void decodeDCF77() {
// Проверка флагов летнего/зимнего времени (Биты 17 и 18)
if (dcf_buffer[17] != dcf_buffer[18]) {
is_summer_time = dcf_buffer[17];
dst_synced = true;
}
// Защита от смещенного буфера
if (dcf_buffer[20] != 1) return;
// 1. Проверяем Зону Минут (Биты 21-27, паритет 28)
if (checkParity(21, 27, 28)) {
int tmp_min = decodeBCD(21, 27);
// Sanity check: минут больше 59 не бывает
if (tmp_min <= 59) {
curr_min = tmp_min;
minutes_synced = true;
}
}
// 2. Проверяем Зону Часов (Биты 29-34, паритет 35)
if (checkParity(29, 34, 35)) {
int tmp_hour = decodeBCD(29, 34);
// Sanity check: часов больше 23 не бывает
if (tmp_hour <= 23) {
curr_hour = tmp_hour;
hours_synced = true;
}
}
// 3. Проверяем Зону Даты (Биты 36-57, паритет 58)
if (checkParity(36, 57, 58)) {
int tmp_day = decodeBCD(36, 41);
int tmp_month = decodeBCD(45, 49);
int tmp_year = decodeBCD(50, 57);
// Sanity check даты: именно он убивает баг "00.00.2000"
// День и месяц не могут быть нулевыми.
// Поскольку твой проект 2026+ года, нулевой год тоже считаем мусором.
if (tmp_day >= 1 && tmp_day <= 31 &&
tmp_month >= 1 && tmp_month <= 12 &&
tmp_year > 0) {
curr_day = tmp_day;
curr_month = tmp_month;
curr_year = tmp_year;
date_synced = true;
}
}
// Обновляем строки, чтобы при следующем кадре OLED вывел новые данные
updateDisplayStrings();
}
////
void loop() {
// Заставляем двоеточия в строке мигать
time_str[2] = (millis() % 1000 < 500) ? ':' : ' ';
time_str[5] = (millis() % 1000 < 500) ? ':' : ' ';
// --- ВНУТРЕННИЕ ЧАСЫ ---
tickRTC(); // Даем часам возможность обновлять секунды и минуты
// --- РАСШИФРОВКА ПАКЕТА ---
if (minute_ready) {
decodeDCF77(); // Запускаем парсинг
minute_ready = false; // Опускаем флаг до следующей минутной паузы
}
handleButton();
// --- ОБРАБОТКА ЭФИРА ---
static unsigned long lastValidPulseTime = 0;
static bool minuteMarkLogged = false;
if (pulseReady) {
noInterrupts();
unsigned long dur = pulseDuration;
pulseReady = false;
interrupts();
// === ДИАГНОСТИКА ПРЕРЫВАНИЯ ===
// Если здесь полетят цифры ~800-900, значит прерывание ловит паузы,
// и логику в FALLING/RISING нужно инвертировать обратно!
//Serial.print("DUR: ");
//Serial.print(dur); Serial.print(" ms. // ");
char newBit = '?';
float pulseScore = 0.0;
// Считаем отклонение от идеальных 100 мс и 200 мс
float d0 = dur - 100.0;
float d1 = dur - 200.0;
// Вычисляем Гауссиану (колокол) для нуля и единицы
float score0 = 100.0 * exp(-(d0 * d0) / 1250.0);
float score1 = 100.0 * exp(-(d1 * d1) / 1250.0);
// Сравниваем, на что больше похож импульс, с порогом отсечения мусора 15%
if (score0 > score1 && score0 > 15.0) {
newBit = '0';
pulseScore = score0;
lastValidPulseTime = millis();
minuteMarkLogged = false;
// === ЗАПИСЫВАЕМ НОЛЬ В БУФЕР ===
if (bit_index < 60) {
dcf_buffer[bit_index] = 0;
bit_index++;
}
}
else if (score1 > score0 && score1 > 15.0) {
newBit = '1';
pulseScore = score1;
lastValidPulseTime = millis();
minuteMarkLogged = false;
// === ЗАПИСЫВАЕМ ЕДИНИЦУ В БУФЕР ===
if (bit_index < 60) {
dcf_buffer[bit_index] = 1;
bit_index++;
}
}
else if (dur < 50) {
// Игнорируем короткие всплески
}
else {
// Мусор
newBit = '2';
pulseScore = 0.0;
}
if (newBit != '?') {
// Скользящая средняя качества плавно стремится к Гауссовой оценке текущего бита
qualityPercent = ((qualityPercent * 7.0) + pulseScore) / 8.0;
if (qualityPercent > 99.9) qualityPercent = 99.9;
addTerminalChar(newBit);
}
}
//
////
//-----------------
//-----------------
// part_#02
//
// --- ЛОВИМ МИНУТНУЮ МЕТКУ ---
if (!minuteMarkLogged && digitalRead(DATA_PIN) == HIGH && lastValidPulseTime > 0) {
if (millis() - lastValidPulseTime > 1500) {
addTerminalChar('3');
qualityPercent = ((qualityPercent * 7.0) + 100.0) / 8.0;
if (qualityPercent > 99.9) qualityPercent = 99.9;
minuteMarkLogged = true;
// === ЖЕСТКАЯ СИНХРОНИЗАЦИЯ (Только 1 раз в минуту!) ===
curr_sec = 0; // Обнуляем секунды
seconds_synced = true; // Разрешаем часам тикать
last_tick_millis = millis(); // Сбрасываем внутренний таймер
minute_ready = true; // Даем добро на парсинг
// Serial.println();
// Serial.print("BITS CAUGHT: ");
// Serial.println(bit_index);
// Serial.print("DUMP: ");
// for (int i = 0; i < 60; i++) {
// Serial.print(dcf_buffer[i] ? "1" : "0");
// }
// Serial.println();
// Сбрасываем индекс для новой минуты
bit_index = 0;
}
}
static unsigned long lastBatTime = 0;
if (millis() - lastBatTime >= 30000) {
lastBatTime = millis();
updateBatteryStatus();
}
if (millis() - lastRobotAnimTime >= 500) {
lastRobotAnimTime = millis();
RobotAnimIndex = (RobotAnimIndex + 1) % 2;
}
static unsigned long lastDrawTime = 0;
if (millis() - lastDrawTime >= 100) {
lastDrawTime = millis();
SpinnerAnimIndex = (SpinnerAnimIndex + 1) % 4;
switch (currentScreen) {
case 0: drawMonitoring(); break;
case 1: drawService1(); break;
case 2: drawService2(); break;
case 3: drawWordTime1(); break;
case 4: drawWordTime2(); break;
case 5: drawWordTime3(); break;
case 6: drawWordTime4(); break;
case 7: drawWordTime5(); break;
case 8: drawWordTime6(); break;
case 9: drawWordTime7(); break;
case 10: drawTerminal(); break;
}
}
}
////
//
// --- ХЕЛПЕРЫ ДЛЯ ЧАСОВЫХ ПОЯСОВ ---
// 1. Получаем текущее время DCF77 (Франкфурт) в секундах
time_t getDcfTime() {
tmElements_t tm_local;
tm_local.Second = curr_sec;
tm_local.Minute = curr_min;
tm_local.Hour = curr_hour;
// Защита: пока дата не поймана из эфира, переменные равны 0.
// Библиотека TimeLib не любит нулевой день и месяц.
tm_local.Day = (curr_day > 0) ? curr_day : 1;
tm_local.Month = (curr_month > 0) ? curr_month : 1;
// Библиотека ожидает год в формате "лет с 1970"
tm_local.Year = CalendarYrToTm(2000 + curr_year);
return makeTime(tm_local); // Пакуем в единое число (Unix Time)
}
// 2. Получаем чистый UTC с учетом зимы/лета
time_t getUtcTime() {
time_t dcf_time = getDcfTime();
if (is_summer_time) {
return dcf_time - (2 * 3600UL); // Летом Германия это UTC+2
} else {
return dcf_time - (1 * 3600UL); // Зимой Германия это UTC+1
}
}
//
////
void handleButton() {
static bool btnPressed = false;
bool reading = !digitalRead(BUTTON_PIN);
if (reading && !btnPressed) {
btnPressed = true;
delay(50);
} else if (!reading && btnPressed) {
btnPressed = false;
currentScreen = (currentScreen + 1) % 11;
oled.clear();
}
}
void updateBatteryStatus() {
// Переводим пин в OUTPUT и прижимаем к земле, замыкая цепь делителя
pinMode(PWR_SWITCH_PIN, OUTPUT);
digitalWrite(PWR_SWITCH_PIN, LOW);
// Даем время на стабилизацию напряжения в делителе (заряд емкости пина АЦП)
delay(10);
// Холостое чтение для переключения мультиплексора АЦП на нужный канал
analogRead(A0);
long sum = 0;
for (byte i = 0; i < 8; i++) {
sum += analogRead(A0);
delay(1);
}
rawADC = sum / 8;
// Размыкаем цепь делителя, переводя пин в высокоимпедансное состояние (INPUT),
// чтобы остановить утечку тока через резисторы.
pinMode(PWR_SWITCH_PIN, INPUT);
// Коэффициент = (V_max_батареи / V_max_на_АЦП) * (1.1V / 1023)
// При текущем делителе 1 единица АЦП = ~1.075 мВ на входе.
// k = RealBattery Voltage / rawADC // calibration
float k = 0.0111607143; //18650;
// float k = 0.00538; //wokwi
batteryVoltage = rawADC * k;
int voltX100 = batteryVoltage * 100;
#ifdef USE_18650
// Для Li-Ion: 4.2V (100%) - 3.2V (0%)
batteryPercent = constrain(map(voltX100, 320, 420, 0, 100), 0, 99);
#else
// Для CR1/3N (Li-MnO2): Новое напряжение ~3.0V - 3.2V, разряжается до ~2.0V
// Желательно скорректировать пороги под даташит конкретной батарейки.
batteryPercent = constrain(map(voltX100, 210, 300, 0, 100), 0, 99);
#endif
}
void drawMonitoring() {
oled.setFont(customNumber); oled.set2X(); oled.setCursor(0, 0); oled.print(time_str);
// вствка метки зимнее / летнее время (CEST/CET солнышко/снежинка)
oled.setFont(customGlyphs); oled.set1X();
uint8_t x_offset = 106;
// мигание зима/лето, для просмотра, по статусу пина D2
// bool isSummerTime = digitalRead(2);
// Отрисовка символа летнего/зимнего времени
if (dst_synced) {
oled.setCursor(x_offset, 0);
if (is_summer_time) {
oled.write(59); oled.write(60); oled.write(61); // три глифа солнца - наверху
} else {
oled.write(65); oled.write(66); oled.write(67); // три глифа снежинки - внизу
}
oled.setCursor(x_offset, 1);
if (!is_summer_time) {
oled.write(68); oled.write(69); oled.write(70); //три глифа снежинки - наверху
} else {
oled.write(62); oled.write(63); oled.write(64); // три глифа солнца - внизу
}
}
oled.set1X(); oled.setCursor(0, 2);
for (int i = 0; i < 25; i++) {
oled.setFont(customGlyphs);
oled.write(57);
}
oled.setFont(customNumber); oled.set2X(); oled.setCursor(0, 3); oled.print(date_str);
oled.set1X(); oled.setCursor(0, 5);
for (int i = 0; i < 25; i++) {
oled.setFont(customGlyphs);
oled.write(57);
}
oled.set1X();
uint8_t x1_offset = 0;
oled.setCursor(x1_offset, 7); oled.setFont(customGlyphs);
oled.write(54); oled.write(55); oled.write(56); oled.write(32); // Иконка FLOW
// oled.setCursor(x1_offset, 6); oled.setFont(customGlyphs);
// oled.write(32); oled.write(32); oled.write(32); oled.write(32);
// тут нужно условие: ест синал "RTC" не вывоим, нет сигнала на D2 - выводим,
// соответственно, если надпись "RTC" есть, значка "FLOW", и меандра быть не должно!
oled.setCursor(x1_offset, 6); oled.setFont(System5x7);
oled.print("RTC");
// рисуем меандр, ошибки, минутный флаг
oled.set2X(); oled.setFont(customMeandr); oled.print(&flowBuf[12]);
oled.setFont(System5x7);
}
void drawService2() {
oled.setFont(customGlyphs); oled.set2X(); oled.setCursor(0, 1);
oled.write(48); oled.write(49); oled.write(50); oled.write(32);
oled.setFont(System5x7);
oled.print(batteryVoltage, 2); oled.print('V');
oled.set1X(); oled.print(rawADC);
oled.set1X(); oled.setCursor(0, 3); oled.setFont(customGlyphs);
oled.write(33); drawBar(batteryPercent, 24); oled.setFont(customGlyphs); oled.write(38);
oled.setFont(customGlyphs); oled.set2X(); oled.setCursor(0, 6);
oled.write(51); oled.write(52); oled.write(53); oled.write(32); oled.write(32); oled.write(32);
oled.setFont(System5x7);
if (sdramPercent < 10) oled.print('0'); oled.print(sdramPercent, 1); oled.print('%');
oled.set1X(); oled.setCursor(0, 5); oled.setFont(customGlyphs);
oled.write(33); drawBar(sdramPercent, 24); oled.setFont(customGlyphs); oled.write(38);
oled.setFont(System5x7);
}
// Форма Гауссова колокола для 25 столбцов (макс. высота 40 пикселей)
const byte bellShape[25] = {1, 2, 3, 4, 5, 8, 13, 18, 24, 30, 35, 39, 40, 39, 35, 30, 24, 18, 13, 8, 5, 4, 3, 2, 1};
void drawService1() {
oled.set2X(); oled.setCursor(0, 0); oled.setFont(customGlyphs);
oled.write(45); oled.write(46); oled.write(32); oled.write(32); oled.write(32); oled.write(32);
oled.setFont(System5x7);
if (qualityPercent < 10) oled.print('0');
oled.print(qualityPercent, 1); oled.print('%');
oled.set1X(); oled.setCursor(0, 2); oled.setFont(customGlyphs);
oled.write(33); drawBar(qualityPercent >= 99.9 ? 100 : (int)qualityPercent, 24); oled.setFont(customGlyphs); oled.write(38);
// --- ОТРИСОВКА МОНОЛИТНОГО КОЛОКОЛА (Строки с 3 по 7, 25 столбцов) ---
oled.setFont(customMeandr); // Включаем шрифт с глифами эквалайзера
for (int r = 0; r < 5; r++) {
oled.set1X();
oled.setCursor(0, 3 + r);
for (int col = 0; col < 25; col++) {
// Вычисляем целевую высоту конкретного столбца (масштабируем от 0 до 100%)
int targetHeight = (bellShape[col] * (int)qualityPercent) / 100;
// Вычисляем базовую высоту текущей строки (строка 4 - это низ = 0, строка 0 - это верх = 32)
int rowBase = (4 - r) * 8;
// Сколько пикселей гистограммы попадает в текущую строку?
int pixelsInThisRow = targetHeight - rowBase;
if (pixelsInThisRow <= 0) {
// Столбец до этой строки не дорос (пустота)
oled.write(52);
} else if (pixelsInThisRow >= 8) {
// Столбец пробивает эту строку насквозь (полный монолит)
oled.write(60);
} else {
// Столбец заканчивается внутри этой строки (частичное заполнение)
// Так как символы идут по порядку: 53 (0px) + pixelsInThisRow дает нужный символ
oled.write(52 + pixelsInThisRow);
}
}
}
oled.setFont(System5x7); // Возврат к стандартному шрифту
}
////
//
void drawWordTime1() {
// Лондон (Европа). Всегда на 1 час отстает от Франкфурта (и летом, и зимой).
time_t t = getDcfTime() - (1 * 3600UL);
drawGenericWordTime(t, F("LONDON.UTC "));
}
void drawWordTime2() {
// Дубай (Без переходов, всегда UTC+4)
time_t t = getUtcTime() + (4 * 3600UL);
drawGenericWordTime(t, F(" DUBAI "));
}
void drawWordTime3() {
// Москва (Без переходов, всегда UTC+3)
time_t t = getUtcTime() + (3 * 3600UL);
drawGenericWordTime(t, F(" MOCKBA "));
}
void drawWordTime4() {
// Пекин (Без переходов, всегда UTC+8)
time_t t = getUtcTime() + (8 * 3600UL);
drawGenericWordTime(t, F(" BEIJING "));
}
void drawWordTime5() {
// Токио (Без переходов, всегда UTC+9)
time_t t = getUtcTime() + (9 * 3600UL);
drawGenericWordTime(t, F(" TOKYO "));
}
void drawWordTime6() {
// Сидней (Чтобы не усложнять австралийским летним временем, берем стандартный UTC+10)
time_t t = getUtcTime() + (10 * 3600UL);
drawGenericWordTime(t, F(" SYDNEY "));
}
void drawWordTime7() {
// Нью-Йорк (Чтобы не усложнять американским летним временем, берем стандартный UTC-5)
time_t t = getUtcTime() - (5 * 3600UL);
drawGenericWordTime(t, F(" NEW-YORK "));
}
////
//
void drawGenericWordTime(time_t city_time, const __FlashStringHelper* city) {
tmElements_t tm_city;
breakTime(city_time, tm_city); // Магия TimeLib: распаковываем секунды обратно в структуру
oled.set2X();
oled.setCursor(0, 0);
oled.print(' ');
oled.setFont(customNumber);
// --- БЛОК ЧАСОВ ---
if (hours_synced) {
print2Digits(tm_city.Hour); // Берем часы уже из распакованного города
} else {
oled.print("--");
}
oled.print(millis() % 1000 < 500 ? ':' : ' ');
// --- БЛОК МИНУТ ---
if (minutes_synced) {
print2Digits(tm_city.Minute);
} else {
oled.print("--");
}
oled.print(millis() % 1000 < 500 ? ':' : ' ');
// --- БЛОК СЕКУНД ---
if (seconds_synced) {
print2Digits(tm_city.Second);
} else {
oled.print("--");
}
// --- ДЕКОР ---
oled.set1X(); oled.setCursor(0, 2);
for (int i = 0; i < 25; i++) {
oled.setFont(customGlyphs); oled.write(57);
}
// --- БЛОК ДАТЫ ---
oled.set2X(); oled.setCursor(0, 3); oled.setFont(customNumber);
if (date_synced) {
print2Digits(tm_city.Day); // Библиотека сама сменит день, если в городе за полночь!
oled.print('.');
print2Digits(tm_city.Month);
oled.print('.');
oled.print(tmYearToCalendar(tm_city.Year)); // Возвращает полный год (напр. 2026)
} else {
oled.print("--.--.----");
}
// --- ДЕКОР ---
oled.set1X(); oled.setCursor(0, 5);
for (int i = 0; i < 25; i++) {
oled.setFont(customGlyphs); oled.write(57);
}
// --- ГОРОД ---
oled.set2X(); oled.setCursor(0, 6); oled.setFont(System5x7); oled.print(city);
oled.set1X();
oled.setFont(System5x7);
}
//
////
void drawTerminal() {
for (byte i = 0; i < 8; i++) {
oled.setCursor(0, i);
oled.print(term[i]);
}
oled.setFont(System5x7);
if (termCol < 21) {
oled.setCursor(termCol * 6, 7);
oled.print(spinnerAnim[SpinnerAnimIndex]);
}
}
void print2Digits(int n) {
if (n < 10) oled.print('0');
oled.print(n);
}
void drawBar(int val, int maxChars) {
int numFilled = map(val, 0, 100, 0, maxChars);
oled.setFont(customGlyphs);
for (int i = 0; i < maxChars; i++) {
oled.write(i < numFilled ? 33 : 36);
}
oled.setFont(System5x7);
}
void runBootSequence() {
oled.clear();
oled.set2X(); oled.setCursor(0, 1); oled.println(F("RD&P Lab "));
oled.set1X(); oled.setCursor(0, 3); oled.println(F(" with"));
oled.set2X(); oled.setCursor(0, 5); oled.println(F(" Gemini"));
oled.set1X(); oled.setCursor(0, 7); oled.println(F(" [O_o]")); delay(1000);
oled.setCursor(0, 7); oled.println(F(" [o_O]")); delay(1000);
oled.setCursor(0, 7); oled.println(F(" [O_o]")); delay(1000);
oled.clear();
oled.set1X(); oled.setCursor(0, 0); oled.println(F("v.0010_final"));
oled.set2X(); oled.setCursor(0, 2); oled.println(F("W-MONITOR"));
oled.set1X(); oled.println(F(" DCF77"));
oled.setCursor(0, 6);
for (int i = 0; i < 25; i++) {
oled.setFont(customGlyphs);
oled.write(42);
delay(40);
}
oled.setCursor(0, 6);
for (int i = 0; i < 25; i++) {
oled.write(39);
delay(40);
}
delay(500); oled.clear();
oled.setFont(System5x7);
}
//-----------------