Девайс HabrScore для хаброзависимых с блекджеком и …

от автора

|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


ссылка на оригинал статьи https://habr.com/ru/company/timeweb/blog/723334/


Комментарии

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

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