Что-то часто стал заглядывать в профиль после каждой новой публикации. Так вот я и решил сделать табло, которое стояло бы на столе, и показывало место в рейтинге, карму, ну и само значение очков рейтинга.
Для желающих повторить подразумевается как возможность сборки из модулей, так и нормальная железка. Но устройство в общем очень даже универсальное, полностью совместимое с 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: %sn", 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
Вопрос к читателям
Предлагаю пройти небольшой опрос, да и ваше мнение в комментариях будет интересно.
Нужен ли такой девайсам? Какой функционал хотелось бы увидеть? Может какие уведомления, или данные с сайта для читателей, но не писателей? В общем предлагайте, а я буду пилить софт и выкладывать. Устройство прописалось на столе возле монитора и всегда подключено к компьютеру, так что залить новый код дело одной минуты.
После публикации буду особенно часто поглядывать на табло, показания которого полностью зависят от ваших плюсов ;)
Автор: Владислав