Загрузка данных


// =====================================================================
//  Сушилка для овощей на ESP8266 (Witty Cloud)
//  Framework: ESP8266 Non-OS SDK (PlatformIO: framework = esp8266-nonos-sdk)
//  - SSR + ПИД + медленный ШИМ (2000 мс)
//  - 3 семисегментных индикатора через PCF8574 (I2C)
//  - 3 кнопки: MODE, UP, DOWN
//  - Таймер до 999 минут, температура 30..99 °C целыми градусами
//  - Wi-Fi AP + веб-интерфейс
//  - Сохранение уставок во flash
//  - UART TX работает для отладки через os_printf
// =====================================================================

#include "ets_sys.h"
#include "osapi.h"
#include "gpio.h"
#include "os_type.h"
#include "user_interface.h"
#include "mem.h"
#include "ip_addr.h"
#include "espconn.h"
#include "eagle_soc.h"

// ==================== ПИНЫ ====================
#define SSR_PIN         12
#define DS18B20_PIN     2
#define BTN_UP_PIN      4
#define BTN_DOWN_PIN    5
#define BTN_MODE_PIN    0       // ТРЕБУЕТ внешнего pullup 10 кОм

#define I2C_SDA_PIN     14
#define I2C_SCL_PIN     13
#define PCF8574_ADDR    0x20    // A0=A1=A2=GND

#define DIGIT1_PIN      16      // десятки  (GPIO16, RTC)
#define DIGIT2_PIN      15      // единицы  (GPIO15)
#define DIGIT3_PIN      3       // третий   (RX, UART только TX!)

// ==================== ТАЙМИНГИ ====================
#define PWM_PERIOD_MS       2000
#define PID_SAMPLE_MS       1000
#define DISPLAY_REFRESH_MS  5
#define BTN_POLL_MS         50
#define LONG_PRESS_TICKS    40      // 40 * 50 мс = 2 сек
#define DP_BLINK_TICKS      100     // 100 * 5 мс = 500 мс
#define SECONDS_PER_MINUTE  60

// ==================== ПИД ====================
#define PID_KP          2.0
#define PID_KI          5.0
#define PID_KD          1.0

// ==================== ГРАНИЦЫ ====================
#define TEMP_MIN        30
#define TEMP_MAX        99
#define TEMP_STEP       1

#define TIMER_MIN_MIN   1
#define TIMER_MAX_MIN   999
#define TIMER_STEP_MIN  1

// ==================== FLASH ====================
#define FLASH_SECTOR    0x3C
#define FLASH_MAGIC     0xA5C3

// ==================== WI-FI ====================
#define AP_SSID         "Dryer"
#define AP_PASS         "12345678"

// ==================== РЕЖИМЫ ====================
enum { MODE_EDIT_TEMP = 0, MODE_EDIT_TIMER, MODE_RUNNING };

// ==================== ГЛОБАЛЬНЫЕ ====================
static os_timer_t pwm_timer, pid_timer, display_timer, btn_timer;

// ШИМ
static uint32_t pwm_counter = 0;
static uint32_t pwm_duty    = 0;

// ПИД
static double pid_input      = 0.0;
static double pid_output     = 0.0;
static double pid_integral   = 0.0;
static double pid_last_error = 0.0;

// Уставки
static int      temp_setpoint       = 50;
static uint32_t timer_setpoint_min  = 5;
static uint32_t timer_remaining_min = 0;
static uint8_t  sec_counter         = 0;

// Режим
static uint8_t current_mode = MODE_EDIT_TEMP;

// Дисплей
static uint8_t  disp_digits[3] = {0, 0, 0};
static uint8_t  blank_digit[3] = {0, 0, 0};
static uint8_t  display_idx    = 0;
static bool     dp_blink_state = true;
static uint32_t dp_blink_cnt   = 0;
static uint8_t  display_phase  = 0;

// Кнопки
static uint8_t  btn_up_last   = 1;
static uint8_t  btn_down_last = 1;
static uint8_t  btn_mode_last = 1;
static uint32_t btn_mode_hold = 0;

// HTTP
static struct espconn http_conn;
static esp_tcp        http_tcp;

// Таблица сегментов (a..g в битах 0..6)
static const uint8_t seg_table[10] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};

// ==================== ПРОТОТИПЫ ====================
static void ICACHE_FLASH_ATTR delay_us(uint32_t us);
static void ICACHE_FLASH_ATTR gpio16_set(uint8_t val);

static void ICACHE_FLASH_ATTR i2c_delay(void);
static void ICACHE_FLASH_ATTR i2c_start(void);
static void ICACHE_FLASH_ATTR i2c_stop(void);
static uint8_t ICACHE_FLASH_ATTR i2c_write_byte(uint8_t data);
static void ICACHE_FLASH_ATTR pcf8574_write(uint8_t data);

static void ICACHE_FLASH_ATTR ow_init(void);
static uint8_t ICACHE_FLASH_ATTR ow_reset(void);
static void ICACHE_FLASH_ATTR ow_write_bit(uint8_t b);
static uint8_t ICACHE_FLASH_ATTR ow_read_bit(void);
static void ICACHE_FLASH_ATTR ow_write_byte(uint8_t b);
static uint8_t ICACHE_FLASH_ATTR ow_read_byte(void);
static double ICACHE_FLASH_ATTR read_temperature(void);

static void ICACHE_FLASH_ATTR all_digits_off(void);
static void ICACHE_FLASH_ATTR select_digit(uint8_t idx);
static void ICACHE_FLASH_ATTR update_display(void);

static void ICACHE_FLASH_ATTR save_params(void);
static void ICACHE_FLASH_ATTR load_params(void);

static void ICACHE_FLASH_ATTR start_timer(void);
static void ICACHE_FLASH_ATTR stop_timer(void);

static void ICACHE_FLASH_ATTR pwm_timer_cb(void *arg);
static void ICACHE_FLASH_ATTR pid_timer_cb(void *arg);
static void ICACHE_FLASH_ATTR display_timer_cb(void *arg);
static void ICACHE_FLASH_ATTR btn_timer_cb(void *arg);

static void ICACHE_FLASH_ATTR http_recv_cb(void *arg, char *data, unsigned short len);
static void ICACHE_FLASH_ATTR http_sent_cb(void *arg);
static void ICACHE_FLASH_ATTR http_discon_cb(void *arg);
static void ICACHE_FLASH_ATTR http_recon_cb(void *arg, sint8 err);
static void ICACHE_FLASH_ATTR wifi_init_ap(void);
static void ICACHE_FLASH_ATTR http_init(void);

// ==================== ЗАДЕРЖКА ====================
static void ICACHE_FLASH_ATTR delay_us(uint32_t us) {
    uint32_t start = system_get_time();
    while (system_get_time() - start < us);
}

// ==================== GPIO16 ====================
static void ICACHE_FLASH_ATTR gpio16_set(uint8_t val) {
    WRITE_PERI_REG(PAD_XPD_DCDC_CONF,
        (READ_PERI_REG(PAD_XPD_DCDC_CONF) & 0xFFFFFFBC) | 0x01);
    WRITE_PERI_REG(RTC_GPIO_CONF,
        (READ_PERI_REG(RTC_GPIO_CONF) & 0xFFFFFFFE) | 0x00);
    WRITE_PERI_REG(RTC_GPIO_ENABLE,
        (READ_PERI_REG(RTC_GPIO_ENABLE) & 0xFFFFFFFE) | 0x01);
    uint32_t out = READ_PERI_REG(RTC_GPIO_OUT);
    if (val) out &= ~(1u << 0);   // инверсная логика GPIO16
    else     out |=  (1u << 0);
    WRITE_PERI_REG(RTC_GPIO_OUT, out);
}

// ==================== I2C (программный) ====================
static void ICACHE_FLASH_ATTR i2c_delay(void) {
    delay_us(4);
}

static void ICACHE_FLASH_ATTR i2c_start(void) {
    GPIO_OUTPUT_SET(I2C_SDA_PIN, 1);
    GPIO_OUTPUT_SET(I2C_SCL_PIN, 1);
    i2c_delay();
    GPIO_OUTPUT_SET(I2C_SDA_PIN, 0);
    i2c_delay();
    GPIO_OUTPUT_SET(I2C_SCL_PIN, 0);
    i2c_delay();
}

static void ICACHE_FLASH_ATTR i2c_stop(void) {
    GPIO_OUTPUT_SET(I2C_SDA_PIN, 0);
    GPIO_OUTPUT_SET(I2C_SCL_PIN, 1);
    i2c_delay();
    GPIO_OUTPUT_SET(I2C_SDA_PIN, 1);
    i2c_delay();
}

static uint8_t ICACHE_FLASH_ATTR i2c_write_byte(uint8_t data) {
    for (uint8_t i = 0; i < 8; i++) {
        GPIO_OUTPUT_SET(I2C_SDA_PIN, (data & 0x80) ? 1 : 0);
        data <<= 1;
        i2c_delay();
        GPIO_OUTPUT_SET(I2C_SCL_PIN, 1);
        i2c_delay();
        GPIO_OUTPUT_SET(I2C_SCL_PIN, 0);
        i2c_delay();
    }
    // ACK
    GPIO_DIS_OUTPUT(I2C_SDA_PIN);
    GPIO_OUTPUT_SET(I2C_SDA_PIN, 1);
    i2c_delay();
    GPIO_OUTPUT_SET(I2C_SCL_PIN, 1);
    i2c_delay();
    uint8_t ack = !GPIO_INPUT_GET(I2C_SDA_PIN);
    GPIO_OUTPUT_SET(I2C_SCL_PIN, 0);
    i2c_delay();
    gpio_output_set(0, 0, I2C_SDA_PIN, 0);
    return ack;
}

static void ICACHE_FLASH_ATTR pcf8574_write(uint8_t data) {
    i2c_start();
    i2c_write_byte((PCF8574_ADDR << 1) | 0);
    i2c_write_byte(data);
    i2c_stop();
}

// ==================== 1-WIRE ====================
static void ICACHE_FLASH_ATTR ow_init(void) {
    GPIO_DIS_OUTPUT(DS18B20_PIN);
    GPIO_OUTPUT_SET(DS18B20_PIN, 1);
}

static uint8_t ICACHE_FLASH_ATTR ow_reset(void) {
    GPIO_OUTPUT_SET(DS18B20_PIN, 0);
    delay_us(490);
    GPIO_DIS_OUTPUT(DS18B20_PIN);
    GPIO_OUTPUT_SET(DS18B20_PIN, 1);
    delay_us(70);
    uint8_t presence = !GPIO_INPUT_GET(DS18B20_PIN);
    delay_us(410);
    return presence;
}

static void ICACHE_FLASH_ATTR ow_write_bit(uint8_t b) {
    GPIO_OUTPUT_SET(DS18B20_PIN, 0);
    if (b) {
        delay_us(6);
        GPIO_DIS_OUTPUT(DS18B20_PIN);
        GPIO_OUTPUT_SET(DS18B20_PIN, 1);
        delay_us(64);
    } else {
        delay_us(60);
        GPIO_DIS_OUTPUT(DS18B20_PIN);
        GPIO_OUTPUT_SET(DS18B20_PIN, 1);
        delay_us(10);
    }
}

static uint8_t ICACHE_FLASH_ATTR ow_read_bit(void) {
    GPIO_OUTPUT_SET(DS18B20_PIN, 0);
    delay_us(6);
    GPIO_DIS_OUTPUT(DS18B20_PIN);
    GPIO_OUTPUT_SET(DS18B20_PIN, 1);
    delay_us(9);
    uint8_t b = GPIO_INPUT_GET(DS18B20_PIN);
    delay_us(55);
    return b;
}

static void ICACHE_FLASH_ATTR ow_write_byte(uint8_t b) {
    for (uint8_t i = 0; i < 8; i++) { ow_write_bit(b & 1); b >>= 1; }
}

static uint8_t ICACHE_FLASH_ATTR ow_read_byte(void) {
    uint8_t b = 0;
    for (uint8_t i = 0; i < 8; i++) { b >>= 1; if (ow_read_bit()) b |= 0x80; }
    return b;
}

static double ICACHE_FLASH_ATTR read_temperature(void) {
    if (!ow_reset()) return -127.0;
    ow_write_byte(0xCC);
    ow_write_byte(0x44);
    delay_us(750000);
    if (!ow_reset()) return -127.0;
    ow_write_byte(0xCC);
    ow_write_byte(0xBE);
    uint8_t lsb = ow_read_byte();
    uint8_t msb = ow_read_byte();
    int16_t raw = (msb << 8) | lsb;
    return raw / 16.0;
}

// ==================== ПИД ====================
static double ICACHE_FLASH_ATTR pid_compute(double input, double setpoint) {
    double error = setpoint - input;
    double p = PID_KP * error;
    pid_integral += error * (PID_SAMPLE_MS / 1000.0);
    double i = PID_KI * pid_integral;
    double d = PID_KD * (error - pid_last_error) / (PID_SAMPLE_MS / 1000.0);
    pid_last_error = error;
    double out = p + i + d;
    if (out > 255) { out = 255; pid_integral -= error * (PID_SAMPLE_MS / 1000.0); }
    if (out < 0)   { out = 0;   pid_integral -= error * (PID_SAMPLE_MS / 1000.0); }
    return out;
}

// ==================== FLASH ====================
static void ICACHE_FLASH_ATTR save_params(void) {
    uint32_t buf[4];
    buf[0] = FLASH_MAGIC;
    buf[1] = (uint32_t)temp_setpoint;
    buf[2] = timer_setpoint_min;
    buf[3] = 0xFFFFFFFF;

    spi_flash_erase_sector(FLASH_SECTOR);
    spi_flash_write(FLASH_SECTOR * 4096, buf, sizeof(buf));
}

static void ICACHE_FLASH_ATTR load_params(void) {
    uint32_t buf[4] = {0};
    spi_flash_read(FLASH_SECTOR * 4096, buf, sizeof(buf));

    if (buf[0] != FLASH_MAGIC) return;

    uint32_t t = buf[1];
    if (t >= TEMP_MIN && t <= TEMP_MAX) temp_setpoint = (int)t;

    uint32_t m = buf[2];
    if (m >= TIMER_MIN_MIN && m <= TIMER_MAX_MIN) timer_setpoint_min = m;
}

// ==================== ТАЙМЕР ====================
static void ICACHE_FLASH_ATTR start_timer(void) {
    timer_remaining_min = timer_setpoint_min;
    sec_counter         = 0;
    display_phase       = 0;
    pid_integral        = 0;
    pid_last_error      = 0;
    os_printf("[RUN] Start: %.1f C, %u min\n", (double)temp_setpoint, timer_setpoint_min);
}

static void ICACHE_FLASH_ATTR stop_timer(void) {
    timer_remaining_min = 0;
    sec_counter         = 0;
    pwm_duty            = 0;
    GPIO_OUTPUT_SET(SSR_PIN, 0);
    os_printf("[RUN] Stop\n");
}

// ==================== ДИСПЛЕЙ ====================
static void ICACHE_FLASH_ATTR update_display(void) {
    blank_digit[0] = blank_digit[1] = blank_digit[2] = 0;

    if (current_mode == MODE_EDIT_TEMP) {
        int t = temp_setpoint;
        if (t < 0)   t = 0;
        if (t > 99)  t = 99;
        disp_digits[0] = (t / 10) % 10;
        disp_digits[1] =  t       % 10;
        disp_digits[2] = 0;
        blank_digit[2] = 1;             // ГАСИМ ТРЕТИЙ РАЗРЯД
    }
    else if (current_mode == MODE_EDIT_TIMER) {
        uint32_t m = timer_setpoint_min;
        if (m > 999) m = 999;
        disp_digits[0] = (m / 100) % 10;
        disp_digits[1] = (m / 10)  % 10;
        disp_digits[2] =  m        % 10;
    }
    else { // MODE_RUNNING
        if (display_phase < 5) {
            int t10 = (int)(pid_input * 10.0 + 0.5);
            if (t10 < 0)   t10 = 0;
            if (t10 > 999) t10 = 999;
            disp_digits[0] = (t10 / 100) % 10;
            disp_digits[1] = (t10 / 10)  % 10;
            disp_digits[2] =  t10        % 10;
        } else {
            uint32_t m = timer_remaining_min;
            if (m > 999) m = 999;
            disp_digits[0] = (m / 100) % 10;
            disp_digits[1] = (m / 10)  % 10;
            disp_digits[2] =  m        % 10;
        }
    }
}

static void ICACHE_FLASH_ATTR all_digits_off(void) {
    gpio16_set(0);
    GPIO_OUTPUT_SET(DIGIT2_PIN, 0);
    GPIO_OUTPUT_SET(DIGIT3_PIN, 0);
}

static void ICACHE_FLASH_ATTR select_digit(uint8_t idx) {
    switch (idx) {
        case 0: gpio16_set(1); break;
        case 1: GPIO_OUTPUT_SET(DIGIT2_PIN, 1); break;
        case 2: GPIO_OUTPUT_SET(DIGIT3_PIN, 1); break;
    }
}

static void ICACHE_FLASH_ATTR display_timer_cb(void *arg) {
    all_digits_off();

    dp_blink_cnt++;
    if (dp_blink_cnt >= DP_BLINK_TICKS) {
        dp_blink_cnt = 0;
        dp_blink_state = !dp_blink_state;
    }

    update_display();

    if (blank_digit[display_idx]) {
        // Разряд погашен — сегменты не выводим, транзистор не открываем
        pcf8574_write(dp_blink_state ? 0x80 : 0x00); // только точка (по желанию)
    } else {
        uint8_t seg = seg_table[disp_digits[display_idx]];
        if (dp_blink_state) seg |= 0x80;
        pcf8574_write(seg);
        select_digit(display_idx);
    }

    display_idx = (display_idx + 1) % 3;
}

// ==================== КНОПКИ ====================
static void ICACHE_FLASH_ATTR btn_timer_cb(void *arg) {
    uint8_t up   = GPIO_INPUT_GET(BTN_UP_PIN);
    uint8_t down = GPIO_INPUT_GET(BTN_DOWN_PIN);
    uint8_t mode = GPIO_INPUT_GET(BTN_MODE_PIN);

    // UP
    if (btn_up_last == 1 && up == 0) {
        if (current_mode == MODE_EDIT_TEMP) {
            temp_setpoint += TEMP_STEP;
            if (temp_setpoint > TEMP_MAX) temp_setpoint = TEMP_MAX;
            save_params();
            os_printf("[EDIT] Temp: %d C\n", temp_setpoint);
        } else if (current_mode == MODE_EDIT_TIMER) {
            if (timer_setpoint_min + TIMER_STEP_MIN <= TIMER_MAX_MIN)
                timer_setpoint_min += TIMER_STEP_MIN;
            save_params();
            os_printf("[EDIT] Timer: %u min\n", timer_setpoint_min);
        }
    }
    btn_up_last = up;

    // DOWN
    if (btn_down_last == 1 && down == 0) {
        if (current_mode == MODE_EDIT_TEMP) {
            temp_setpoint -= TEMP_STEP;
            if (temp_setpoint < TEMP_MIN) temp_setpoint = TEMP_MIN;
            save_params();
            os_printf("[EDIT] Temp: %d C\n", temp_setpoint);
        } else if (current_mode == MODE_EDIT_TIMER) {
            if (timer_setpoint_min >= TIMER_MIN_MIN + TIMER_STEP_MIN)
                timer_setpoint_min -= TIMER_STEP_MIN;
            save_params();
            os_printf("[EDIT] Timer: %u min\n", timer_setpoint_min);
        }
    }
    btn_down_last = down;

    // MODE
    if (btn_mode_last == 1 && mode == 0) {
        btn_mode_hold = 0;
    }
    else if (btn_mode_last == 0 && mode == 0) {
        btn_mode_hold++;
        if (btn_mode_hold == LONG_PRESS_TICKS) {
            if (current_mode == MODE_RUNNING) {
                stop_timer();
                current_mode = MODE_EDIT_TEMP;
            } else if (timer_setpoint_min > 0) {
                start_timer();
                current_mode = MODE_RUNNING;
            }
            btn_mode_hold = 0xFFFF;
        }
    }
    else if (btn_mode_last == 0 && mode == 1) {
        if (btn_mode_hold < LONG_PRESS_TICKS) {
            if (current_mode == MODE_EDIT_TEMP) {
                current_mode = MODE_EDIT_TIMER;
                os_printf("[MODE] EDIT_TIMER\n");
            } else if (current_mode == MODE_EDIT_TIMER) {
                current_mode = MODE_EDIT_TEMP;
                os_printf("[MODE] EDIT_TEMP\n");
            }
        }
        btn_mode_hold = 0;
    }
    btn_mode_last = mode;
}

// ==================== ШИМ / ПИД ====================
static void ICACHE_FLASH_ATTR pwm_timer_cb(void *arg) {
    pwm_counter++;
    if (pwm_counter >= PWM_PERIOD_MS) pwm_counter = 0;
    uint32_t thr = (pwm_duty * PWM_PERIOD_MS) / 255;
    GPIO_OUTPUT_SET(SSR_PIN, (pwm_counter < thr) ? 1 : 0);
}

static void ICACHE_FLASH_ATTR pid_timer_cb(void *arg) {
    pid_input = read_temperature();

    if (current_mode == MODE_RUNNING && timer_remaining_min > 0) {
        if (pid_input > -100.0) {
            pid_output = pid_compute(pid_input, (double)temp_setpoint);
            pwm_duty = (uint32_t)pid_output;
        } else {
            pwm_duty = 0;
        }

        sec_counter++;
        if (sec_counter >= SECONDS_PER_MINUTE) {
            sec_counter = 0;
            timer_remaining_min--;
            os_printf("[RUN] T=%.1f C, left=%u min, duty=%u\n",
                      pid_input, timer_remaining_min, (unsigned)pwm_duty);
            if (timer_remaining_min == 0) {
                stop_timer();
                current_mode = MODE_EDIT_TEMP;
            }
        }

        display_phase = (display_phase + 1) % 10;
    } else {
        pwm_duty = 0;
    }
}

// ==================== HTTP ====================
static const char *http_header =
    "HTTP/1.1 200 OK\r\n"
    "Content-Type: text/html; charset=utf-8\r\n"
    "Connection: close\r\n\r\n";

static void ICACHE_FLASH_ATTR http_recv_cb(void *arg, char *data, unsigned short len) {
    struct espconn *conn = (struct espconn *)arg;
    char *body, *resp;

    if (os_strncmp(data, "GET /set?t=", 11) == 0) {
        int t = atoi(data + 11);
        if (t < TEMP_MIN) t = TEMP_MIN;
        if (t > TEMP_MAX) t = TEMP_MAX;
        temp_setpoint = t;
        save_params();
        os_printf("[WEB] Temp set: %d C\n", t);
    }
    else if (os_strncmp(data, "GET /time?m=", 12) == 0) {
        int m = atoi(data + 12);
        if (m < TIMER_MIN_MIN) m = TIMER_MIN_MIN;
        if (m > TIMER_MAX_MIN) m = TIMER_MAX_MIN;
        timer_setpoint_min = (uint32_t)m;
        save_params();
        os_printf("[WEB] Timer set: %d min\n", m);
    }
    else if (os_strncmp(data, "GET /start", 10) == 0) {
        if (current_mode != MODE_RUNNING && timer_setpoint_min > 0) {
            start_timer();
            current_mode = MODE_RUNNING;
        }
    }
    else if (os_strncmp(data, "GET /stop", 9) == 0) {
        stop_timer();
        current_mode = MODE_EDIT_TEMP;
    }

    body = (char *)os_zalloc(640);
    if (!body) { espconn_disconnect(conn); return; }

    const char *mode_str = (current_mode == MODE_RUNNING)   ? "RUNNING" :
                           (current_mode == MODE_EDIT_TEMP) ? "EDIT_TEMP" :
                                                              "EDIT_TIMER";

    os_sprintf(body,
        "<!DOCTYPE html><html><head><meta charset='utf-8'>"
        "<meta name='viewport' content='width=device-width,initial-scale=1'>"
        "<title>Dryer</title></head><body>"
        "<h2>Сушилка</h2>"
        "<p>Режим: <b>%s</b></p>"
        "<p>Температура: <b>%.1f °C</b></p>"
        "<p>Уставка темп.: <b>%d °C</b></p>"
        "<p>Уставка таймера: <b>%u мин</b></p>"
        "<p>Осталось: <b>%u мин</b></p>"
        "<p>Мощность: %u / 255</p>"
        "<form action='/set' method='get'>"
        "Температура (30..99): "
        "<input type='number' name='t' min='30' max='99' step='1' value='%d'>"
        "<input type='submit' value='OK'></form>"
        "<form action='/time' method='get'>"
        "Таймер (1..999 мин): "
        "<input type='number' name='m' min='1' max='999' step='1' value='%u'>"
        "<input type='submit' value='OK'></form>"
        "<p><a href='/start'>▶ СТАРТ</a> &nbsp; "
        "<a href='/stop'>■ СТОП</a></p>"
        "</body></html>",
        mode_str, pid_input, temp_setpoint,
        (unsigned)timer_setpoint_min, (unsigned)timer_remaining_min,
        (unsigned)pwm_duty, temp_setpoint, (unsigned)timer_setpoint_min);

    resp = (char *)os_zalloc(os_strlen(body) + 256);
    if (!resp) { os_free(body); espconn_disconnect(conn); return; }

    os_sprintf(resp, "%s%s", http_header, body);
    espconn_sent(conn, (uint8 *)resp, os_strlen(resp));

    os_free(body);
    os_free(resp);
    (void)len;
}

static void ICACHE_FLASH_ATTR http_sent_cb(void *arg) {
    espconn_disconnect((struct espconn *)arg);
}
static void ICACHE_FLASH_ATTR http_discon_cb(void *arg) { (void)arg; }
static void ICACHE_FLASH_ATTR http_recon_cb(void *arg, sint8 err) { (void)arg; (void)err; }

static void ICACHE_FLASH_ATTR http_init(void) {
    http_conn.type = ESPCONN_TCP;
    http_conn.state = ESPCONN_NONE;
    http_tcp.local_port = 80;
    http_conn.proto.tcp = &http_tcp;
    http_conn.proto.tcp->local_ip[0] = 0;
    http_conn.proto.tcp->local_ip[1] = 0;
    http_conn.proto.tcp->local_ip[2] = 0;
    http_conn.proto.tcp->local_ip[3] = 0;
    espconn_regist_recvcb(&http_conn, http_recv_cb);
    espconn_regist_sentcb(&http_conn, http_sent_cb);
    espconn_regist_disconcb(&http_conn, http_discon_cb);
    espconn_regist_reconcb(&http_conn, http_recon_cb);
    espconn_accept(&http_conn);
}

// ==================== WI-FI AP ====================
static void ICACHE_FLASH_ATTR wifi_init_ap(void) {
    struct softap_config ap;
    wifi_softap_get_config(&ap);
    os_memset(ap.ssid, 0, sizeof(ap.ssid));
    os_memset(ap.password, 0, sizeof(ap.password));
    os_memcpy(ap.ssid, AP_SSID, os_strlen(AP_SSID));
    os_memcpy(ap.password, AP_PASS, os_strlen(AP_PASS));
    ap.ssid_len = os_strlen(AP_SSID);
    ap.channel = 6;
    ap.authmode = AUTH_WPA_WPA2_PSK;
    ap.max_connection = 2;
    ap.beacon_interval = 100;
    wifi_softap_set_config(&ap);
    wifi_set_opmode(STATIONAP_MODE);
}

// ==================== INIT ====================
void ICACHE_FLASH_ATTR user_init(void) {
    uart_div_modify(0, UART_CLK_FREQ / 115200);
    os_printf("\n\n=== Dryer start ===\n");

    // --- SSR ---
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12);
    gpio_output_set(0, 0, SSR_PIN, 0);
    GPIO_OUTPUT_SET(SSR_PIN, 0);

    // --- DS18B20 ---
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_GPIO2);
    ow_init();

    // --- Кнопки ---
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO4_U, FUNC_GPIO4);
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO5_U, FUNC_GPIO5);
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO0_U, FUNC_GPIO0);
    GPIO_DIS_OUTPUT(BTN_UP_PIN);
    GPIO_DIS_OUTPUT(BTN_DOWN_PIN);
    GPIO_DIS_OUTPUT(BTN_MODE_PIN);
    PIN_PULLUP_EN(PERIPHS_IO_MUX_GPIO4_U);
    PIN_PULLUP_EN(PERIPHS_IO_MUX_GPIO5_U);
    PIN_PULLUP_EN(PERIPHS_IO_MUX_GPIO0_U); // + ВНЕШНИЙ pullup 10 кОм!

    // --- I2C (PCF8574) ---
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTMS_U, FUNC_GPIO14); // SDA
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTCK_U, FUNC_GPIO13); // SCL
    gpio_output_set(0, 0, I2C_SDA_PIN, 0);
    gpio_output_set(0, 0, I2C_SCL_PIN, 0);
    GPIO_OUTPUT_SET(I2C_SDA_PIN, 1);
    GPIO_OUTPUT_SET(I2C_SCL_PIN, 1);
    pcf8574_write(0x00); // все сегменты погашены

    // --- Разряды ---
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDO_U, FUNC_GPIO15);  // DIGIT2
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_GPIO3);  // DIGIT3 (RX)
    gpio_output_set(0, 0, DIGIT2_PIN, 0);
    gpio_output_set(0, 0, DIGIT3_PIN, 0);
    GPIO_OUTPUT_SET(DIGIT2_PIN, 0);
    GPIO_OUTPUT_SET(DIGIT3_PIN, 0);
    all_digits_off();

    // --- Загрузка параметров ---
    load_params();
    os_printf("Loaded: temp=%d C, timer=%u min\n",
              temp_setpoint, timer_setpoint_min);

    // --- Wi-Fi + HTTP ---
    wifi_init_ap();
    http_init();

    // --- Таймеры ---
    os_timer_disarm(&pwm_timer);
    os_timer_setfn(&pwm_timer, pwm_timer_cb, NULL);
    os_timer_arm(&pwm_timer, 1, 1);

    os_timer_disarm(&pid_timer);
    os_timer_setfn(&pid_timer, pid_timer_cb, NULL);
    os_timer_arm(&pid_timer, PID_SAMPLE_MS, 1);

    os_timer_disarm(&display_timer);
    os_timer_setfn(&display_timer, display_timer_cb, NULL);
    os_timer_arm(&display_timer, DISPLAY_REFRESH_MS, 1);

    os_timer_disarm(&btn_timer);
    os_timer_setfn(&btn_timer, btn_timer_cb, NULL);
    os_timer_arm(&btn_timer, BTN_POLL_MS, 1);

    os_printf("Ready. AP: %s / %s\n", AP_SSID, AP_PASS);
}
uint32 ICACHE_FLASH_ATTR
user_rf_cal_sector_set(void)
{
    enum flash_size_map size_map = system_get_flash_size_map();
    uint32 rf_cal_sec = 0;

    switch (size_map) {
        case FLASH_SIZE_4M_MAP_256_256:
            rf_cal_sec = 128 - 5;
            break;
        case FLASH_SIZE_8M_MAP_512_512:
            rf_cal_sec = 256 - 5;
            break;
        case FLASH_SIZE_16M_MAP_512_512:
        case FLASH_SIZE_16M_MAP_1024_1024:
            rf_cal_sec = 512 - 5;
            break;
        case FLASH_SIZE_32M_MAP_512_512:
        case FLASH_SIZE_32M_MAP_1024_1024:
            rf_cal_sec = 1024 - 5;
            break;
        case FLASH_SIZE_64M_MAP_1024_1024:
            rf_cal_sec = 2048 - 5;
            break;
        case FLASH_SIZE_128M_MAP_1024_1024:
            rf_cal_sec = 4096 - 5;
            break;
        default:
            rf_cal_sec = 0;
            break;
    }
    return rf_cal_sec;
}