Skip to content

Repository files navigation

Student Attendance

Электронный журнал посещаемости студентов на ESP32

Оглавление:


О проекте:

  • Проект разрабатывался в течение года как реальный инструмент для старосты университетской группы: удобный, быстрый и всегда под рукой - прямо в Telegram. Идея на первый взгляд простая, но неожиданно глубокая в реализации: ESP32 подключается к WiFi, общается с ботом в Telegram и пишет данные в Google Sheets. Всё хранилище - обычная таблица, никаких серверов и баз данных.

  • К моменту вынужденной остановки проект был в версии v0.7 и находился на завершающей стадии: дорабатывалась система подсчёта пропусков, доводился до ума пользовательский опыт, внедрялась новая, более нативная структура таблицы Google Sheet. До заморозки система несколько месяцев проработала в реальном учебном процессе.

  • Причина заморозки: жёсткая блокировка Telegram в России, в том числе в WiFi-сетях, сделала проект полностью нерабочим. Весь функционал был принципиально завязан на Telegram Bot API, прямых аналогов с таким же удобством не нашлось, а переписывать всё с нуля в условиях нехватки времени оказалось невозможным. Надеяться на разблокировку особо не приходится, поэтому развитие проекта, скорее всего, окончено.


Архитектура:

                        ┌─────────────────────────────────────────┐
                        │                  ESP32                  │
                        │                                         │
  ┌─────────────┐       │  setup()                                │
  │  Telegram   │◄─────►│    WiFi_Connect()                       │
  │  Bot API    │       │    ArduinoOTA.begin()                   │
  │  (FastBot)  │       │    FFat.begin()                         │
  └─────────────┘       │    week/settings/chat file.read()       │
         ▲              │    menu.start_page()                    │
         │              │    list.begin() -- парсинг расписания   │
         │              │                                         │
  ┌─────────────┐       │  loop() (кооперативная многозадачность) │
  │  Google     │◄─────►│    bot.tick()                           │
  │  Sheets     │       │    *.tick() x3 (FileData)               │
  │  API v4     │       │    timer.tick()                         │
  └─────────────┘       │    serviceMess.tick()                   │
                        │    ArduinoOTA.handle()                  │
                        │    [смена дня -> checkTableWeek()]      │
                        │    [таймер -> проверка кучи]            │
                        │                                         │
                        │  Входящее сообщение -> newMsg()         │
                        │    ├── Админ -> menu / briefInput       │
                        │    ├── Группа -> команды группы         │
                        │    └── Неизвестный -> отказ + лог       │
                        │                                         │
                        │  ┌──────────┐  ┌──────────────────────┐│
                        │  │  FFat    │  │  Куча (heap)         ││
                        │  │ /week    │  │  WeekInfo[2]         ││
                        │  │ /data    │  │  расписание текущей  ││
                        │  │ /settings│  │  и смежной недели    ││
                        │  └──────────┘  └──────────────────────┘│
                        └─────────────────────────────────────────┘

Функционал:

Меню редактирования пропусков:

  • Основной режим. Через инлайн-меню Telegram-бота старосте предлагается выбрать фамилию студента из списка группы, затем дату (текущая по умолчанию, или произвольная в прошлом вплоть до начала семестра), а затем тип отметки для каждой пары выбранного дня: УП (уважительная причина), неУП (неуважительная) или присутствие.

  • Учитывается разделение на две подгруппы: в меню отображаются только те пары, которые реально есть у подгруппы этого студента. Доступны быстрые действия - «Все УП», «Все неУП», «Нет пропусков» - для мгновенного выставления статуса на все пары дня.

  • Текущий статус ячеек считывается из таблицы перед показом меню, так что редактирование уже выставленных отметок - без лишних усилий.

Дерево меню:

[Главная]
├── Редактировать
│   └── [Фамилия]
│       ├── Дата: DD.MM.YY  (по умолчанию - сегодня)
│       │   └── [Месяц] -> [День в сетке календаря]
│       ├── (1) УП / неУП / Присутствие
│       ├── (2) УП / неУП / Присутствие
│       ├── ...
│       ├── Все УП  |  Все неУП  |  Нет пропусков
│       └── Поставить -> запись в Google Sheets
│
├── Подсчитать
│   └── [Фамилия]
│       ├── Общее УП ──────────────────────────────┐
│       ├── Общее неУП ────────────────────────────┤-> выбор диапазона недель -> результат
│       └── По предметам (неУП)                    │
│           └── [Предмет] ─────────────────────────┘
│
├── Статистика
│   └── Общее неУП -> диапазон недель -> таблица по всем студентам
│
└── Настройки
    └── Сроки промежуточной аттестации -> выбор диапазона недель

Сокращённый ввод:

  • Пожалуй, самая нетривиальная фича в плане реализации. Позволяет отмечать пропуски сразу списку студентов обычным сообщением без навигации по меню для каждого:
1 пара 02.03
Иванов
Петрова
Сидоров уп
  • Система разбирает сообщение: находит нужный день и пару в расписании, вычитывает уже существующие отметки из таблицы, обновляет нужные ячейки.
  • Проверяет валидность введенных данных для каждого элемента ввода - вплоть до присутствия введенной пары для конкретного студента в конкретный день в неделю нужной четности.

Поддерживаемые форматы условия:

Формат Пример Описание
Без условия Иванов Дата и пара определяются автоматически по текущему времени
Ключевое слово 1 пара сегодня / вчера / позавчера Дата вычисляется от текущего дня
Явная дата 1 пара 02.03 Конкретная дата; если день введен без месяца - принимается текущий месяц
Несколько пар 1,3 пара 15.04 Несколько пар за один ввод

Поддерживаемые суффиксы к фамилии:

Суффикс Результат (тип отметки в таблице)
(без суффикса) неУП (если в таблице ещё не стоит УП. Если стоит - считается приоритетным и не изменяется)
уп / УП УП
неуп / неУП неУП
тут / ТУТ Присутствие

Специальные режимы:

  • Режим присутствия - строка П перед списком фамилий инвертирует логику: указанные люди присутствовали, а всем остальным ставится пропуск
  • Нечёткое распознавание фамилий - допускается до N опечаток (настраивается через SURNAME_ERRORS_NUM), система сама определит, кого имел в виду пользователь, и при желании уведомит об этом

Определение текущей пары (ввод без условия):

  • Система сравнивает realTime с массивом lessons[] (время начала и конца каждой пары) с допуском ±MINUTES_OFFSET минут с обеих сторон. Если текущее время не попадает ни в одну пару - запрос отклоняется с сообщением об ошибке.

Подсчёт пропусков:

  • Позволяет подсчитать пропуски конкретного студента за произвольный диапазон недель.

Три режима:

Режим Что считает Формула
Общее УП Пропуски по уважительной причине COUNTIF(FILTER(...); "R")
Общее неУП Неуважительные пропуски COUNTIF(FILTER(...); "D")
По предмету неУП по конкретной дисциплине COUNTIFS(FILTER(...); "предмет"; FILTER(...); "D")
  • Под капотом - формулы Google Sheets (COUNTIF + FILTER), которые записываются в ячейку таблицы через Sheets API, вычисляются прямо на стороне Google и считываются обратно. Никакой локальной обработки сотен ячеек - всё на стороне облака.

Пример итоговых формул:

// Режим "Общее неУП" за диапазон недель:
=COUNTIF(FILTER(C581:U617;MOD(ROW(C581:U617)-588;23)=0);"D")

// Режим "По предмету":
=COUNTIFS(FILTER(C244:U284;MOD(ROW(C244:U284)-244;24)=0);"Физ практикум (лб)";
          FILTER(C244:U284;MOD(ROW(C244:U284)-244-2;24)=0);"D")

Статистика по группе:

  • Групповой режим: запускает подсчёт последовательно для каждого студента из списка и возвращает итоговую таблицу в Telegram в форматированном виде (моноширинный шрифт через markdown):
Фио:               Нки:
Иванов             --------- 3
Петрова            --------- 1
Сидоров            --------- 7

Автодостроение таблицы:

  • При каждом включении ESP32 сравнивает дату последней записанной в таблице недели с реальной датой. Если прошло несколько недель - система автоматически достраивает все пропущенные через batchUpdate API: копирует структуру предыдущей недели той же чётности (нечётная/чётная - расписание у них может различаться), очищает ячейки пропусков и проставляет актуальные даты в заголовки дней. Всё это без единого касания таблицы вручную.

Один цикл достройки одной недели - три запроса batchUpdate:

  1. copyPaste - скопировать блок строк предыдущей недели той же чётности в позицию новой недели (копируется всё: форматирование, структура, номера пар)
  2. repeatCell - очистить только ячейки с пропусками студентов в скопированном блоке
  3. updateCells - записать актуальные даты в заголовки учебных дней (формат: Вторник, 22.02.26)
  • Перед добавлением нового учебного дня в заголовок вычисляется его дата через sumDate() с учётом пропуска выходных дней посреди недели.

Вспомогательные команды:

Команды в чате с администраторами и в группе:

Команда Доступ Описание
/start Администраторы Приветствие; остальным администраторам - уведомление о входе
/res Администраторы Немедленная перезагрузка ESP32
/comms Все Список доступных команд
/список Все Общий список группы с нумерацией
/список1 Все Список первой подгруппы
/список2 Все Список второй подгруппы
Кинуть кубик Все Бросает виртуальный кубик (1-6)
  • Контроль доступа: сообщения от неизвестных пользователей (не в Admins[] и не в Groups[]) обрабатываются отдельно - в первый раз отправляется вежливое перенаправление к старосте, chatID запоминается и все повторные сообщения от этого пользователя игнорируются без ответа. Об обращении всегда уведомляется error_chat.

Технические особенности:

Управление памятью:

  • malloc / realloc / free для расписания. Расписание двух недель хранится в куче. В конструкторе WeekInfo под каждый день выделяется буфер на MAX_LESSONS_IN_DAY элементов LessInfo. После парсинга расписания из Sheets для каждого дня выполняется shrink-to-fit через realloc - буфер сжимается до фактического числа пар. Дни без пар - free() + nullptr.

  • Своп указателей при смене чётности. Объекты WeekInfo хранятся в массиве week_object[2], а week[2] - это массив указателей на них. Когда нечётная неделя сменяется чётной (или наоборот), вместо копирования данных просто меняются местами два указателя - week[0] и week[1]. Расписание при этом остаётся в памяти нетронутым.

  • MemoryControl. Класс для мониторинга свободной RAM: в конструкторе фиксирует текущий объём кучи (_start_heap, _keep_heap), check() проверяет не упала ли куча ниже порога MIN_FREE_HEAP (50 КБ) и обновляет _keep_heap до минимума, getDiff() возвращает, сколько байт было занято с момента создания объекта, resetKeep() обновляет базовую точку. Используется на протяжении всех "тяжёлых" операций с прерыванием при нехватке. Периодически вызывается и в loop() для фонового мониторинга.

  • PROGMEM. Строковые константы, обращение к которым идёт только на чтение - названия месяцев (months[]), дней недели (DaysOfWeek[]), количество дней в каждом месяце (day_month[]) - хранятся в PROGMEM (flash), а не в RAM. Критично для платформы с ограниченным heap.

  • DeleteTimer. Класс для отложенного удаления сообщений через Telegram Bot API. Хранит динамический массив структур timer_data (message_id, chatID, период, стартовый millis). tick() опрашивается в loop() каждые 200 мс, при срабатывании помечает элемент message_id = -1. MyRealloc() пересобирает массив, выбрасывая помеченные элементы - без лишних free/malloc, только один calloc на новый блок.


Google Sheets API:

  • Три вида запросов:

    • GSheet.values.get() - чтение значений диапазона
    • GSheet.values.update() - запись значений в диапазон (выставление отметок, запись формулы)
    • GSheet.batchUpdate() - структурные операции (copyPaste, repeatCell, updateCells) - только при достройке недель
  • Логика повторных попыток. Все API-вызовы обёрнуты в while (!api_call() && tries < TryNum) tries++. При исчерпании попыток - сообщение об ошибке в error_chat. Значения GetTryNum и SetTryNum задаются в settings.h.

  • Pre-refresh токена. GSheet.setPrerefreshSeconds(10 * 60) - токен сервисного аккаунта обновляется за 10 минут до истечения, чтобы не получить ошибку авторизации в середине операции.

  • Кэширование ширины таблицы. После первого чтения новой таблицы реальная ширина блока недели (количество столбцов) записывается в settings.table_width[2] и сохраняется в FFat. Это гарантирует попадание в нужный диапазон за один запрос при последующих чтениях, без дочитывания лишних данных.

  • Обработка пустых ячеек. Google Sheets API v4 не включает в JSON-ответ ячейки с пустым значением - они просто отсутствуют в поле values. Код явно достраивает недостающие пути через FirebaseJson::set(), чтобы не получить invalidPath при последующем обращении к "несуществующей" ячейке диапазона.

  • charOffset(col, n). Функция для вычисления "адреса" конкретного столбца, позволяет сдвигать буквенный адрес столбца col на n позиций вправо. Возвращает букву/-ы столбца, сдвинутую/-ые на n позиций от заданной. Поддерживает однобуквенные (A-Z) и двухбуквенные (AA-ZZ) адреса: при переполнении последней буквы за Z автоматически инкрементируется префиксная буква.

  • columnLetterToIndex(col). Конвертирует буквенное обозначение столбца в 0-based числовой индекс, который требует batchUpdate - например, A -> 0, C -> 2, AA -> 26.

  • Координаты таблицы параметризованы. Все опорные точки структуры Google Sheet - имя листа (SheetName), столбец и строка заглавной ячейки недели (weekInfo_c, weekInfo_i), начало списка студентов (people_list_c, people_list_i), шаг между неделями в строках (offset) - вынесены в settings.h. Логика адресации не содержит жёстко закодированных координат. В этом одна из ключевых уникальностей проекта - без жестко заданных границ размещения таблицы, с помощью нескольких опорных точек и математики алгоритм корректно работает с любым расписанием любой группы без перенастройки.

  • Подсчёт через формулы. Строка формулы собирается динамически в зависимости от режима (COUNTIF или COUNTIFS), диапазона недель и строки конкретного студента. Формула записывается в ячейку за пределами рабочей области таблицы, после чего та же ячейка читается обратно - Google вычислил формулу и вернул числовой результат. Никакой локальной обработки.


Telegram и OTA:

  • #define ATOMIC_FS_UPDATE в начале главного файла включает поддержку сжатых прошивок - это позволяет доставлять OTA-обновления через Telegram даже по медленному каналу.

  • OTA в режиме CriticalError(). Логика использования функции - плохо! Должна была быть исправлена при выходе финальной версии. При критической ошибке устройство уходит в бесконечный for(;;) с единственной активной операцией ArduinoOTA.handle(). Прошивка с фиксом может быть накатана без физического доступа к железу.

  • Синхронизация времени через Telegram. bot.getTime(3) возвращает структуру FB_Time с реальным временем (UTC+3). Используется: для определения текущей пары в сокращённом вводе, при вычислении дат в checkTableWeek(), для валидации дат в конструкторе Date. Синхронизация происходит при старте и обновляется в каждой итерации loop().

  • Многоадминный режим. Массив Admins[] содержит Telegram ID всех чатов администраторов. Статусное сообщение и сообщение-меню рассылаются и редактируются сразу во всех чатах - старосты и их замы видят одно и то же в реальном времени. Поддерживается одновременная работа нескольких администраторов (не многопоточность! Она планировалась в финальной версии).

  • Автоочистка сервисных сообщений. bot.clearServiceMessages(true) в setup() - автоматическое удаление служебных уведомлений Telegram вида "... закрепил сообщение" и аналогичных, чтобы они не засоряли чат.

  • Период опроса бота - bot.setPeriod(50) - 50 мс. Кооперативная многозадачность: в loop() последовательно вызываются bot.tick(), три FileData::tick(), timer.tick(), serviceMess.tick(), ArduinoOTA.handle(). Блокирующих delay() в основном цикле нет.


Хранилище и персистентность:

  • Три файла в FFat:
Файл Переменная Содержит
/weekdata.dat week_off Номер текущей недели от начала семестра
/data.dat chat_settings ID статусного сообщения и меню в каждом чате администратора
/settings.dat settings Диапазон промежуточной аттестации, ширина таблиц по чётности
  • week_off - критическая переменная. Счётчик числа недель с начала семестра - основа для вычисления координат любой строки в таблице. Неверное значение означает запись пропусков не в ту строку. Комментарий в коде прямо предупреждает: "НЕ ЗНАЕШЬ - НЕ МЕНЯЙ!" Сохраняется в FFat при каждом изменении через FileData::updateNow() или tick().

  • Сохранение ID сообщений. ID статусного сообщения и сообщения с меню для каждого чата администратора сохраняются в FFat. После перезагрузки ESP32 не создаёт новые сообщения, а редактирует уже существующие - чат остаётся чистым. Логика в start_page() определяет по результату file.read() (FDstat_t), нужно ли отправить сообщение заново или достаточно его отредактировать.

  • WiFi. Подключение с WiFi.setAutoReconnect(true) - при потере связи модуль переподключается сам. WiFi.setMinSecurity(WIFI_AUTH_OPEN) - совместимость с открытыми сетями. Если при старте WiFi не поднялся за WIFI_RES_PERIOD миллисекунд - ESP32 перезагружается.


Структуры данных и утилиты:

  • Date. Структура день/месяц/год с конструктором-валидатором: проверяет диапазоны дня, месяца и года, с учётом корректного числа дней в каждом месяце через getDayInMonth(). Февраль учитывает високосность через StampUtils::isLeap(year). Перегружены operator= и operator<. sumDate(Date*, int) прибавляет или вычитает произвольное число дней с корректным переходом месяца и года в обе стороны.

  • Time. Класс для хранения времени (часы + минуты). Перегружены арифметические операторы: + и - с другим Time или с количеством минут (uint8_t), с корректным переходом через полночь. Все операторы сравнения: <, >, ==, !=, <=, >=. Используется для определения, какая пара сейчас идёт в сокращённом вводе без условия.

  • SetInfo (nka) и CountInfo (count). Структуры-контексты для текущей операции. SetInfo хранит: фамилию, строку Н-ок (nki), дату, день недели, координаты ячейки в таблице (posC, posI), подгруппу, чётность недели. CountInfo хранит: фамилию, индекс в списке, подгруппу, предмет, режим подсчёта, итоговый результат.

  • WeekInfo / DailySchedule / LessInfo. Иерархия структур расписания. WeekInfo - одна неделя: дата понедельника, чётность, массив DailySchedule на 7 дней. DailySchedule - один день: количество пар, динамический массив LessInfo. LessInfo - одна пара: номер, принадлежность подгруппе (0 - только первая, 1 - только вторая, 2 - обе).

  • getNIndex(). Вычисляет координаты ячейки в таблице (posC, posI) для заданной фамилии и даты из nka. Через StampUtils::dateToDays2000() вычисляет разницу с текущей неделей в днях, из неё получает weeks_ago и days_ago, из них - чётность нужной недели и строку. Столбец вычисляется суммированием числа пар всех предшествующих дней - алгоритм содержит "магические числа", смысл которых раскрыт в ранних коммитах (до февраля 2026).

  • CheckSurnameMatch(). Побайтовое сравнение двух строк с допустимым числом несовпадений max_errors. Возвращает 1 при точном совпадении, 2 при совпадении с допустимыми ошибками, 0 при несовпадении. Корректно работает с кириллицей в UTF-8 - сравнение идёт по 2 байта (один кириллический символ).


Пользовательский интерфейс:

  • ServiceMess. Класс для управления "статусным сообщением" в чатах администраторов. Вместо рассылки новых сообщений при каждом действии одно и то же сообщение редактируется - bot.editMessage() во всех чатах сразу. edit(text, del_period) - обновляет текст и опционально запускает таймер авто-сброса через del_period мс. tick() опрашивается в loop() и при срабатывании таймера очищает сообщение, убирая временный статус.

  • Конечный автомат меню. Класс Menu с переменной way типа String, хранящей текущий путь в дереве состояний (например, "011" - ветка редактирования, уровень выбора пары). Разветвление по way.startsWith("01"), way.startsWith("02") и т.д. Кнопка «Назад» удаляет последний символ (way.remove(way.length()-1)), «На главную» - сбрасывает в "0". Флаг ret_command разделяет "вернуться на эту же страницу" и "перейти назад".

  • Выбор диапазона недель. В режимах подсчёта и статистики реализован интерактивный пикер диапазона: список всех прошедших недель отображается кнопками, пользователь нажимает нужную и затем отмечает её как "Начало", "Конец" или "Начало и конец". Текущие границы визуально выделяются символами START_SYMBOL / END_SYMBOL / STARTEND_SYMBOL прямо в тексте кнопок. Этот же пикер используется в настройках промежуточной аттестации.

  • Отображение календаря при выборе даты. При выборе произвольной даты в меню редактирования бот отображает месяц в виде сетки: заголовок пн-вт-ср-чт-пт-сб-вс, числа расставлены по столбцам с учётом смещения первого дня месяца (pre_offset пустых ячеек). Это создает визуально красивый и правильный с точке зрения визуального отображения дат и соответствия их реальным дням недели интерфейс. Отображаются числа только от начала семестра или начала месяца до последнего дня текущей недели. Исключает выставление пропусков на некорректные даты. Пустые ячейки для выравнивания - пробел.


Нам понадобится:

Железо:

  • ESP32 (с достаточным объёмом RAM - желательно >= 320 КБ heap). Автор использовал ESP32-S3 N16R8.

Сервисы:

  • Telegram-бот (токен от @BotFather)
  • Аккаунт Google с таблицей в Google Sheets специфичной структуры
  • Сервисный аккаунт Google Cloud с доступом к Sheets API и приватным ключом

Библиотеки для Arduino IDE:

  • FastBot
  • ESP_Google_Sheet_Client
  • FileData
  • Stamp / StampUtils
  • StringUtils

Настройка и запуск:

Caution

Проект не готов к запуску в режиме «скачал -> прошил -> работает». Ниже объясняю, почему это так, и что для этого нужно.

  1. Файл settings.h содержит всё: WiFi-реквизиты, токен бота, ключ сервисного аккаунта Google, ID таблицы и листа, список студентов с подгруппами, расписание пар по времени, список предметов. В репозитории вместо него лежит settings-example.h - заглушка без реальных значений. Переименуйте его в settings.h и заполните своими данными перед компиляцией.

  2. Проект работает с таблицей конкретной структуры в Google Sheets. Шаблона в репозитории нет - воссоздать её без углублённого чтения кода не получится. Но это и не нужно, см. ниже. Поля weekInfo_c, less_num_c, people_list_c, offset в settings-example.h описывают координаты данных в таблице.

  3. Для работы с Google Sheets API необходим сервисный аккаунт в Google Cloud Console с разрешёнными API: Sheets API и Drive API. Приватный ключ аккаунта прописывается в PRIVATE_KEY.


Важно о запуске:

Important

Даже при полной настройке проект не будет работать в России без проксирования Telegram-трафика на стороне ESP32 - именно блокировка Telegram и послужила причиной заморозки разработки. При наличии VPN/прокси в сети, к которой подключена ESP32, ограничений нет.


Статус проекта:

  • Версия на момент заморозки: v0.7
  • Проект находится в стадии незавершенной неопределенности, чтобы оживить его даже с обходами блокировок могут потребоваться значительные ресурсы доработки.
  • Статус: разработка остановлена, баги не исправляются
  • Часть задач и известных проблем задокументирована в Tasks.ino
  • Pull Request'ы и Issues приветствуются, хотя реакция на них может быть не оперативной.

Примечание:

Note

Проект написан одним автором в свободное время, и занял суммарно около года работы (только ручной кодинг в целях обучения, только самостоятельные идеи и из воплощение). Большая часть нетривиальных решений продиктована реальными ограничениями: скромным объёмом RAM на ESP32, необходимостью поддерживать живой пользовательский опыт параллельно с разработкой.

Проект не претендует на идеальность архитектуры - зато честно отражает реальный инженерный процесс: парсинг внешнего API на микроконтроллере, управление динамической памятью в условиях её дефицита, реализация конечного автомата пользовательского интерфейса и нечёткий поиск по строкам с поддержкой кириллицы. Всё это работало в реальной группе и решало реальную задачу.

Проект является вызовом самому себе, автор преследовал мысль проверить себя в решении столь объемной задачи ресурсами относительно слабого МК. Цель была достигнута, но жизнь - жестокая штука, и проект так и не был запущен.

About

Прототип электронного журнала на ESP32

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages