// =============================================================================
//  Урок 9 «Каналы связи: UART и I2C» (7 класс): одна программа на весь урок
// =============================================================================
//
//  Плату прошивают ОДИН раз до урока. На уроке перепрошивать ничего не нужно:
//  символ, скорость UART и адрес I2C меняются командами в мониторе порта.
//
//  Программа делает две вещи одновременно:
//
//   1) UART (практика А). Каждые 100 мс отправляет один символ (сначала «A»)
//      через Serial2 на вывод GPIO17 со скоростью 9600 бод.
//      xSTEM Scope: опыт «UART 9600: один байт»; К1 красный — GPIO17, синий — GND.
//
//   2) Медленный I2C (практика Б). Каждые 300 мс спрашивает у датчика AHT20
//      (адрес 0x38) его байт состояния. Такт шины 1 кГц — в 100 раз медленнее
//      обычного, чтобы каждый бит был виден на датчике НАУРА (точка раз в 10 мкс).
//      xSTEM Scope: опыт «Медленный I2C: адрес датчика»;
//      К1 красный — SDA (GPIO21), К2 красный — SCL (GPIO22), оба синих — GND.
//
//  Команды в мониторе порта (115200 бод, в конце строки Enter):
//
//   char B       — отправлять по UART символ B (одна латинская буква или цифра)
//   text opus    — отправлять по UART слово или фразу подряд, до 24 символов:
//                  «text claude one love» — 15 байт, 15,6 мс, целиком в кадре на ×1
//   baud 19200   — скорость UART: 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200
//   addr 39      — спрашивать по I2C адрес 0x39 (шестнадцатеричный, от 08 до 77)
//   ?            — показать, что сейчас настроено
//
//  Прошить:  pio run -e lesson9 -t upload
//  Монитор:  pio device monitor
//
//  Модуль AHT20 + BMP280 подключён к ESP32 как обычно: VCC — 3V3, GND — GND,
//  SDA — GPIO21, SCL — GPIO22. На модуле уже есть подтягивающие резисторы;
//  программа на всякий случай включает и встроенные резисторы ESP32.
// =============================================================================
#include <Arduino.h>

// ----------------------------------------------------------------- выводы ---
constexpr int UART_RX = 16;   // приём Serial2 (на уроке не нужен, но вывод занят)
constexpr int UART_TX = 17;   // передача Serial2: сюда К1 в практике А
constexpr int SDA_PIN = 21;   // линия данных I2C: сюда К1 в практике Б
constexpr int SCL_PIN = 22;   // линия такта I2C: сюда К2 в практике Б

// ---------------------------------------------------------------- времена ---
// Символ раз в 100 мс: кадр UART длится около 1 мс, между кадрами линия
// успевает «отдохнуть» в высоком уровне, и синхронизация ловит каждый кадр
// с самого начала, со старт-бита.
constexpr uint32_t UART_PERIOD_MS = 100;
// Запрос к датчику раз в 300 мс: один разговор по медленной шине длится около
// 20 мс, остальное время шина молчит — так кадр осциллографа почти всегда
// начинается со СТАРТа.
constexpr uint32_t I2C_PERIOD_MS = 300;
// Половина такта I2C. 500 мкс низкий SCL + 500 мкс высокий SCL = 1 мс = 1 кГц.
// На обычной шине (100 кГц) это было бы 5 мкс — датчик НАУРА такого не увидит.
constexpr uint32_t HALF_US = 500;

// ---------------------------------------------- то, что меняется командами ---
String   uartText = "A";      // что шлём по UART: 'A' = 65 = 0x41 = 01000001; text — целая фраза
constexpr unsigned TEXT_MAX = 24;   // 24 байта по 1,04 мс помещаются в кадр датчика (30,7 мс)
uint32_t uartBaud = 9600;     // скорость UART, бод = бит в секунду; бит = 1/9600 с ≈ 104 мкс
uint8_t  i2cAddr  = 0x38;     // кого спрашиваем по I2C: у AHT20 адрес 0x38 = 0111000
int      lastAck  = -1;       // прошлый ответ датчика (-1 = ещё не спрашивали),
int      lastStatus = -1;     // чтобы печатать в монитор только изменения

// =============================================================================
//  UART
// =============================================================================
// Всю работу делает встроенный в ESP32 приёмопередатчик UART. Программа только
// кладёт байт в очередь, а он сам выдаёт на GPIO17 кадр из 10 бит:
//   старт-бит 0  →  8 бит данных, МЛАДШИЙ ПЕРВЫМ  →  стоп-бит 1,
// каждый бит ровно 1/скорость секунды. Между кадрами линия держит 1 (покой).
// Для 'A' на проводе: 0 | 1 0 0 0 0 0 1 0 | 1.
// Фраза уходит байтами подряд: стоп-бит одного байта сразу сменяется старт-битом следующего.
void uartSend() {
  Serial2.print(uartText);
}

// =============================================================================
//  Медленный I2C
// =============================================================================
// На шине I2C у обеих линий «открытый сток»: любое устройство умеет только
// ПРИЖАТЬ линию к земле (0) или ОТПУСТИТЬ её. Отпущенную линию поднимает к
// 3,3 В подтягивающий резистор, поэтому 1 никто не «выдаёт», она получается
// сама. Так на одном проводе могут сидеть много устройств и не мешать
// друг другу: если двое тянут к нулю, ничего не сгорит.

// Отпустить линию: вывод становится входом, 1 делает резистор.
void release(int pin) { pinMode(pin, INPUT_PULLUP); }
// Прижать линию к земле: вывод становится выходом с уровнем 0.
void pullLow(int pin) { pinMode(pin, OUTPUT); digitalWrite(pin, LOW); }
// Подождать половину такта.
void halfTick() { delayMicroseconds(HALF_US); }

// Один такт шины — один бит.
//   1. Пока SCL низкий, выставляем бит на SDA (данные меняются только здесь).
//   2. Поднимаем SCL: бит «действует», его читают все устройства на шине.
//   3. Смотрим, что сейчас на SDA: так плата слышит ответ датчика.
//   4. Опускаем SCL: такт закончен.
// Правило для учеников: бит читается там, где SCL высокий.
int clockBit(int bit) {
  if (bit) release(SDA_PIN); else pullLow(SDA_PIN);  // 1. бит на SDA
  halfTick();
  release(SCL_PIN);                                  // 2. SCL вверх
  halfTick();
  int seen = digitalRead(SDA_PIN);                   // 3. что на SDA
  pullLow(SCL_PIN);                                  // 4. SCL вниз
  return seen;
}

// Один разговор с датчиком. На экране xSTEM Scope он выглядит так:
//   СТАРТ | такты 1–7: адрес | такт 8: «читать» | такт 9: ACK |
//   такты 10–17: байт состояния | такт 18: NACK | СТОП
// Возвращает true, если датчик отозвался; его байт состояния кладёт в status.
bool i2cAsk(uint8_t &status) {
  // СТАРТ: SDA падает, пока SCL высокий. В остальное время SDA меняется только
  // при низком SCL, поэтому такой спад все узнают как «начало разговора».
  // Синхронизация xSTEM Scope (спад К1 = SDA) ловит именно его.
  pullLow(SDA_PIN);
  halfTick();
  pullLow(SCL_PIN);

  // Такты 1–8: 7 бит адреса, СТАРШИЙ ПЕРВЫМ (в UART было наоборот!),
  // и 8-й бит: 1 = «хочу читать», 0 = «хочу писать».
  // Для 0x38: 0 1 1 1 0 0 0 и 1, вместе байт 0x71.
  uint8_t first = (i2cAddr << 1) | 1;
  for (int i = 7; i >= 0; i--) clockBit((first >> i) & 1);

  // Такт 9: плата отпускает SDA и слушает. Датчик, узнавший свой адрес,
  // прижимает SDA к нулю — это ACK, «я дома». Если устройства с таким
  // адресом нет, линию никто не прижмёт, резистор оставит 1 — это NACK.
  bool ack = clockBit(1) == 0;

  status = 0;
  if (ack) {
    // Такты 10–17: теперь SDA ведёт датчик, плата только читает 8 бит.
    // Бит 0x08 = 1 — датчик откалиброван; бит 0x80 = 0 — датчик свободен.
    for (int i = 0; i < 8; i++) status = (status << 1) | clockBit(1);
    // Такт 18: плата не прижимает SDA (NACK) — «спасибо, больше не нужно».
    clockBit(1);
  }
  // Если ответа не было, разговор обрывается сразу после 9-го такта.

  // СТОП: SDA растёт, пока SCL высокий. Шина снова свободна.
  pullLow(SDA_PIN);
  halfTick();
  release(SCL_PIN);
  halfTick();
  release(SDA_PIN);
  return ack;
}

// =============================================================================
//  Монитор порта: подсказки и команды
// =============================================================================
void printSettings() {
  if (uartText.length() == 1) {
    char c = uartText[0];
    Serial.printf("UART: символ '%c' (код %d = 0x%02X), %lu бод, бит %.1f мкс\n",
                  c, c, c, (unsigned long)uartBaud, 1e6 / uartBaud);
  } else {
    // Коды каждого символа — чтобы ученики сверили свою расшифровку с экрана
    Serial.printf("UART: текст '%s', %u байт = %.1f мс, %lu бод, бит %.1f мкс\n      коды:",
                  uartText.c_str(), uartText.length(), uartText.length() * 10e3 / uartBaud,
                  (unsigned long)uartBaud, 1e6 / uartBaud);
    for (char c : uartText) Serial.printf(" %c=%02X", c, c);
    Serial.println();
  }
  Serial.printf("I2C:  адрес 0x%02X, такт 1 кГц\n", i2cAddr);
}

void printHelp() {
  Serial.println("Команды: char B | text opus | baud 19200 | addr 39 | ?");
}

// Разбор одной строки, например "addr 39".
void handle(String line) {
  line.trim();
  if (line == "?") { printSettings(); return; }

  int space = line.indexOf(' ');
  String cmd = space < 0 ? line : line.substring(0, space);
  String arg = space < 0 ? "" : line.substring(space + 1);
  arg.trim();

  if (cmd == "char" && arg.length() == 1) {
    uartText = arg;
    printSettings();
  } else if (cmd == "text" && arg.length() >= 1 && arg.length() <= TEXT_MAX) {
    uartText = arg;
    printSettings();
  } else if (cmd == "text") {
    Serial.printf("Текст от 1 до %u символов, например: text opus\n", TEXT_MAX);
  } else if (cmd == "baud") {
    const long allowed[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200};
    long b = arg.toInt();
    for (long a : allowed) {
      if (b == a) {
        uartBaud = b;
        Serial2.updateBaudRate(uartBaud);  // приёмопередатчик сразу переходит на новую скорость
        printSettings();
        return;
      }
    }
    Serial.println("Скорость: 1200, 2400, 4800, 9600, 19200, 38400, 57600 или 115200");
  } else if (cmd == "addr") {
    long a = strtol(arg.c_str(), nullptr, 16);  // «39» и «0x39» читаются одинаково
    if (a >= 0x08 && a <= 0x77) {               // 7-битные адреса, кроме служебных
      i2cAddr = a;
      lastAck = -1;                             // напечатать ответ по новому адресу
      printSettings();
    } else {
      Serial.println("Адрес шестнадцатеричный, от 08 до 77, например: addr 38");
    }
  } else {
    printHelp();
  }
}

// Копим символы до Enter, не останавливая программу: UART и I2C работают дальше.
void readMonitor() {
  static String line;
  while (Serial.available()) {
    char c = Serial.read();
    if (c == '\n' || c == '\r') {
      if (line.length()) handle(line);
      line = "";
    } else {
      line += c;
    }
  }
}

// =============================================================================
//  Запуск и главный цикл
// =============================================================================
void setup() {
  Serial.begin(115200);                                   // монитор порта
  Serial2.begin(uartBaud, SERIAL_8N1, UART_RX, UART_TX);  // UART урока: 8 бит, без чётности, 1 стоп-бит
  release(SDA_PIN);                                       // шина свободна: обе линии отпущены,
  release(SCL_PIN);                                       // резисторы держат на них 1
  delay(100);                                             // AHT20 просыпается после включения питания
  Serial.println("Урок 9: UART на GPIO17, медленный I2C на GPIO21 (SDA) и GPIO22 (SCL)");
  printSettings();
  printHelp();
}

void loop() {
  static uint32_t lastUart = 0, lastI2c = 0;
  uint32_t now = millis();

  readMonitor();

  // Пора отправить символ по UART?
  if (now - lastUart >= UART_PERIOD_MS) {
    lastUart = now;
    uartSend();
  }

  // Пора спросить датчик по I2C?
  if (now - lastI2c >= I2C_PERIOD_MS) {
    lastI2c = now;
    uint8_t status;
    bool ack = i2cAsk(status);
    // Печатаем только когда ответ изменился, чтобы монитор не заливало строками.
    if ((int)ack != lastAck || (ack && status != lastStatus)) {
      if (ack) Serial.printf("0x%02X ответил (ACK), байт состояния 0x%02X\n", i2cAddr, status);
      else     Serial.printf("0x%02X: нет ответа (NACK)\n", i2cAddr);
      lastAck = ack;
      lastStatus = status;
    }
  }
}
