HabraTab — девайс для хаброзависимых

от автора

Что-то часто стал заглядывать в профиль после каждой новой публикации. Так вот я и решил сделать табло, которое стояло бы на столе, и показывало место в рейтинге, карму, ну и само значение очков рейтинга.

Для желающих повторить подразумевается как возможность сборки из модулей, так и нормальная железка. Но устройство в общем очень даже универсальное, полностью совместимое с 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

Вопрос к читателям

Предлагаю пройти небольшой опрос, да и ваше мнение в комментариях будет интересно.

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

После публикации буду особенно часто поглядывать на табло, показания которого полностью зависят от ваших плюсов 😉

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Итересно было бы…
22.22% Вытравить плату и собрать девайс самому 10
44.44% Собрать на готовых модулях 20
42.22% Купить набор для сборки 19
26.67% Купить готовый девайс 12
Проголосовали 45 пользователей. Воздержались 15 пользователей.

ссылка на оригинал статьи https://habr.com/ru/post/712114/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *