Как стать автором
Обновить

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

Время на прочтение 9 мин
Количество просмотров 13K
Туториал

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

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

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

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

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Итересно было бы…
29.41% Вытравить плату и собрать девайс самому 65
33.94% Собрать на готовых модулях 75
42.53% Купить набор для сборки 94
24.89% Купить готовый девайс 55
Проголосовал 221 пользователь. Воздержались 94 пользователя.
Теги:
Хабы:
Всего голосов 145: ↑143 и ↓2 +141
Комментарии 53
Комментарии Комментарии 53

Публикации

Истории

Работа

QT разработчик
15 вакансий
Программист C++
128 вакансий