Что-то часто стал заглядывать в профиль после каждой новой публикации. Так вот я и решил сделать табло, которое стояло бы на столе, и показывало место в рейтинге, карму, ну и само значение очков рейтинга.
Для желающих повторить подразумевается как возможность сборки из модулей, так и нормальная железка. Но устройство в общем очень даже универсальное, полностью совместимое с Arduino IDE, достаточно воткнуть USB и можно шить. Порог вхождения минимальный. А почему универсальное- только изменением кода можно парсить что угодно с любого сайта.
Раз уж устройство будет включено всегда и стоять на столе- оно показывает еще и время, температуру и влажность воздуха.

API хабра
… которого нет 🙁
Тут я задумался как получать данные. Поддержка сказала что АПИ задача не приоритетная, но где‑то в планах существует. Значит придется парсить по якорям. То‑есть ищем кусок уникальных данных перед нужным значением, и ориентируясь по нему извлекаем нужные.
Собственно код
Весь код написан в среде ArduinoIDE. Основная функция, конечно‑ получение значений. Голый код страницы профиля пользователя весит около 120кБ. Хранить его можно разве что в файловой системе, но особого смысла нет. Для класса Stream в Arduino IDE есть отличная функция find(), которой мы и воспользуемся.
Код функции парсинга с комментариями
if ((WiFi.status() == WL_CONNECTED)) { //Если есть подключение к Wifi http.begin(client, SURL + USER + "/"); //Открываем HTTP соединение delay(10); int httpCode = http.GET(); //Производим GET запрос delay(10); Serial.print("httpCode"); Serial.println(httpCode); if (httpCode==200) { //Если ответ 200 WiFiClient* stream = http.getStreamPtr(); //Пребразуем данные в поток Stream if (stream->available()) { //Если поток доступен //----------------карма stream->find(R"rawliteral(karma__votes_positive">)rawliteral"); //Ищем якорь for (int i = 0; i < 5; i++) { //Отступ от якоря stream->read(); } for (byte i = 0; i < 5; i++) { //Читаем нужное количество символов KARMA[i] = stream->read(); } //----------------рейтинг stream->find(R"rawliteral(tm-rating__counter">)rawliteral"); for (byte i = 0; i < 7; i++) { RATING[i] = stream->read(); } //----------------позиция stream->find("В рейтинге"); for (int i = 0; i < 118; i++) { stream->read(); } for (byte i = 0; i < 4; i++) { RatingPos[i] = stream->read(); } Serial.println(KARMA); Serial.println(RATING); Serial.println(RatingPos); Serial.println("END"); } delay(10); Serial.println(); Serial.print("[HTTP] connection closed or file end.\n"); } else { Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str()); } http.end(); delay(10); }
и
Весь код
Главный
String USER = "ENGIN33RRR"; //Имя пользователя const char ssid[] = "Eng"; //SSID const char password[] = "123456789h"; //Пароль от WiFi const char* ntpServer1 = "pool.ntp.org"; //Первый сервер времени const char* ntpServer2 = "time.nist.gov"; //Второй сервер времени const long gmtOffset_sec = 21600; //Часовой пояс в секундах String SURL = "https://habr.com/ru/users/"; //Начало адреса до страницы пользователя //LОбъявляем Дисплей #include <GxEPD2_BW.h> #define USE_VSPI_FOR_EPD #define GxEPD2_DISPLAY_CLASS GxEPD2_BW #define MAX_DISPLAY_BUFFER_SIZE 65536ul #define GxEPD2_DRIVER_CLASS GxEPD2_290_T94_V2 #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8)) GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/5, /*DC=*/17, /*RST=*/16, /*BUSY=*/4)); //Шрифты #include <Fonts/FreeMonoBold9pt7b.h> #include <Fonts/FreeMonoBold12pt7b.h> #include <Fonts/FreeMonoBold18pt7b.h> #include <Fonts/FreeMonoBold24pt7b.h> #include <Fonts/FreeSerifBoldItalic18pt7b.h> //Библиотеки Wifi, HTTP и времени #include <WiFi.h> #include <WiFiClient.h> #include <WiFiClientSecure.h> WiFiClientSecure client; #include <HTTPClient.h> HTTPClient http; #include "time.h" #include "sntp.h" //Библиотека для датчика темпреатуры/влажности #include <Adafruit_Sensor.h> #include <DHT.h> #define DHTPIN 27 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); //Переменные для хранения данных и фиксации изменений String KARMA = "000"; String RATING = "000.0"; String RatingPos = "999"; String KARMA1; String RATING1; String RatingPos1; float Temp; float Hum; float HumR; float TempR; char TimeDisp[9]; byte count; bool flag; long ms; long ms1; bool blink; bool noWiFi; byte WS; void setup() { xTaskCreatePinnedToCore( Graph, //Функция потока "Task2", //Название потока 16000, //Стек потока NULL, //Параметры потока 1, //Приоритет потока NULL, //Идентифкатор потока 0); //Ядро для выполнения потока delay(500); xTaskCreatePinnedToCore( FileUpdate, //Функция потока "Task1", //Название потока 10000, //Стек потока NULL, //Параметры потока 2, //Приоритет потока NULL, //Идентифкатор потока 1); //Ядро для выполнения потока delay(500); } void FileUpdate(void* pvParameters) { Serial.begin(115200); //Инициализация UART //http.setReuse(true); http.setTimeout(3000); http.setReuse(true); Connect(); //Подключаемся к WiFi client.setInsecure(); //Игнорируем сертификаты HTTPS for (;;) //Цикл потока { if (WiFi.status() == WL_CONNECTED) { //Если есть подключение if (WiFi.RSSI() > -60) { //переводим уровень сигнала для значка WS = 2; } else if (WiFi.RSSI() > -70) { WS = 1; } else { WS = 0; } noWiFi = 0; findVAR(); //Функция поиска значений } else { Reconnect(); //Переподключить Wifi noWiFi = 1; } if (!KARMA1.equals(KARMA) || !RATING1.equals(RATING) || !RatingPos1.equals(RatingPos)) { //Детектируем изменения в переменных KARMA1 = KARMA; RATING1 = RATING; RatingPos1 = RatingPos; flag = 1; //И поднимаем флаг } vTaskDelay(20000); //Пауза 20 секунд } } void Graph(void* pvParameters) { //Поток отрисовки на дисплей configTime(gmtOffset_sec, 0, ntpServer1, ntpServer2); //Инициализируем службу времени dht.begin(); //Инициализируем датчик температуры display.init(); //Инициализируем дисплей display.setRotation(3); display.clearScreen(); //Очистка экрана display.setTextColor(GxEPD_BLACK); display.fillScreen(GxEPD_WHITE); Static(); // Отрисовка статического изображения display.display(false); //Полный вывод на дисплей for (;;) { //Цикл потока отрисовки на дисплей updLocalTime(); //Обновляем время в переменной if (millis() > ms + 1000) { //Обновляем показания датчика раз в секунду ms = millis(); HumR = dht.readHumidity(); TempR = dht.readTemperature(); } if (!isnan(HumR) || !isnan(TempR)) { //Если значения не NAN, копируем в перменные Hum = HumR; Temp = TempR; } if (millis() > ms1 + 1000) { //Моргалка для потери WiFi ms1=millis(); blink = !blink; } Dynamic(); // Функция отрисовки меняющихся данных } } void loop() { //Не используется }
Работа с WiFi
void Connect(void) { WiFi.mode(WIFI_STA); delay(10); WiFi.begin(ssid, password); delay(10); while (WiFi.status() != WL_CONNECTED && count < 15) { count++; delay(500); } delay(10); } void Reconnect(void) { KARMA = "000"; RATING = "000.0"; RatingPos = "999"; WiFi.disconnect(); vTaskDelay(1000); WiFi.begin(ssid, password); count = 0; while (WiFi.status() != WL_CONNECTED && count < 15 ) { count++; delay(500); } }
Графика
void Static() { display.setFont(&FreeMonoBold12pt7b); display.fillRect(0, 0, 296, 20, GxEPD_BLACK); display.setTextColor(GxEPD_WHITE); display.setCursor(10, 16); display.print("HabraTab"); display.fillRect(10, 80, 276, 2, GxEPD_BLACK); display.fillRect(15, 50, 73, 2, GxEPD_BLACK); display.fillRect(103, 50, 90, 2, GxEPD_BLACK); display.fillRect(208, 50, 73, 2, GxEPD_BLACK); display.setTextColor(GxEPD_BLACK); display.setCursor(18, 72); display.print("Karma"); display.setCursor(115, 72); display.print("Score"); display.setCursor(215, 72); display.print("R No"); display.setFont(&FreeSerifBoldItalic18pt7b); display.fillRect(15, 22, 100, 26, GxEPD_WHITE); display.setCursor(15, 45); display.print(KARMA.toInt()); display.fillRect(110, 22, 100, 26, GxEPD_WHITE); display.setCursor(110, 45); display.print(RATING.toFloat(), 1); display.fillRect(220, 22, 85, 26, GxEPD_WHITE); display.setCursor(220, 45); display.print(RatingPos.toInt()); display.setFont(&FreeMonoBold12pt7b); display.setCursor(5, 100); display.print("@"); display.print(USER); display.setTextColor(GxEPD_BLACK); display.fillRect(0, 108, 296, 20, GxEPD_BLACK); } void Dynamic() { display.setFont(&FreeMonoBold12pt7b); display.fillRect(145, 0, 150, 20, GxEPD_BLACK); if (!noWiFi || blink) { display.fillCircle(148, 9, 3, GxEPD_WHITE); display.fillRect(156, 6, 2, 8, GxEPD_WHITE); if (WS == 1) { display.fillRect(162, 4, 2, 12, GxEPD_WHITE); } if (WS == 2) { display.fillRect(162, 4, 2, 12, GxEPD_WHITE); display.fillRect(168, 2, 2, 16, GxEPD_WHITE); } } display.setTextColor(GxEPD_WHITE); display.setCursor(175, 16); display.print(TimeDisp); vTaskDelay(1); if (flag) { display.setFont(&FreeSerifBoldItalic18pt7b); display.setTextColor(GxEPD_BLACK); display.fillRect(15, 22, 95, 26, GxEPD_WHITE); display.setCursor(15, 45); display.print(KARMA.toInt()); display.fillRect(110, 22, 95, 26, GxEPD_WHITE); display.setCursor(110, 45); display.print(RATING.toFloat(), 1); display.fillRect(220, 22, 76, 26, GxEPD_WHITE); display.setCursor(220, 45); display.print(RatingPos.toInt()); flag = 0; } display.setFont(&FreeMonoBold12pt7b); display.fillRect(0, 108, 200, 20, GxEPD_BLACK); display.setTextColor(GxEPD_WHITE); display.setCursor(10, 124); vTaskDelay(1); display.print("T"); display.print(Temp, 1); display.setFont(&FreeMonoBold9pt7b); display.setCursor(82, 118); display.print("o"); display.setFont(&FreeMonoBold12pt7b); display.setCursor(120, 124); display.print("H"); display.print(Hum, 1); display.print("%"); vTaskDelay(10); display.display(true); vTaskDelay(10); }
Время
void setTime (){ sntp_set_time_sync_notification_cb(timeavailable); configTime(gmtOffset_sec, 0, ntpServer1, ntpServer2); } void updLocalTime() { struct tm timeinfo; getLocalTime(&timeinfo); strftime(TimeDisp,9, "%H:%M:%S", &timeinfo); } // Callback function (get's called when time adjusts via NTP) void timeavailable(struct timeval *t) { }
Все остальное не так интересно‑ работа с дисплеем, датчиком температуры и влажности, время и подключение/пере подключение к WiFi. Ну разве что пара слов о FreeRTOS.
Так как время хочется видеть актуальное, вплоть до секунды, чтобы обращение к серверу ему не мешало‑ отрисовка на дисплей вынесена в отдельный поток. Так у нас все что касается дисплея исполняется на одном ядре, а все что касается сети‑ на другом.
Используемые библиотеки:
Библиотеки для работы с WiFi и HTTP уже есть в ядре ESP32 для Arduino IDE.
Железо
Собираем из модулей

Хотел было поставить TFT на 3.5 дюйма, но что-то лень рисовать новую плату, а из старых проектов особо ничего не подгонишь. Вспомнил что есть у меня E-Ink дисплеи, которые я еще нигде не использовал. А тут как раз- и светится по ночам не будет, и данные часто обновлять не обязательно. Выбор пал на небольшой дисплей диагональю 2.9 дюйма c разрешением 296х128 пикселей. Но версия с TFT конечно будет, и даже будет аватар показывать, но позже.

Сердцем конечно будет ESP32. Для 8266 данных многовато будет, работа со строками и файлами занимает много ресурсов, да и второе ядро выделенное только на работу с сетью уменьшает вероятность глюков. К тому-же на будущее планируется TFT дисплей и графика, а это требует ресурсов. Здесь подойдет любая отладочная плата с ESP32 Wroom на борту.
Подключение
Тут ничего особенного, у модуля 4 проводной SPI + 2 контакта. К ESP32 цепляется просто:

// BUSY -> 4, RST -> 16, DC -> 17, CS -> SS(5), CLK -> SCK(18), DIN -> MOSI(23), GND -> GND, 3.3V -> 3.3V
Датчик DHT22 подключается выходом на 27 ногу ESP32, естественно еще питание.
Железная версия
Для законченной версии была нарисована схема:

Тут стандартная для ESP32 обвязка и UART мост на CH340. По питанию стоит 1117 на 3.3В. Обвязка дисплея стандартная из даташита.
И плата:

Корпуса как такового не подразумевается, плата будет стоять на ножке из металла:



Ножка вырезана на лазере из нержавеющей стали толщиной 1мм. Метал достаточно тонкий и нижняя часть сгибается просто плоскогубцами. Чертеж в формате DXF будет в файлах.
На нижние грани рекомендую наклеить резину или вспененный уплотнитель на самоклеящейся основе.

Навесные сопли на фото выше указывают на мою забывчивость. Первоначально развел плату под CH340C, и уже при сборке оказалось что они у меня кончились, пришлось ставить CH340G и навешивать кварц. Куда делась крышка ESP32 даже не спрашивайте:)
Файлы
Файлы печатной платы и схемы в DipTrace + исходник в Arduino IDE:
https://github.com/ENGIN33RRR/HabraTab
Вопрос к читателям
Предлагаю пройти небольшой опрос, да и ваше мнение в комментариях будет интересно.
Нужен ли такой девайс хабровчанам? Какой функционал хотелось бы увидеть? Может какие уведомления, или данные с сайта для читателей, но не писателей? В общем предлагайте, а я буду пилить софт и выкладывать. Устройство прописалось на столе возле монитора и всегда подключено к компьютеру, так что залить новый код дело одной минуты.
После публикации буду особенно часто поглядывать на табло, показания которого полностью зависят от ваших плюсов 😉
ссылка на оригинал статьи https://habr.com/ru/post/712114/
Добавить комментарий