|300


Понравилась статья HabraTab — девайс для хаброзависимых, где описана разработка устройства для визуализации рейтинга пользователя на Хабре.


И мне очень захотелось подобное устройство, вот только было несколько но:


  • Очень лень было делать, заказывать и паять печатную плату
  • Еще хотелось выводить рейтинг последней статьи, но хардкодить адрес и каждый раз пересобирать прошивку показалось очень муторно.
  • Разработка на С/С++ меня не пугает, так как занимаюсь этим более 20 лет, но писать что-то под Arduino у меня душа не лежит. И это не говоря про необходимость настройки системы сборки под конкретный микроконтроллер.

Короче, немного поразмыслив, было принято решение делать свое устройство для визуализации рейтингов на Хабре, и как обычно с блекджеком и… ну вы поняли. И самое главное, чтобы можно было собирать устройство из покупных деталей с Алиэкспресса для максимально простого повторения и кодить на чем-нибудь попроще, чем на С/С++.


Аппаратная часть


Не мудрствуя лукаво, нашел самый дешевый микроконтроллер в связке с экраном:



|300


А так как Raspberry Pi Pico W поддерживает разработку не только на С/С++, но и имеет интерпретатор Micropython, то на этом я и решил остановиться.


Характеристики Raspberry Pi Pico W
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 мм


|800

Даташит на микроконтроллер


Описание работы с отладочной платой


Принципиальной схемы 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 и наслаждаемся результатом

Все должно работать примерно вот так:


|300


Комментарии (12)


  1. dlinyj
    00.00.0000 00:00
    +5

    Какая великолепная годнота! Просто очень круто, спасибо автор!


    1. rsashka Автор
      00.00.0000 00:00
      +3

      Спасибо!


  1. Exosphere
    00.00.0000 00:00
    +3

    И встроенный пульсокисметр, чтобы фиксировать волнение в момент публикации и при обновлении страницы с рейтингом и комментариями :-)


    1. rsashka Автор
      00.00.0000 00:00
      +1

      Это конечно шутка, но наверно в каждой шутке есть что-то и от серьезного.
      Для Ардуино есть подобный шильд, можно и подключить :-)
      Только носить устройство будет неудобно, оно большое. А вот если бы было что-то похожее в маленьком корпусе


      1. vconst
        00.00.0000 00:00
        +1

        15*20 мм, вроде?
        Ну… Не очень мало. Но если вшить в тканевый браслет и подпаяться чем-то типа мгтф — то будет довольно сносно


        1. rsashka Автор
          00.00.0000 00:00
          +1

          Ну если с подключенным проводом сидеть, то наверно можно, но мне кажется, что будет не очень удобно. Разве что, периодически проверятся :-)


          1. dlinyj
            00.00.0000 00:00
            +2

            Обпубликовал, жмакаешь F5, смотришь комментарии и графики сердцебиения :)


          1. vconst
            00.00.0000 00:00
            +1

            Значит, с другой стороны браслета, надо какой-то контроллер с ble… И чем то все это питать еще. Так и бывает, одно за другое и у тебя на руке уже что-то размером с пип-бой ))


            1. rsashka Автор
              00.00.0000 00:00

              Наверно это и останавливает от постоянного наращивания всяческого функционала в носимых устройствах.


              1. vconst
                00.00.0000 00:00

                Это не сразу приходит )


  1. gogalaim
    00.00.0000 00:00
    +3

    А где обещанные женщины легкого поведения?


    1. rsashka Автор
      00.00.0000 00:00
      +2

      Ну вряд ли вы всерьез надеялись увидеть на Хабре фотографии женщин легкого поведения :-)