Понравилась статья HabraTab — девайс для хаброзависимых, где описана разработка устройства для визуализации рейтинга пользователя на Хабре.
И мне очень захотелось подобное устройство, вот только было несколько но:
- Очень лень было делать, заказывать и паять печатную плату
- Еще хотелось выводить рейтинг последней статьи, но хардкодить адрес и каждый раз пересобирать прошивку показалось очень муторно.
- Разработка на С/С++ меня не пугает, так как занимаюсь этим более 20 лет, но писать что-то под Arduino у меня душа не лежит. И это не говоря про необходимость настройки системы сборки под конкретный микроконтроллер.
Короче, немного поразмыслив, было принято решение делать свое устройство для визуализации рейтингов на Хабре, и как обычно с блекджеком и… ну вы поняли. И самое главное, чтобы можно было собирать устройство из покупных деталей с Алиэкспресса для максимально простого повторения и кодить на чем-нибудь попроще, чем на С/С++.
Аппаратная часть
Не мудрствуя лукаво, нашел самый дешевый микроконтроллер в связке с экраном:
-
Raspberry Pi Pico W (RP2040) с модулем беспроводной связи ~ чуть более 600 руб.
-
Макетная платы для Pi Raspberry Pi Pico с 3,5-дюймовым сенсорным экраном (с прицелом на будущее развитие) ~ 1600 руб.
А так как Raspberry Pi Pico W поддерживает разработку не только на С/С++, но и имеет интерпретатор Micropython, то на этом я и решил остановиться.
Raspberry Pi Pico W — это плата микроконтроллера на основе микроконтроллера Raspberry Pi RP2040. Он имеет два ядра ARM Cortex-M0+, работающими на частоте до 133 МГц, 256кб RAM, 30 GPIO и большим количеством разнообразных интерфейсов. Для хранения прошивки и данных используется флеш память 2 Мб для кода и данных.
Характеристики:
- USB 1.1 с поддержкой устройства и хоста
- Низкомощный режим сна и спящий режим
- 26 многофункциональных контактов GPIO
- 2 × SPI, 2 × I2C, 2 × UART, 3 × 12-bit ADC, 16 × управляемых ШИМ каналов
- Часы реального времени (RTC)
- Датчик температуры
- Ускоренные вычисления с плавающей точкой на чипе
- 8 программируемых I/O (PIO) портов для пользовательской периферийной поддержки
- Wi-Fi модуль
Входное напряжение питания:
- Через USB: 5 В
- Через пин VSYS: 1,8–5,5 В
- Напряжение логических уровней: 3,3 В
- Потребляемый ток: до 140 мА
- Размеры: 52,7×21×12,3 мм
Описание работы с отладочной платой
Принципиальной схемы HabrScore не привожу, так как никаких изменений в покупные платы не вносилось, а вся сборка устройства заняла 5 секунд.
Программная часть
Чтобы иметь всегда актуальную информацию не только о рейтинге пользователя, но и о его последней публикации, делаю запрос по адресу https://habr.com/ru/users/ <USER> /posts/
. На этой странице присутствует и рейтинги пользователя и список его статей, поэтому достаточно одного запроса к Хабру, чтобы получить всю интересующую информацию.
Парсинг страницы сперва делал на базе обычной версии Python и только после его отладки портировал наработки на MicroPython.
# Read about program at https://habr.com/ru/post/723334/
# Source at https://github.com/rsashka/HabrScore
USER = 'rsashka'
WIFI_SSID = ''
WIFI_PASS = ''
TIMEZONE_SEC = 10800
QUERY_SEC = 20
import os
import sys
import network
import time
import ntptime
import ussl
import usocket
import gc
import machine
# Query Habr and parse response
def habr_query(user, marks):
result = dict(marks)
for key in marks:
result[key] = None
try:
url = 'https://habr.com/ru/users/'+user+'/posts/'
_, _, host, path = url.split('/', 3)
# copy & paste from urequests
s = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
ai = usocket.getaddrinfo(host, 443, 0, usocket.SOCK_STREAM)
ai = ai[0]
s = usocket.socket(ai[0], usocket.SOCK_STREAM, ai[2])
s.connect(ai[-1])
s = ussl.wrap_socket(s, server_hostname=host)
s.write(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8'))
data = None
while True:
# Read small chunks because there is not enough RAM for readline()
temp = s.read(1024)
if (data is not None):
data = temp
temp = s.read(1024)
data += temp
else:
# First reading
data = b''
continue
if data:
# We are looking for only the necessary keys as substrings
for key in marks:
# The key is looked up only once
if (result[key] is not None):
continue
start = data.find(marks[key])
if (start == -1):
continue
# Get data between angle brackets
pos = data.find(b">", start)
end = data.find(b"<", pos)
if (pos>0 and end>0):
value = data[pos:end]
result[key] = value.decode().strip(' \n\t\r><');
else:
# End of data
break
except:
print('\nBegin machine.soft_reset()\n')
# Software reset and restart MicroPython
machine.soft_reset()
return result
# Output screet on the EP-0164 (https://aliexpress.ru/item/1005004743550177.html?sku_id=12000030313984981)
import machine
from micropython import const
from ili934xnew import ILI9341, color565
import glcdfont
import tt14
import tt24
import tt32
from random import randint
SCR_WIDTH = const(320)
SCR_HEIGHT = const(240)
SCR_ROT = const(3)
#fonts = [glcdfont,tt14,tt24,tt32]
spi = machine.SPI(
0,
baudrate=40000000,
miso=machine.Pin(4),
mosi=machine.Pin(7),
sck=machine.Pin(6))
print(spi)
display = ILI9341(
spi,
cs=machine.Pin(13),
dc=machine.Pin(15),
rst=machine.Pin(14),
w=SCR_WIDTH,
h=SCR_HEIGHT,
r=SCR_ROT)
update_screen = True
CLR_BG = color565(255, 255, 255)
def message(msg, msg2=None, clr_txt=color565(0, 0, 0), clr_bg=CLR_BG):
update_screen = True
display.set_color(color565(0, 0, 0), clr_bg)
display.erase()
display.set_color(clr_txt, clr_bg)
display.set_pos(15,110)
display.set_font(tt32)
display.print(msg)
if(msg2):
display.set_pos(15,150)
display.set_font(tt24)
display.print(msg2)
def error(msg, msg2=None):
message(msg, msg2, color565(255, 0, 0))
def status(msg):
display.fill_rectangle(0, 220, SCR_WIDTH, 30, CLR_BG)
display.set_color(color565(100, 100, 100), CLR_BG)
display.set_pos(5,223)
display.set_font(tt14)
display.print(msg)
res = None
print(os.uname());
# Print title
sync_done = False
last_query = time.time()
update_screen = True
counter = 0;
# Main loop
led = machine.Pin(0, machine.Pin.OUT)
wlan = network.WLAN(network.STA_IF)
while(True):
led.value(not led.value())
wlan.active(True)
if(not wlan.isconnected()):
status('Connect to WiFi ...')
#print(wlan.scan())
start_s = time.time()
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
if (time.time() - start_s) > 5:
error('WiFi connection error!', 'Check SSID and password.')
time.sleep(1)
counter+=1
if(counter > 10):
error('Performing a hard reboot!')
time.sleep(2)
# Hardware reset
machine.reset()
wlan.active(True)
time.sleep_ms(500)
wlan.connect(WIFI_SSID, WIFI_PASS)
print(wlan.ifconfig())
#print(wlan.status())
if(not sync_done):
status('Sync local time ...')
try:
print("Local time before synchronization:%s" %str(time.localtime()))
ntptime.settime()
print("Local time after synchronization:%s" %str(time.localtime()))
sync_done = True
except:
error('Error syncing time')
print("Error syncing time")
time.sleep(1)
# Search anchors in html page
query = {
# For user
'KARMA': b'tm-karma__votes',
'SCORE': b'tm-votes-lever__score-counter tm-votes-lever__score-counter tm-votes-lever__score-counter_rating',
# For article
'VIEWS': b'class="tm-icon-counter__value',
'VOTES': b'class="tm-votes-meter__value tm-votes-meter__value',
'BOOKMARK': b'bookmarks-button__counter',
'COMMENTS': b'tm-article-comments-counter-link__value',
}
HEAD_BGR = color565(10, 0, 0)
if(update_screen):
display.set_color(color565(0, 0, 0), CLR_BG)
display.erase()
display.fill_rectangle(0, 0, SCR_WIDTH, 40, HEAD_BGR)
display.set_color(color565(255, 255, 255), HEAD_BGR)
display.set_pos(10,4)
display.set_font(tt32)
display.print("HabrScore")
update_screen = False
if ((res is None) or (time.time() - last_query > QUERY_SEC)):
last_query = time.time()
status('Request about user "@'+USER+'"')
new = habr_query(USER, query)
if(new['SCORE'] and new['KARMA']):
res = new
status('Request completd')
else:
status('Request fail. Use old data')
#res = {'VOTES': '+1', 'BOOKMARK': '14', 'KARMA': '96', 'SCORE': '52.1', 'COMMENTS': '29', 'VIEWS': '4.2K'} #habr_query(USER, query)
else:
display.set_pos(200,4)
display.set_font(tt32)
display.set_color(color565(255, 255, 255), HEAD_BGR)
if(sync_done):
_, _, _, hour, minute, second, _, _ = time.localtime(time.time() + TIMEZONE_SEC)
curr_time = "{:02}:{:02}:{:02}".format(hour, minute, second)
else:
curr_time = "No time"
display.print(curr_time)
status("Score update after {} seconds".format(last_query + QUERY_SEC - time.time()))
time.sleep(1)
continue
print(res)
# time.sleep(5)
# Print user rating
# USER KARMA SCORE
USER_OFFSET = 50
display.set_pos(4, USER_OFFSET+4)
display.set_font(tt24)
display.set_color(color565(0, 0, 0), CLR_BG)
display.print(" User:")
display.set_pos(80, USER_OFFSET)
display.set_font(tt32)
display.set_color(color565(100, 150, 180), CLR_BG)
display.print("@"+USER)
if(res['KARMA'] and len(res['KARMA'])>1 and res['KARMA'][0] == '-'):
CLR_KARMA = color565(255, 0, 0)
else:
CLR_KARMA = color565(0, 255, 0)
display.set_pos(4, USER_OFFSET+45)
display.set_font(tt24)
display.set_color(color565(0, 0, 0), CLR_BG)
display.print("Karma:")
display.set_pos(85, USER_OFFSET+40)
display.set_font(tt32)
display.set_color(CLR_KARMA, CLR_BG)
if(res['KARMA']):
display.print(res['KARMA'])
display.set_pos(150, USER_OFFSET+45)
display.set_font(tt24)
display.set_color(color565(0, 0, 50), CLR_BG)
display.print("Score:")
display.set_pos(250, USER_OFFSET+40)
display.set_font(tt32)
if(res['SCORE']):
display.print(res['SCORE'])
# Print the rating of the latest article
# VOTES BOOKMARK COMMENTS VIEWS
ARTICLE_OFFSET = 130
display.set_pos(20, ARTICLE_OFFSET+4)
display.set_font(tt24)
display.set_color(color565(120, 120, 120), CLR_BG)
display.print("Rating of the latest article:")
if(res['VOTES'] and len(res['VOTES'])>1 and res['VOTES'][0] == '-'):
CLR_VOTES = color565(255, 0, 0)
else:
CLR_VOTES = color565(0, 255, 0)
display.set_pos(30, ARTICLE_OFFSET+40)
display.set_font(tt32)
display.set_color(CLR_VOTES, CLR_BG)
if(res['VOTES']):
display.print(res['VOTES'])
display.set_pos(90, ARTICLE_OFFSET+40)
display.set_font(tt32)
display.set_color(color565(0, 0, 0), CLR_BG)
if(res['VIEWS']):
display.print(res['VIEWS'])
display.set_pos(180, ARTICLE_OFFSET+40)
display.set_font(tt32)
display.set_color(color565(0, 0, 0), CLR_BG)
if(res['COMMENTS']):
display.print(res['COMMENTS'])
display.set_pos(260, ARTICLE_OFFSET+40)
display.set_font(tt32)
display.set_color(color565(0, 0, 0), CLR_BG)
if(res['BOOKMARK']):
display.print(res['BOOKMARK'])
display.set_font(tt14)
display.set_color(color565(180, 180, 180), CLR_BG)
display.set_pos(25, ARTICLE_OFFSET+70)
display.print('Votes')
display.set_pos(100, ARTICLE_OFFSET+70)
display.print('Views')
display.set_pos(160, ARTICLE_OFFSET+70)
display.print('Comments')
display.set_pos(240, ARTICLE_OFFSET+70)
display.print('Bookmarks')
sys.exit()
Для разработки и отладки кода под HabrScore пользовался средой разработки Thony Python IDE
При портирование на MicroPython пришлось немного повозится из-за того, что Raspberry Pi Pico W вообще очень мелкий микроконтроллер с мизерным объемом RAM, которого иногда не хватает для буфера под считанную страницу, а иногда бывают глюки и при установке обычного SSL соединения.
Поэтому пришлось немного переделать функцию чтения GET запросов и сделать потоковый парсинг получаемых данных, а в некоторых местах даже добавить перезагрузку всего устройства.
Финальные исходники прошивки на MicroPython с библиотеками и шрифтами для экрана можно скачать в репозитории на GitHub
Инструкция по запуску HabrScore
Запустить все это хозяйств можно следующим образом:
- Скачиваем загрузчик MicroPython для Raspberry Pi Pico W
- При подключении USB к компьютеру удерживаем нажатой кнопку Boot Select. После этого в доступных USB девайсах должен быть виден флеш накопитель на который и нужно записать скаченный загрузчик. После этого плата сама отключится и перезагрузится уже с новой прошивкой с поддержкой MicroPython.
- Скачиваем репозиторий с кодом.
- В файле main.py вписываем имя пользователя Хабра, название WiFi сети и пароль для неё
- Запускаем Thony Python IDE и подключаем плату
- В параметрах Thony Python IDE выбираем правильный интерпретатор MicroPython
- Сохраняем все *.py файлы на устройство (Сохранить как… RP2040 Device)
- Проверяем работу девайса HabrScore и наслаждаемся результатом
Все должно работать примерно вот так:
Комментарии (12)
Exosphere
00.00.0000 00:00+3И встроенный пульсокисметр, чтобы фиксировать волнение в момент публикации и при обновлении страницы с рейтингом и комментариями :-)
rsashka Автор
00.00.0000 00:00+1Это конечно шутка, но наверно в каждой шутке есть что-то и от серьезного.
Для Ардуино есть подобный шильд, можно и подключить :-)
Только носить устройство будет неудобно, оно большое. А вот если бы было что-то похожее в маленьком корпусеvconst
00.00.0000 00:00+115*20 мм, вроде?
Ну… Не очень мало. Но если вшить в тканевый браслет и подпаяться чем-то типа мгтф — то будет довольно сносноrsashka Автор
00.00.0000 00:00+1Ну если с подключенным проводом сидеть, то наверно можно, но мне кажется, что будет не очень удобно. Разве что, периодически проверятся :-)
vconst
00.00.0000 00:00+1Значит, с другой стороны браслета, надо какой-то контроллер с ble… И чем то все это питать еще. Так и бывает, одно за другое и у тебя на руке уже что-то размером с пип-бой ))
dlinyj
Какая великолепная годнота! Просто очень круто, спасибо автор!
rsashka Автор
Спасибо!