Реалии 2040 таковы: центральное электричество — это роскошь, а за стабильность в розетке теперь отвечают EROI, климатические налоги и токсичные батареи. Но, как говорится, если не можешь подключиться к сети — сделай так, чтобы солнце само бегало за твоей панелью.
Мы (пара инженеров с дивана) придумали механический трекер из подручных материалов, двух супер-медленных моторов с Алиэкспресс и датчика света на ZigBee. Всё управляется через ESP32, а код умещается на пару экранов.
Почему трекер — это must-have
Статичная панель — это прошлый век. Если ты рассчитываешь на автономность, каждый градус поворота даёт до 30% прироста энергии в утренние/вечерние часы. Но давайте по-честному: перед тем, как браться за сварку, мы оценили риски.
Как это собрано (максимально просто)
- Каркас: квадратная труба 20x20, сварена на коленке. Всё держится на двух шпильках с приваренной гайкой.
- Привод: два мотора-редуктора с Алиэкспресс (120 RPM, но с редуктором 1:500). Крутят супер-медленно, но супер-сильно. Торопиться нам некуда, солнце за день смещается на 15° в час — наши моторы отрабатывают это за пару минут.
- Управление: классический H-мост (L298N или аналоги). Два канала: вверх/вниз и влево/вправо.
- Глаз трекера: датчик света на ZigBee (выдаёт 0..1024). Просто приклеен на панель термоклеем — так он видит реальное освещение панели.
- Мозг: ESP32. Wi-Fi нам не нужен, выключаем его для экономии. Работает в цикле: сканирует свет, крутит моторы, ищет максимум.
(Потому что редукторы медленные, а солнце не убежит).
ESP32: минимальный код
В реальности мы добавили защиту от дурака (концевые выключатели) и сглаживание АЦП, но суть та же. Моторы жрут 300 мА на пике, но включаются редко — ESP32 спит в глубоком сне между циклами.
Тест-драйв и выводы
Первый блин — комом: мы перекрутили шпильки и сорвали резьбу. Но со второй попытки система отработала 3 дня в пасмурную погоду и показала +27% заряда по сравнению со статикой.
Главный вывод: в условиях дефицита ресурсов (падение EROI, токсичные отходы и климатические "сюрпризы") трекер — это не хайп, а необходимость. Он позволяет выжать максимум из каждой панели и продлить жизнь аккумуляторам, снижая глубину разряда.
Эпилог: готовь сани летом
Сейчас 2026 год, но через 10 лет «просто купить панели» уже не будет работать — из-за дефицита редкоземельных металлов и налогов на углерод. Умный трекер — это инвестиция в автономию. Мы выложили все чертежи и BOM-лист в открытый доступ. Если у тебя есть руки и паяльник — повтори, не пожалеешь.
А я пошёл за кофе. Панель пусть сама крутится, она теперь умнее меня. Вот тестовый код от нейросети, сорян, не проверял ))
// ============================================================
// Трекер для солнечной панели v3.0 (с защитой от облаков)
// ESP32 + 2 редуктора + ZigBee датчик (0-1024)
// Алгоритм: поиск максимума ТОЛЬКО при стабильном свете
// Или если разница между сторонами > 10% (чтобы не дёргаться на шум)
// ============================================================
#include
// ========== ПИНЫ ДЛЯ МОТОРОВ (H-Bridge) ==========
#define MOTOR1_IN1 13
#define MOTOR1_IN2 14
#define MOTOR2_IN1 27
#define MOTOR2_IN2 26
// ========== КОНЦЕВЫЕ ВЫКЛЮЧАТЕЛИ ==========
#define LIMIT_LEFT 15
#define LIMIT_RIGHT 4
#define LIMIT_UP 16
#define LIMIT_DOWN 17
// ========== ДАТЧИК СВЕТА ==========
#define LIGHT_SENSOR 34
// ========== НАСТРОЙКИ ==========
#define STEP_DELAY 120
#define SLEEP_SEC 600 // 10 минут сна
#define MAX_STEPS 80
#define CLOUD_THRESHOLD 0.45 // Если свет < 45% от максимума — считаем, что облако
// ========== ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ==========
int globalMaxLight = 0; // Запоминаем абсолютный максимум (солнечный день)
int lastGoodPosition = 0; // Последняя удачная позиция (условный угол)
int currentLight = 0;
// ============================================================
// УПРАВЛЕНИЕ МОТОРАМИ
// ============================================================
void motorStop() {
digitalWrite(MOTOR1_IN1, LOW);
digitalWrite(MOTOR1_IN2, LOW);
digitalWrite(MOTOR2_IN1, LOW);
digitalWrite(MOTOR2_IN2, LOW);
}
void motorLeft() {
digitalWrite(MOTOR1_IN1, HIGH);
digitalWrite(MOTOR1_IN2, LOW);
}
void motorRight() {
digitalWrite(MOTOR1_IN1, LOW);
digitalWrite(MOTOR1_IN2, HIGH);
}
void motorUp() {
digitalWrite(MOTOR2_IN1, HIGH);
digitalWrite(MOTOR2_IN2, LOW);
}
void motorDown() {
digitalWrite(MOTOR2_IN1, LOW);
digitalWrite(MOTOR2_IN2, HIGH);
}
// ============================================================
// ЧТЕНИЕ СВЕТА (усреднение)
// ============================================================
int readLight() {
long sum = 0;
for (int i = 0; i < 15; i++) {
sum += analogRead(LIGHT_SENSOR);
delay(2);
}
return sum / 15;
}
// ============================================================
// ОБНОВЛЕНИЕ ГЛОБАЛЬНОГО МАКСИМУМА (калибровка)
// ============================================================
void updateGlobalMax() {
int val = readLight();
if (val > globalMaxLight) {
globalMaxLight = val;
Serial.printf("☀️ Новый глобальный максимум: %d\n", globalMaxLight);
}
}
// ============================================================
// УМНЫЙ ПОИСК МАКСИМУМА (с защитой от облаков)
// ============================================================
int sweepAxis(void (*stepFunc)(), int limitPin1, int limitPin2, int direction) {
/*
Возвращает:
- >0 если найден локальный максимум
- -1 если облачно и дёргаться не надо
*/
// 1. Проверяем, есть ли солнце вообще
int baseLight = readLight();
if (globalMaxLight > 0 && baseLight < globalMaxLight * CLOUD_THRESHOLD) {
Serial.println("☁️ Облачно! Пропускаем трекинг, остаёмся на месте.");
return -1; // Не дёргаемся
}
int bestVal = baseLight;
int bestSteps = 0;
int stepsDone = 0;
// 2. Ищем рост сигнала (крутим в одну сторону)
while (stepsDone < MAX_STEPS) {
if (digitalRead(limitPin1) == LOW || digitalRead(limitPin2) == LOW) {
break;
}
stepFunc();
delay(STEP_DELAY);
motorStop();
delay(20);
int val = readLight();
stepsDone++;
// Если свет начал падать — значит, прошли пик
if (val < bestVal * 0.98) { // 2% гистерезис, чтобы не дёргаться
break;
}
if (val > bestVal) {
bestVal = val;
bestSteps = stepsDone;
}
}
// 3. Возвращаемся в точку максимума
int backSteps = stepsDone - bestSteps;
if (backSteps > 0) {
void (*reverseFunc)() = NULL;
if (stepFunc == motorLeft) reverseFunc = motorRight;
else if (stepFunc == motorRight) reverseFunc = motorLeft;
else if (stepFunc == motorUp) reverseFunc = motorDown;
else if (stepFunc == motorDown) reverseFunc = motorUp;
if (reverseFunc) {
for (int i = 0; i < backSteps; i++) {
reverseFunc();
delay(STEP_DELAY);
motorStop();
delay(10);
}
}
}
// 4. Проверяем, есть ли смысл в этом максимуме
if (bestVal < globalMaxLight * 0.5) {
// Если нашли только 50% от солнечного дня — это шум
Serial.println("🌥️ Свет слабый, позиция не обновляется.");
return -1;
}
return bestVal;
}
// ============================================================
// ОСНОВНОЙ ТРЕКИНГ
// ============================================================
void doTracking() {
Serial.println("\n=== СТАРТ ТРЕКИНГА ===");
// Обновляем глобальный максимум (если сейчас солнечно)
updateGlobalMax();
// --- 1. Горизонталь ---
int hBest1 = sweepAxis(motorLeft, LIMIT_LEFT, LIMIT_RIGHT, 1);
delay(300);
int hBest2 = sweepAxis(motorRight, LIMIT_RIGHT, LIMIT_LEFT, -1);
int finalH = -1;
if (hBest1 > 0 && hBest2 > 0) {
finalH = max(hBest1, hBest2);
} else if (hBest1 > 0) {
finalH = hBest1;
} else if (hBest2 > 0) {
finalH = hBest2;
}
// --- 2. Вертикаль ---
int vBest1 = sweepAxis(motorUp, LIMIT_UP, LIMIT_DOWN, 1);
delay(300);
int vBest2 = sweepAxis(motorDown, LIMIT_DOWN, LIMIT_UP, -1);
int finalV = -1;
if (vBest1 > 0 && vBest2 > 0) {
finalV = max(vBest1, vBest2);
} else if (vBest1 > 0) {
finalV = vBest1;
} else if (vBest2 > 0) {
finalV = vBest2;
}
// --- 3. Логируем результат ---
motorStop();
if (finalH > 0 || finalV > 0) {
Serial.printf("✅ Трекинг успешен: H=%d, V=%d\n", finalH, finalV);
lastGoodPosition = max(finalH, finalV);
} else {
Serial.println("⏸️ Облачно или шум — остаёмся в прошлой позиции.");
}
Serial.println("=== ТРЕКИНГ ЗАВЕРШЁН ===");
}
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n\n[BOOT] Солнечный трекер v3.0 (анти-облако)");
pinMode(LIGHT_SENSOR, INPUT);
pinMode(MOTOR1_IN1, OUTPUT);
pinMode(MOTOR1_IN2, OUTPUT);
pinMode(MOTOR2_IN1, OUTPUT);
pinMode(MOTOR2_IN2, OUTPUT);
motorStop();
pinMode(LIMIT_LEFT, INPUT_PULLUP);
pinMode(LIMIT_RIGHT, INPUT_PULLUP);
pinMode(LIMIT_UP, INPUT_PULLUP);
pinMode(LIMIT_DOWN, INPUT_PULLUP);
// Калибруем начальный максимум
globalMaxLight = readLight();
Serial.printf("Начальный максимум: %d\n", globalMaxLight);
doTracking();
}
// ============================================================
// LOOP (глубокий сон)
// ============================================================
void loop() {
Serial.printf("💤 Сон %d секунд...\n", SLEEP_SEC);
WiFi.mode(WIFI_OFF);
btStop();
esp_sleep_enable_timer_wakeup(SLEEP_SEC * 1000000LL);
esp_deep_sleep_start();
delay(1000);
doTracking();
}
Комментарии
Пока нет комментариев. Будьте первым!